jvm 深入理解类加载机制

任何一个字节码流可以唯一定义一个类或接口,下文将字节码流统称为字节码文件

类生命周期:加载,连接(验证,准备,解析),初始化,使用,卸载 , 其中加载,验证,准备,初始化,使用,卸载等步骤相对顺序稳定,而解析阶段可以在初始化前进行操作,也可以在初始化操作后真正使用符号引用再进行解析操作,也正是因为可以在初始化后真正使用时进行解析操作,才可以支持java的动态解析,如:直到调用对象的方法时,再去解析方法引用,这种情况只有在运行时才能直到真正的调用对象

类加载过程:将指定环境下的字节码文件加载至虚拟机内存,经过加载,连接,解析操作形成正确有效可用的java对象,注意:此处java对象并非实例对象,而是指Class对象,用于定义java类元数据信息

加载阶段:包括三个步骤,1.将字节码文件通过类加载器加载至虚拟机内存 2.将字节码文件中静态结构转换为方法区的运行时数据结构  3.根据方法区运行时数据结构构造对象元对象Class 

将字节码文件加载至虚拟机内存,此项操作比较宽松,jvm并没有规范从哪里加载字节码文件,所以我们可以从本地文件系统加载字节码文件,也可以通过网络传输加载,还可以从zip压缩包中加载字节码文件,甚至可以从内存中计算生成字节码流记载至虚拟机内存。加载完字节码文件后,根据字节码文件静态数据结构将类信息,常量池等转换为方法区的运行时数据结构,最好根据方法区运行时数据结构生成类元对象Class,jvm并没有规范Class对象一定要存放在堆区,各类虚拟机可自主实现Class对象的存储位置,HotSpot虚拟机将Class对象存放在方法区

连接阶段:验证,准备,解析

验证:验证操作并不是等到加载操作完全完成后才开始执行,一般在进行类加载操作的时候会同步执行验证操作,验证操作具体包括:文件格式验证,元数据验证,字节码验证,符号引用验证。文件格式验证操作主要验证字节码文件是否符合jvm规范,比如文件魔术是否为cafebabe,文件的主次版本是否能被本虚拟机接受执行等操作,确保字节码执行不会危害虚拟机安全,虽然编译过程会对一些有编译错误的代码拒绝编译,但是由于虚拟机并没有规定字节码文件的来源,我们可以用16进制编辑器编写恶意代码,如果没有文件格式验证,可能会危害虚拟机的自身安全。元数据验证操作主要包括这个类是否有父类,除java.lang.Object每个类都要有父类,且只能有一个父类,一个类是否继承了不能被继承final的父类,一个实现类是否实现抽象父类或父接口的抽象方法,是否重写了父类的final方法等,确保类的元数据的正确性。字节码验证操作主要字节码的语义,比如类型是否强制转换为不能强转的类型,保证read,load操作相同类型,保证跳转指令不会跳转至方法体外的指令上去等操作。符号引用验证操作主要符号引用的合法性,结合解析阶段使用,确保符号引用能找到对应的类或接口,确保字段或方法的符号引用能找到对象的字段或方法,验证对符号引用表示的类型的访问权限等操作

准备:在方法区为字节码流分配内存,在Class对象为类字段赋初值,比如private static final a=100,此处在准备阶段类变量被分配内存并赋初值a=0 注意:此处只对类变量赋初值,对实例变量阶段不做处理,对常量会根据字段的ContantValue值对常量赋值,final int a=100 此处在准备阶段a被赋值为100

解析:将常量池的符号引用转换为直接引用,符号引用与本地机器无关,而且符号引用表示的类并不一定需要加载至内存,可以直接使用符号引用代表某个类,并不要指向具体的目标代码,但在使用的使用需要将符号引用表示的类加载至内存,直接内存与本地内存相关,可以根据直接引用直接或间接定位到目标代码,比如 java.long.Object符号引用表示指向Object类型的符号,此时Object类并不一定需要加载至内存,而将java.lang.Object转换为直接引用时,则java.lang.Object指向方法区Object类的地址。解析操作涉及常量池符号引用的解析,包括constant_class_info,constant_fieldref_info,constant_methodref_info,constant_interfacemethodref_info,constant_nameandtype_info,constant_methodtype_info,constant_methodhandle_info常量项的解析,除了constant_methodhandle_info常量项的解析操作,其他类型的解析操作解析成功一次后会对解析结果进行缓存,提高下次解析的效率,而constant_methodtype_info本身就是动态类型,每次的解析结果都不一样,则不会进行缓存,主要是类与接口的解析,字段解析,方法解析,接口方法解析,方法句柄解析。类与接口解析:如果对象类型为非数组引用类型,那么根据类或接口的全限定名通过类加载器加载至内存将符号引用转换为指向类内存结构的直接引用,若对象类型为数组类型,那么虚拟机直接构造数组类型内存结构,将符号引用指向内存数组对象的直接引用,然后判断当前类对引用类型的访问权限。字段解析:根据field_info中class_index属性找到字段所属类,若类不存在方法区,在进行类加载操作,若找不到类则直接失败,若类class存在,则在class类中查找与field简单名称和字段描述符都相同的属性(每个类中存储的字段都是本类声明的字段,不包括从父类或父接口继承来的字段),如果找到,则直接返回,若没有,则在父接口中递归查找,找到则返回,若没有,则再在父类中递归查找,若存在,则直接返回,若没有则失败。方法解析:方法解析与字段解析不一致,方法解析优先查本类,再递归查父类,最好递归查父接口,而字段解析中优先查本类,但再递归查父接口,最好递归查父类,方法解析其他具体步骤与字段解析一致,只是在父类或父接口的先后递归查找顺序不一致。接口方法解析独立出来,与方法解析有点不一样,因为接口方法只能出现在接口中,不会出现在类中,所以接口方法解析过程为根据class_index查找类或接口,如果找到class为类而不是接口时则直接失败,否则在本接口查找与name_and_type_index相同的方法简单名称和描述符,若找到则直接返回,没有则递归在父接口查找,如果找到则返回,没有找到则解析失败

初始化:初始化阶段是类加载的最后一个阶段,虚拟机根据代码中静态变量的赋值和静态代码块的操作按代码顺序自动生成<cinit>类初始化方法,根据开发人员的自主定义初始化类变量,类,抽象类,甚至接口在进行类加载时,虚拟机都会自动生成<cinit>,不过接口中不允许有静态代码块,也接口的类变量全是final常量,这些字段的赋值在准备阶段已经完成,所以接口中<cinit>方法没太大意义,所以<cinit>方法对类或接口并不是必须的,如果类或接口既没有静态语句块,也没有类变量的赋值语句,则虚拟机不会去创建<cinit>方法,且为了保证加载类的唯一性,虚拟机自动为<cinit>添加同步语义,即一个线程在进行类初始化时,其他对需要同一个类进行初始化操作的线程需要阻塞等待。jvm定义了有且只有下面5种情况会触发类初始化

1.实例化某类对象,访问类的静态字段,访问类的静态方法,即遇到new,putstatic,getstatic,invokestatic指令时如果类没有进行初始化则会进行类初始化

2.通过反射实例化对象,访问类的字段,方法时如果类没有进行初始化则会进行初始化操作

3.进行子类初始化时,如果发现父类没有进行初始化操作,则会先触发父类的初始化操作(根据此条规则说明第一个初始化的类一定是Object,因为Object是所有引用类型的父类,包括数组类型)

4.虚拟机会自动执行运行主方法的类的初始化操作,即运行main方法的类运行之前会进行初始化操作

5.当使用jdk7动态语言支持时,如果constant_methodhandle_info常量项的解析结果为ref_putstatic,ref_getstatic,ref_invokestatic时,如果解析结果对应的类没有进行初始化时,则会触发对应类的初始化操作

虚拟机规范表明有且只有上述五种情况会进行类初始化操作,上述操作被称为主动引用,其他一些方法看似调用了类的字段或方法,但不会触发对应类的初始化,那些操作被称为被动引用。被动引用举例:1.在子类中引用父类的类变量,一般来说子类会继承父类的类变量,但通过子类访问类变量时,只会触发父类初始化,而不会触发子类初始化  2.访问某个类的常量属性时,不会触发类初始化,常量赋值在准备阶段已经完成,引用常量字段可直接通过常量池访问,不会触发类初始化  3.初始化一个引用数组时,不会触发数组元素引用类型的初始化,而是进行数组元素组成的数组类型的初始化,如 Person[] o=new Person[10],不会触发person类初始化,而是触发[com.test.Person 类初始化 

类加载器:类加载器主要进行类加载过程,将特定环境下的字节码流加载至内存,类加载器主要考虑的问题:在哪里加载字节码流?如何加载字节码流,加载字节码流前的操作?将字节码流如何转换成Class对象?基于前面的问题,类加载器的主要方法有findClass(),defineClass(),finfClass方法用于在何处找到字节码流以及对字节码流的后续处理,defineClass方法用于将字节码流转换为Class对象。从何处加载字节码流:可以从本地文件系统直接加载.class字节码流文件,也可以从zip压缩文件加载字节码流,可以从网络传输中加载字节码流,可以内存中直接生成字节码流。对应字节码流的操作:加密解密操作。类加载器种类:启动类加载器BootstrapClassLoader,扩展类加载器ExtClassLoader,应用类加载器AppClassLoader,自定义类加载器,启动类加载器负责加载java_home/lib目录下的,或者加载虚拟机参数 -Xbootclasspath参数目录下的指定文件名表示的class,比如rt.jar,一些非法不能识别的文件名即使在加载目录下也不会被加载。ExtClassLoader扩展类加载器主要负责加载java_home/ext目录下的class文件,AppClassLoader应用类加载器主要负责加载classpath目录下的class文件,通过System.getProperty("java.class.path")可获取classpath。由于BootstrapClassLoader类加载器直接由C++代码实现,并不是ClassLoader子类,我们不能控制BootstrapClassLoader加载,由虚拟机自动控制,其次需要注意的是:AppClassLoader类加载器是ExtClassLoader类加载器的子类

双亲委派模型:当一个类加载器被系统要求去加载某个class文件时,这个类加载器首先判断自己是否有父类加载器,如果没有则由BootstrapClassLoader启动类加载器先进行类加载操作,如果有父类加载器,则先由父类加载器进行类加载操作,以上两种情况可将BootstrapClassLoader类加载器看成是所有类加载器的父类,即:可以说成先由父类加载器尝试加载类。如果父类加载器加载成功,则直接返回父类加载器加载的类,否则子类加载器本身进行加载类操作,如果加载成功,则返回自身加载的类,否则返回类加载失败。为什么要根据双亲委派模型进行类加载操作?保证加载类的全局唯一性,即父类加载器负责目录下的class文件不会被子类相同类型的class文件所覆盖,比如你可以在classpath目录下创建一个java.lang.Object字节码文件,根据双亲委派模型,会先由启动类加载器加载java_home/lin目录下的java.lang.Object字节码文件,不会去加载自主定义的字节码文件

猜你喜欢

转载自www.cnblogs.com/zzlove2018/p/9099477.html
今日推荐