目录:Class文件被载入虚拟机后,会做哪些额外的处理?类加载的具体步骤是怎么样的?
- Class文件的装载流程
- Class类型以文件形式存在,只有被Java虚拟机装载的Class类型才能在程序中使用;
- 系统装载Class类型可分为加载,连接和初始化3个步骤;
- 连接又分为验证,准备和解析3步;
- 类的装载条件
- Class只有在必须要使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。
- 一个类或接口在初次使用前,必须要进行初始化
- 所谓的"使用",是指主动使用,有以下几种情况:
- 当创建一个类的实例时,比如使用new关键字,或者通过反射,克隆,反序列化;
- 当调用类的静态方法时,即当使用了字节码invokestatic指令;
- 使用类或接口的静态字段时(final常量除外),比如,使用getstatic或者putstatic指令;
- 使用java.lang.reflect包中的方法反射类的方法时;
- 当初始化子类时,要求先初始化父类;
- 作为启动虚拟机,含有main()方法的那个类;
- 主动引用的示例
分析: 系统首先装载Parent类,之后装载Child类。符合主动装载中的两个条件,使用new关键字创建类的实例会装载相关类,以及在初始化子类时,必须先初始化分类 |
- 被动引用的示例
分析: 虽然在UseParent中,直接访问了子类对象,但是Child子类并未被初始化,只有Parent父类被初始化。在引用一个字段时,只有直接定义该字段的类,才会被初始化; |
- 注意,Child类没有被初始化,但此时Child类已经被系统加载,只是没有进入到初始化阶段;
- 使用-XX:+TraceClassLoading参数运行这段代码,输出结果:Child已经被加载入系统,但Child的初始化并未进行;
- final常量并不会引起类的初始化,示例:
|
在编译后的UseFinalField.class中,并没有引用FinalFieldClass类,而是将final常量直接存放到常量池中,因此,FinalFieldClass类自然不会被加载。 |
结果: FinalFieldClass类并没有因为其常量字段constString被引用而初始化。这是因为在Class文件生成时,final常量由于其不变性,做了适当的优化。分析UseFinalField类生成的Class文件,可以看到main()函数的字节码为: 在字节码偏移3的位置,通过ldc将常量池第22项入栈,在此Class文件中常量池第22项为: |
- 总结:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化;
- 加载类
- 加载类处于类装载的第一阶段,虚拟机要完成一下工作:
- 通过类的全名,获取类的二进制数据流;
- 解析类的二进制数据流为方法区的数据结构;
- 通过二进制信息,创建java.lang.Class类的实例,该类是反射得以实现的关键数据;
- 虚拟机获取二进制数据流的途径:
- 通过文件系统读入一个class后缀的文件;
- 或读入jar,zip等归档数据包,提取类文件;
- 实例:通过Class类,获取java.lang.String类的所有方法信息
- Java虚拟机完成类的加载是通过ClassLoader类加载器;
- 验证类
- 类被加载到系统后,开始连接操作,验证是该操作的第一步;
- 目的:
- 保证加载的字节码是合法,合理并符合规范的,验证流程大致如下:
1.判断类的二进制数据是否符合格式要求和规范; |
- 准备
- 类通过验证后,虚拟机会进入准备阶段。这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值;各类型初始值如下:
- 常量字段也会在准备阶段被附上正确的值,这个行为由虚拟机来做,属于变量的初始化;
- 这个阶段不会有任何代码被执行;
- 示例:
- 在类中定义如下常量:
- 在生成的class文件中,可以看到该字段含有ConsantValue属性,直接存放于常量池中。该常量constString在准备阶段被附上字符串"CONST"(并非由字节码引起的)
- 如果没有final的修饰,仅仅作为普通的静态变量:
- constString的赋值在函数clinit中发生,属于Java字节码的行为。字段constString上未携带任何数据信息,在clinit方法中,将字符串常量CONST通过ldc指令压栈,并通过putstatic语句进行赋值;
- ldc字节码会加载一个常量到操作数栈中,putstatic字节码设置给定的静态字段的值;
- 解析类
- 准备阶段之后,进入解析阶段,需要完成的工作有:
- 将类,接口,字段和方法的符合引用转为直接引用;
- 符号引用
- 就是字面量的引用,和虚拟机的内部数据结构,内存布局无法;
- 在Class类文件中,常量池就进行了大量的符号引用;
- 示例:查看System.out.println()的字节码
它使用了常量池第24项,常量池的结构如下图: |
常量池第24项被invokevirtual使用,顺着CONSTANT_Methodref #24的引用关系查找, |
- 符号引用只是一部分,当一个方法被调用,系统需要明确知道该方法的位置。Java虚拟机为每个类都准备了一张方法表,所有的方法都在这张表上。当需要调用一个类的方法时,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,使方法被调用成功;
- 综上,"解析"就是将符号引用转变为直接引用,也就是得到类,字段,方法在内存中的指针或者偏移量;
- 如果直接引用存在,可以肯定系统中存在该类,方法,字段等。但如果只存在符号引用,则不能确定;
- 关于String的说明,CONSTANT_String的解析
- 在Java代码中直接使用字符串常量,会在类中出现CONSTANT_String,它表示字符串常量,且引用一个CONSTANT_UTF8的常量项;
- Java虚拟机内部运行时,会维护一张字符串拘留表,它保存所有出现过的字符串常量,且没有重复项,只要以CONSTANT_String形式出现的字符串都会在这张表中;
- 使用String.intern()可以得到一个字符串的拘留表中的引用,因为拘留表不含重复项,所有字面相同的字符串的String.intern()方法,会返回相等的数据;
- 示例:
分析: |
- 初始化
- 该阶段是类装载的最后一个阶段。如果前面没有出现问题,那么类可以顺利装载到系统;
- 该阶段,类才会开始执行Java字节码;
- 该阶段,执行类的初始化方法,clinit;
- clinit是由编译器自动生成,由类静态成员的赋值语句以及static语句块合并产生的;
- 示例:
Java编译器会为这段代码生成如下的clinit: clinit函数中,整合了SimpleStatic类中的static赋值语句以及static语句块,先后对id和Number两个成员变量进行赋值 |
- 示例:在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类clinit总是在子类clinit之前被调用。也就是说,子类的static块优先级高于父类
分析: |
- 示例:Java编译器并不会为所有的类都产生clinit初始化函数。如果一个类既没有赋值语句,也没有static语句块,那么生成的clinit函数就应该为空,因此,编辑器就不会为该类插入clinit函数;
StaticFinalClass只有final常量,而final常量在准备阶段初始化,而并不在初始化阶段处理,因此对于StaticFinalClass来说,clinit就无事可做,因此,在产生的class文件中,没有该函数存在 |
- 类初始化的多线程问题
- 对于clinit函数的调用,虚拟机会在内部确保其多线程的安全性。当多个线程试图初始化同一个类时,只有一个线程可以进入clinit函数,其它线程等待,如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行clinit函数了(使用这个类时,虚拟机会直接返回给它已经准备好的信息)
- 示例:clinit是带锁线程安全的,在多线程环境下进行类初始化的时候,可能引起死锁,这种死锁很难发现,它看起来并没有可用的信息。
- 下段代码在展示类初始化的时候,产生了线程死锁的问题:
上段代码由3个类组成,StaticA,StaticB和StaticDeadLockMain。在StaticDeadLockMain中创建了两个线程,线程A试图去初始化StaticA,线程B尝试去初始化StaticB,在StaticA的初始化过程中,会去尝试初始化StaticB,同样在StaticB的初始化过程中,也去初始化StaticA,如下图: |