第17次文章:初探JVM
- 2019 年 10 月 8 日
- 筆記
类加载全过程
一、为什么研究类加载全过程?
(1)有助于了解JVM运行过程
(2)更深入了解java动态性(解热部署,动态加载),提高程序的灵活性。
二、类加载机制
JVM把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成JVM可以直接使用的java类型的过程。
(1)加载:
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器的参与。这个java.lang.Class对象其实就是我们在利用反射机制时候的那个镜像对象。
(2)链接:
将java类的二进制代码合并到JVM的运行状态之中的过程
(3)初始化:
-初始化阶段是执行类构造器<client>()方法的过程。类构造器<client>()方法是由编译器自动收集类中的所有类变量的赋值动作和静息态语句块(static块)中的语句合并产生的。
-当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先初始化其父类
-虚拟机会保证一个类的<client>()方法在多线程环境中正确加锁和同步。
三、下面我们主要对类的初始化时机进行讨论
1、类加载器的初始化时机主要分为主动引用和被动引用
(1)类的主动引用(一定会发生类的初始化)
-new一个类的对象
-调用类的静态成员(除了final常量)和静态方法
-使用java.lang.reflect包的方法对类进行反射调用
-当初始化一个类,如果其父类没有被初始化,则会先初始化其父类
(2)类的被动引用(不会发生类的初始化)
-当访问一个静态域时,只有真正声明这个域的类才会被初始化。通过子类引用父类的静态变量,不会导致子类初始化
-通过数组定义类引用,不会触发此类的初始化
-引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
2、为了测试类的初始化时机,我们首先创建一个父类A_Father和子类A
class A extends A_Father{ public static int width = 100; public static final int MAX = 100; static { System.out.println("静态初始化类A"); width = 300; } public A() { System.out.println("构造对象A"); } } class A_Father{ public static int hight = 100; static { System.out.println("静态初始化类A_Father"); } }
3、然后我们在main函数中进行更改不同的调用方法,查看所得结果,测试类的初始化时机。
package com.peng.test; /** * 测试类加载的全过程 */ public class Demo01 { static { System.out.println("静态初始化类Demo01"); } public static void main(String[] args) throws ClassNotFoundException { System.out.println("执行Demo01的main方法"); A a = new A(); A a2 = new A(); } }
我们查看一下结果:

tips:
(1)我们对Demo01类的初始化顺序进行分析。Demo01类的静态static块儿首先被执行,然后进入main方法,在new对象A的时候(主动引用)。由于A为父类A_Father的子类,并且父类没有被初始化,所以类加载器首先初始化的是父类A_Father(主动引用),而不是直接初始化子类A。
(2)与此同时,类只会被加载一次,当加载完以后,后面的调用不会再去重新加载这个类,所以在我们重新new一个对象a2的时候,仅仅调用了子类A的构造器,而并没有重新加载子类A和父类A_Father。
(3)在对类进行初始化的时候,编译器其实是将静态赋值语句,比如“public static int width = 100;”和静态初始化块“static{}”合并之后一起运行的。
4、下面我们再更改一种调用方法,测试其初始化时机
package com.peng.test; public class Demo01 { static { System.out.println("静态初始化类Demo01"); } public static void main(String[] args) throws ClassNotFoundException { System.out.println("执行Demo01的main方法"); System.out.println(A.width); } }
我们查看一下结果:

tips:
通过结果我们可以看出,当我们调用类的静态成员(除了final常量)和静态方法时,类A和A_Father被直接初始化了。这也属于主动引用中的一种。还有一种就属于反射调用类“Class.forName("com.peng.test.A");”此时也会出现上述结果。
5、我们现在将类的引用改为被动引用,重新查看一下结果
package com.peng.test; public class Demo01 { static { System.out.println("静态初始化类Demo01"); } public static void main(String[] args) throws ClassNotFoundException { System.out.println(A.MAX); A[] as = new A[10]; System.out.println(A.hight); } }
结果图:

tips:
(1)首先我们调用了A中的静态域MAX,由于MAX被final关键字修饰,属于常量,所以在编译的时候,常量就已经被存入了调用类的常量池中。在我们进行调用常量池中数据的时候,不需要初始化类就可以进行使用。(被动引用)
(2)在我们定义一个数组的时候,虽然使用了new关键字,但是也属于被动引用,所以没有进行初始化操作。(被动引用)
(3)在此方法调用中,我们使用了A.hight静态属性值,但是由于这个静态域是父类A_Father声明的,所以在调用A.hight静态域的时候,JVM并不会去初始化A类,而是仅仅初始化A_Father类。(被动引用)