专业的JAVA编程教程与资源

网站首页 > java教程 正文

菜鸟面试:就Java类的加载顺序(JVM工作原理)而谈

temp10 2024-10-19 14:57:55 java教程 8 ℃ 0 评论

这部分也是比较常考查的~ 上文有讲过一点,这篇将会以比较全面地以代码运行的角度去理解类加载器加载类的顺序~

菜鸟面试:就Java类的加载顺序(JVM工作原理)而谈

1、先用代码来看结果

先单独看一个类:

Java代码

  1. class Parent {

  2. {

  3. System.out.println("Parent的普通代码块");

  4. }

  5. static {

  6. System.out.println("Parent的静态代码块");

  7. }

  8. public Parent() {

  9. System.out.println("Parent的构造代码块");

  10. }

  11. public static void staticMethod1() {

  12. System.out.println("Parent的静态代方法");

  13. }

  14. public static void staticMethod2() {

  15. System.out.println("Parent的静态代方法2");

  16. }

  17. @Override

  18. protected void finalize() throws Throwable {

  19. super.finalize();

  20. System.out.println("销毁Parent类");

  21. }

  22. }

在另一个类里调用:

Java代码

  1. Parent.staticMethod1();

  2. Parent.staticMethod2();

  3. System.out.println();

  4. Parent parent = new Parent();

调用结果:

Parent的静态代码块

Parent的静态代方法

Parent的静态代方法2

Parent的普通代码块

Parent的构造代码块

说明:类中static 代码块在第一次调用时加载,类中static成员按在类中出现的顺序加载。普通代码块和构造代码块是在类初始化以后才会被执行。

下面再结合继承来看~

Java代码

  1. class Child extends Parent {

  2. {

  3. System.out.println("Child的普通代码块");

  4. }

  5. static {

  6. System.out.println("Child的静态代码块");

  7. }

  8. public Child() {

  9. System.out.println("Child的构造代码块");

  10. }

  11. public static void staticMethod1() {

  12. System.out.println("Child的静态代方法");

  13. }

  14. public static void staticMethod2() {

  15. System.out.println("Child的静态代方法2");

  16. }

  17. @Override

  18. protected void finalize() throws Throwable {

  19. super.finalize();

  20. System.out.println("销毁Child类");

  21. }

  22. }

在另一个类调用:

Java代码

  1. Child child = new Child();

调用结果:

Parent的静态代码块

Child的静态代码块

Parent的普通代码块

Parent的构造代码块

Child的普通代码块

Child的构造代码块

说明:如果一个类继承自一个父类,那么初始化它的时候,将会:父类的静态代码块--> 子类的静态代码块 -->父类的普通代码块 -->父类的默认构造器 -->子类的普通代码块 -->子类的构造器

如果承接上面,初始化子类后再调用销毁方法:

Java代码

  1. try {

  2. child.finalize();

  3. } catch (Throwable e) {

  4. e.printStackTrace();

  5. }

那么打印结果将会多上:

销毁Parent类

销毁Child类

说明:销毁的时候,将先调用父类的销毁方法finalize()再调用子类的finalize()。

2、再用理论来看原因——JVM工作原理

一个Java类的生命周期:

加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载

(1)加载

首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。

(2)链接:

验证:确保被加载类的正确性;

准备:为类的静态变量分配内存,并将其初始化为默认值;

解析:把类中的符号引用转换为直接引用;

(3)为类的静态变量赋予正确的初始值

3、类的初始化

(1)类什么时候才被初始化 (类初始化不一定是通过new)

1)创建类的实例,也就是new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName(“com.lyj.load”))

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM启动时标明的启动类,即文件名和类名相同的那个类

(2)类的初始化顺序

1)如果这个类还没有被加载和链接,那先进行加载和链接

2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)

3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。

4)总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法

4、类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如:

类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。加载类的方式有以下几种:

1)从本地系统直接加载

2)通过网络下载.class文件

3)从zip,jar等归档文件中加载.class文件

4)从专有数据库中提取.class文件

5)将Java源文件动态编译为.class文件(服务器)

5、加载器

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

(1)加载器介绍

1)BootstrapClassLoader(启动类加载器)

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。

2)ExtensionClassLoader(标准扩展类加载器)

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。

3)AppClassLoader(系统类加载器)

负责记载classpath中指定的jar包及目录中class

4)CustomClassLoader(自定义加载器)

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

(2)类加载器的顺序

1)加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

2)在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。

3)Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。

3、Java堆栈和方法区

1)栈

在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

每个线程包含一个栈区,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享(指的是线程共享,而给进程共享)。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

2)堆

堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后,在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。 引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是java比较占内存的原因。实际上,栈中的变量指向堆内存中的变量,这就是java中的指针!

Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

jvm只有一个堆区(heap)被所有线程共享。

3)方法区

方法区跟堆一样,被所有的线程共享。用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

4)常量池

常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名; 字段的名称和描述符; 方法和名称和描述符。虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在方法区(Method Area),而不是堆中。

4、JVM回收

谈JVM机制其实太难太复杂,大部分人不会。。。。所以可以用JVM回收时机以及减少gc开销的角度去答~

1)GC什么时候回回收?

对象没有引用

作用域发生未捕获异常

程序在作用域正常执行完毕

程序执行了System.exit()

程序发生意外终止(被杀进程等)

2)如何减少GC开销?

不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。

尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

能用基本类型如Int,Long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

3)JVM垃圾回收算法

标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,将还存另外一块上面,然后在把已使用过的内存空间一次清理掉。

标记-整理算法:标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。新生代都发现有大批对象死去,选用复制算法。老年代中因为对象存活率高,必须使用“标记-清理”或“标记-整理”算法来进行回收。

本人纯属菜鸟勿喷哦!记得加关注:lsl码农搞笑。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表