本章介绍了类加载过程的”加载“、”准备“、”验证“、”解析“和”初始化“5个阶段中虚拟机进行了哪些动作,还介绍了类加载器的工作原理及其对虚拟机的意义。
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止。
生命周期:加载、验证、准备、解析、初始化、使用和卸载7个阶段(粗体5个顺序是确定的)。
主动引用:
- 遇到new、getstatic、putstaic或invokestatic这4条指令
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,则需先触发其初始化。
- 当初始化一个类时,如果发现其父类还没有进行初始化,则需先解决父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的类(包含main()方法的那个类),虚拟机会先初始化这个主类
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
被动引用:
- 对于静态字段,只有直接定义这个字段的类才会被初始化
- 通过数组定义来引用类,不会触发类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载的过程
加载
类加载的第一个阶段。
过程:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据访问入口。
验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
过程:
- 文件格式验证(检查Class文件格式规范)
- 元数据验证(检查语义合法性、逻辑性)
- 字节码验证(最复杂的阶段)
- 符号引用验证(确保解析动作能正常执行)
准备
是正式为类变量分配内存并设置类变量初始化值的阶段。
重点:
- 这时进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量
- 默认变量初始化值(常量从常量池取对应的常量值)
解析
是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:符号引用以一组符号来描述所有引用的目标,符号可以是任何形式的字面量。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
- 解析对象(7类符号引用):
- 类或接口
- 字段
- 类方法
- 接口方法
- 方法类型
- 方法句柄
- 调用点限定符。
- 类或接口的解析(3个步骤)
- 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
- 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Inter”的形式,按上面的规则加载数组元素类型
- 如果上面解析没问题,在解析完成前不要进行符号引用验证,确认D是否具备对C的访问权限。
字段解析
类方法解析
接口方法解析
初始化
真正开始执行类中定义的Java程序代码,执行类构造器<cinit>()
方法的过程。
过程:
类加载器
“通过一个类的全限定名来获取描述此类的二进制流”这个动作放到Java虚拟机外部去实现,实现这个动作的代码模块称为“类加载器”。
类与类加载器
用于实现类的加载动作。
例:两个类是否相等(前提是被同一个类加载器加载的)
双亲委派模型
类加载器粗分2类:
- 启动类加载器(BootstrapClassLoader)
- 所有其他类的加载器(java.class)
细分3类:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 启动程序类加载器(ApplicationClassLoader)
过程:
优点:Java类随着它的类加载器一起具备了一种带有优先级的层次关系
代码实现:
破坏双亲委派模型
loadClass()失败->findClass()–JDK1.2后添加一个新的方法,为了兼容1.2之间的代码。
线程上下文类加载器
产生原因:基础类调回用户的代码(如:JNDI服务),Java中所有涉及SPI的加载动作基本上都采用这种方式