【JVM】第七章 虚拟机类加载机制
https://blog.****.net/luanlouis/article/details/50529868
一、概述
class文件中描述的各种信息,最终都要加载到虚拟机中之后才能运行和使用。
类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类的加载过程是在运行期间完成的和编译时无关。所以java是一种可以动态扩展的语言。
二、类加载生命周期
从上图可以看出整个生命周期分为:加载、验证、准备、解析、初始化、使用、卸载 7个过程
其中加载、验证、准备、初始化、卸载这5个过程是顺序的。
解析阶段不一定在初始化之前,为了支持java语言动态绑定的特点。有时候可以在初始化之后进行解析过程。
1、什么情况下必须进行初始化?
ps:先说明一下这里指的初始化不是我们java语言说的执行构造方法之后分配堆内存,而是执行static{}方法体的阶段。初始化的目的有两个。1、生成Class类对象 2、对static字段赋值
jvm中没有明确定义什么情况下进行类加载。但明确定义了什么情况下必须进行初始化。
1)遇到new(创建类实例)、getstatic(访问static类或字段)、putstatic(访问static类或字段)或invokestatic(调用类方法(static方法))这4条字节码指令时。
- 创建对象
- 使用static字段
- 调用static方法
2)使用java.lang.reflect包的方法对类进行反射调用时。如果类没有初始化,则需要先触发初始化。
3)当初始化一下类时,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。(Class类对象实例)
4)当虚拟机启动时,用户需要指定一个执行主类(包含main()方法的类),虚拟机会先初始化这个主类。
5)使用jdk1.7动态语言支持时。如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic\REF_invokestatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
二、类加载过程
-------------------------------加载阶段-------------------------------------------------
1、加载
“加载”是类加载的一个过程,不要混淆这两个概念。在“加载”阶段,虚拟机需要完成3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。(没有规定从哪获取,怎么获取。各种加载器就出现了)
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(加载到方法区运行时常量池中)
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
ps:这个过程需要类加载器参与。一会我们会讲类加载器的双亲委派模型。
-------------------------------连接阶段-------------------------------------------------
连接阶段:将java类的二进制代码合并到JVM的运行状态之中的过程
2、验证
目的为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1)文件格式验证
魔数、版本号、常量池常量类型等
2)元数据验证
进行语义分析,以保证描述的信息符合java语言规范。
3)字节码验证
对方法体进行验证,保证程序语义是合法的、符合逻辑的。保证方法体在运行时不会做出危害虚拟机的安全事件。
4)符号引用验证(解析阶段进行)
符号引用转化为直接引用,确保直接引用合法
3、准备
1、对static修鉓的字段进行开辟内存空间,设置默认值。
2、对有ConstantValue值的字段进行赋值(final修鉓的字段都有ConstantValue值)
4、解析
虚拟机将运行时常量池内的符号引用替换为直接引用的过程。(并非java.lang.Class对象)
就是在class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info这7种类型的常量出现。
符号引用:
发生在编译阶段,以一组符号表示所引用的目标,只要能在使用时无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关。引用的目标并一定已加载到内存中。
直接引用:
直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。在运行时常量池中记录直接引用,并把常量标识为已解析状态。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
-------------------------------初始化阶段-------------------------------------------
5、初始化
这个阶段才真正开始执行类中定义的java程序代码。
1)执行<clinit>()方法也就是class文件里的 static{}静态块。将static修饰的字段(非final)进行赋值。
2)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。也就是说父class的static{}先执行。
3)虚拟机地保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,一个class只会进行一次初始化动作。(怎么保障的,讲线程的时候再说)
4)接口不太一样。执行接口<clinit>()方法时不需要先执行其父接口<clinit>()方法。接口的实现类<clinit>()方法时也不需要执行该接口的<clinit>()方法。
ps:<clinit>()方法和init方法是不同的。不要混淆。这时候还没有new对象分配内存呢。
三、类加载器
刚上面也说了,在加载阶段需要类加载器参与将class文件加载到内存中来。
从哪里获取,怎么获取没有定义,这个动作放到虚拟机的外部实现,所以出现了可以满足各种场景的类加载器。
1、类与类加载器
每一种类加载器都拥有一个独立的类名称空间。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
即使这两个类源于同一个class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不同。
这里的“相等”包括代表类的class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定的情况。
2、双亲委派模型
1)启动类加载器
负责将存放在<JAVA_HOME>\lib目录下,或被-XbootClasspath参数指定的路径中,类库加载到虚拟机内存中。
2)扩展类加载器
这个加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中,或被java.ext.dirs系统变量指定的路径中的所在类库。
3)应用程序类加载器
这个加载器由sun.misc.Launcher$AppClassLoader实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值。它也称为系统类加载器,负责加载用户类路径(classpath)上所指定的类。
类加载器之间的层次关系,称为类加载的双亲委派模型。
工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成。每个层次都一样。只有父类加载去加载不成功,子类加载器才会尝试去加载。
优先顺序是:启动类加载器-》扩展类加载器-》应用程序类加载器-》自定义类加载器
双亲委派模型保证java程序的稳定动作很重要。
如类java.lang.Object它存在rt.jar中。无论哪个类加载器想要加载这个类,都会最终委托给启动类加载器去完成。因此object类在程序的各种类加载器环境中都是同一个类。
如果没有双亲委派模型,每个加载器都自行加载,这样就会出现多少object的Class实例。