JVM学习(六) JVM类加载机制
前言
侵删,记录学习笔记。
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
图 1 类的生命周期
加载,验证,准备,初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。
有且只有5种必须立即对类进行初始化的场景
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。例子场景:使用new实例化对象,读取或这是一个类的静态字段,调用一个类的静态方法的时候。
- 使用反射技术对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后
的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种场景的行为被称为对一个类进行主动引用。除此之外,所有引用类的方式都不会初始化,称为被动引用。
加载
在加载阶段,JVM需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类加载不一定要从一个class文件获取,既可以从ZIP包、JAR包或WAR包获取,也可以通过动态代理运行时计算生成,又或者由其他文件生成,如JSP文件转换成对应的Class类
验证
验证阶段是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配。
这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将 会在对象实例化时随着对象一起分配在Java堆中
这里说的初始值“通常情况”下是数据类型的零值。
public static int code=200;
变量code准备阶段过后的初始值是0而不是200。将code赋值为200的put static指令是程序被编译后,存放于类构造器方法中。
注意:
public static final int CODE=200;
如果变量被 static final修饰,在编译阶段会为CODE生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将CODE赋值为200
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用在Class文件中它以CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。
符号引用
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可 以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的 内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各 不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义 在Java虚拟机规范的Class文件格式中。
直接引用
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是 一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引 用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目 标必定已经在内存中存在。
初始化
初始化是类加载过程的最后一个阶段,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。从另外一个角度来讲,初始化阶段是执行类构造器<clinit>()方法的过程。
类构造器<clinit>
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定,静态代码块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不 需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的< clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定 是java.lang.Object。
类加载器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提 供了3种类加载器:
启动类加载器
负责加载JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被JVM认可的类。
扩展类加载器
负责加载JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器
负责加载用户路径(classpath)上的类库
JVM通过双亲委派模型来进行类的加载,同时我们也可所以通过继承
java.lang.ClassLoader实现自定义的类加载器
图 2 类加载器双亲委派模型
双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会去尝试自己去加载。
双亲委派的好处
比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器要加载这个类,最终都会委托给顶层的启动类加载器进行加载,这样就保证了使用不用的类加载器最终得到的是同样一个Object对象。
图 3 双亲委派过程示意图
OSGI(动态模型系统)
由于用户对于动态性的追求,代码热替换,模块热部署,使得双亲模型被"破坏"。
Sun公司所提出的JSR-294[1]、JSR-277[2]规范在与JCP组织的模块化规范之争中落败给JSR291(即OSGi R4.2),虽然Sun不甘失去Java模块化的主导权,独立在发展Jigsaw项目,但目 前OSGi已经成为了业界“事实上”的Java模块化标准[3],而OSGi实现模块化热部署的关键则是 它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类 加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
OSGI旨在为实现Java程序的模块化编程提供基础条件,基于OSGI的程序很可能可以实现模块级的热插拔功能。
参考资料
《Java虚拟机(第二版)》
JAVA核心面试知识整理.pdf