JVM- 技术专题 - 类加载机制
一、类加载机制
虚拟机把描述类的数据从Class文件 (二进制流) 加载到内存,并对数据结构进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java的类加载机制。类加载器加载的类可以是磁盘上的class文件,也可以是网络或者解压得到的二级制文件(符合class格式),这里的class文件是一串二进制的字节流。
类加载机制
二、类加载的时机
虚拟机规范了下面情况必须对类进行初始化?
1. 遇到new,getstatic,putstatic,invokestatic四个字节码指令时,如果类没有进行初始化,则先进行类的初始化过程(new ,读取或者设置类的static字段,final的放在常量池除外,调用类的静态方法)
2. 使用java.lang.reflect包进行反射调用
3. 当初始化一个类的方法,如果父类没有初始化,先初始化父类
4. main方法所在的类在虚拟机启动时加载
5.当使用动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化
三、类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示。
类加载过程
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
3.1 加载
加载过程主要完成以下3件事
1、通过一个类的全限名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3、在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据和访问入口
而获取二进制字节流的方式可以有很多种,不一定是通过类的全限名;比如从 ZIP 读取,网络中读取或者动态代理读取等等。
加载阶段和连接阶段的部分内容,是交叉进行的。
3.2 验证
这一阶段是为了确保 Class 文件中的字节流是否符合当前虚拟机的要求,并且不会伤害到虚拟机自身安全;主要分为4个模块的验证。
3.2.1 文件格式验证
文件格式主要验证 字节流是否符合 Class 的规范,如:
1、是否以魔数 OXCAFEBABE(咖啡宝贝) 开头
2、主、次版本号是否在当前虚拟机的处理范围内
3、常量池的常量中是否存在不被支持的类型等等
实际上,验证远远不止上面这些,它的主要目的就是为了把字节流正确的解析并存储在方法区之内,只有通过了这个阶段,字节流才会流进内存的方法区中进行存储,后面的验证都是基于方法区的存储结构进行的,不会再操作字节流。
3.2.2 元数据验证
这个阶段是对字节码描述的信息进行语义分析,比如:
是否有父类(除了Object 类,所有的类都应该有父类)
这个类的父类是否继承了不允许被继承的类 (被final修饰过的类)等等
总之,就是验证这个字节码的信息符不符合Java的语义,保证不符合Java语言规范的元数据类型。
3.2.3 字节码验证
该验证主要是分析数据流和控制流,确定语义是合法的,符合逻辑的。在元数据验证验证之后,还需要字节码的验证,比如操作栈放置了一个 int 类型的数据,在使用时,却按 long 类型来加载本地变量表中。实际上,字节码的验证大部分是避免Java代码在运行或者使用时的一些保护措施。
3.2.4 符号引用验证
最后一个阶段的验证是虚拟机将符号引用转化为直接引用的时候,这个转换动作将在连接的第三阶段 – 解析阶段进行。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。通常校验以下内容:
1、符号引用中通过字符串描述的全限定名能否找到对应的类,或者有没有访问权限。
2、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段等等
总的来说,符号引用验证是确保解析动作能够正常运行的保障之一,如果无法通过符号引用验证,则会抛出NoSuchMethodError 等方法。
3.3 准备(对静态变量进行默认初始化)
准备阶段正式为类变量分配内存和设置初始值的阶段,这些变量所使用的内存将在方法区中进行分配。需要注意的是,这里方法区的内存分配仅包含类变量(即被static修饰的变量),而不包括实例变量,实例变量讲在对象初始化的时候被分配在 Java 堆中;然后,这里说的初始值,其实是讲数据类型置为零值,假设
public static int value = 123;
那么它在准备阶段过后的初始值为0而不是123,因为此时并未执行任何Java方法。但如果是用 final 修饰,则准备阶段还是123. // 常量
3.4 解析
解析是把符号引用转换为直接引用的过程;在 class 文件格式中,符号引用常常以,CONSTANT_FIELDREF_INFO等等类型的常量出现。然后来理解一下符号引号和直接引用的含义:
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,且引用的目标不一定已经加载到内存中。
直接引用:直接引用可以是一个直接指向目标的指针,相对偏移量或是一个能间接访问目标的句柄。
要解析一个符号引号,通过通过以下几种方式:
3.4.1 类或接口的解析
当前代码所处的类为D,用N表示一个未被解析过的类或接口 的直接引用,类用C来表示,那虚拟机的解析的过程如下:
1、如果C不是一个数组,那虚拟机在会把它的全限定名传递给D的类加载器;在这个过程中,又会触
其他相关类的加载动作,例如加载它的父类;如果加载过程失败,则解析失败
2、如果C是一个数组,并且数组的元素为对象,,那将会按照第1点的规则加载数组元素类型。
3、如果上面都没有发生异常,则C在虚拟机中,已经成为一个有效的类或接口了。但解析完成之前,还需要进行符号引用验证,确保D对C有访问权限。
3.4.2 字段解析
要解析一个重未被解析过得字段符号引用,首先会先解析字段所属的类或接口的符号引用。如果在解析这个类或接口的符号引用发生异常,则字段解析失败;如果解析成功,则继续校验:
1、如果C本身的简单名称和字段描述符都与目标匹配的字段,则解析结束,返回这个字段的直接引用,查找结束。
2、如果C 中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,有则返回。
3、如果C不是 Object 类,则也会递归搜索父类,有则返回。
4、否则,抛出 NoSuchFieldError 异常。
以上查找返回直接引用之后,还会对权限进行验证,如果失败,也会抛出IllegalAccessError 的异常。
3.4.3 类方法解析
类方法的解析,也需要先经过类和接口的解析,解析成功才会继续:
1、类方法和接口方法引号引用的常量类型定义是分开的,如果在方法表中发现 class_index 中索引的是C的接口,则直接抛出 IncompatibleClassChangeError 异常
2、经过了第一步,如果C类中,有与之简单名称和描述符都匹配的方法,则返回直接引用。
3、若没有,则在C类中的父类继续查找,有则返回
4、否则,则在 C 类实现的接口列表和父类接口中递归去查找,有,则说明C类是个抽象类,返回直接引用,否则抛异常。
5、若都失败了,则抛出 NoSuchMethodError 异常
以上查找返回直接引用之后,还会对权限进行验证,如果失败,也会抛出IllegalAccessError 的异常。
3.4.4 接口方法解析
接口方法也需要先经过类和接口的解析,解析成功才会继续,如果解析成功,依然用C表示这个接口,接下来才会继续去解析;接口方法先回判断 class_info 中的索引C是个类而不是接口,就会抛出异常,否则则继续在接口C或者它的父接口中去继续查找匹配的简单名称和描述符,符合则返回,否则抛出异常。
3.4 初始化(对静态变量进行显式初始化)
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码,初始化阶段是执行类构造器 <client>()方法的过程。<client>()方法就是我们熟悉的 static{ }静态语句块,比如初始化一些变量,或加载JNI 库时都是 static 模块中。静态语句块只能访问到定义在它之前的变量,在它之后的,能赋值,但不能访问,如:
对于初始化阶段,虚拟机严格规范了有且只有5中情况下,必须对类进行初始化:
1、当遇到 new 、 getstatic、putstatic或invokestatic 这4条语句是,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
2、使用 reflect 反射时,如果类没初始化,需要触发其初始化
3、初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
4、当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
5、当使用动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化
接着我们用代码验证一下:测试main方法不是在Child类中,可以另外写一个测试类进行测试
输出分别如下:
而如果 value 被 final 修饰,则不会出发初始化,因为 value 已经存在 NotInitialization 常量池中了。只输出123
如果使用数组,情况也是不一样的:
窗口没有打印什么,这说明并没有出发Parent 或者 Child 的初始化。这是因为数组在初始化时,会由虚拟机初自动生成一个不同报名的 Child 全限定名,直接继承 Object 类,创建动作有字节码 newarray 触发。
从上面看,由于父类的 <client>()先执行,所以父类定义的静态语句块要优先于子类的变量赋值操作。如下,打印为2不是1:先执行public static int value = 1;再执行value = 2;最后执行public static int B = value ;此时B=2;
四、类加载器
从上面看到,类加载阶段中,是通过一个 类的全限定名来获取描述此类的二进制字节流 的,这个动作如果放到外部去做,以便程序自己决定如何去获取所需要的类。我们叫做 “类加载器”,我们举个反射的例子来说明从外部去加载这个二进制流的理解:
首先,添加一个自己定义的 classloader:继承ClassLoader
在自定义 ClassLoader 中,我们只传进来了报名加类名.class ,实际上可以是任意 .class 文件,然后我们在 main 函数中,这样:
五、双亲委派模型
它的工程流程是: 当一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是委派给她的父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载器都会传递到父加载器中;只有父加载器无法完成时,子加载器才会尝试自己去加载,它的模型如下:
类加载器的加载过程详解
1、启动类加载器(Bootstrap classLoader):这个类主要将 JAVA_HOME\lib ,或者被 -Xbootclasspath 参数所指定路径中的,并且能被虚拟机识别的类库,加载到内存中
2、扩展类加载器(ExtClassLoader):负责加载 JAVA_HOME\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径的所有类库,开发者可以直接使用扩展加载器。
3、应用程序类加载器(AppClassLoader):这个类加载器有 AppClassLoader 实现的,是 ClassLoader 中的 getSystemClassLoader() 返回值,所以也叫系统类加载器。负责加载用户类路径上所指定的类库。如果用户没有自定义加载器,则都使用这个类加载器,比如反射方法中的 Class.formName
该模型的优点:1、带有优先级的层次关系,不会因为用户自行编写其他相同名称的类而变得混乱。
2、如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。
总结:类加载阶段主要是针对类变量进行操作,准备阶段在方法区中完成,之后再进行初始化。类加载和类的实例化不同,要区别开来。JVM先去方法区中找有没有相应的类的.class存在,如果有,直接使用,如果没有,则把相关类加载到方法区中。加载的时候分为两部分,1是加载非静态内容,将非静态内容加载到非静态区域,不做任何处理,等实例化的时候再处理。2是加载静态内容到静态区域,先对所有静态变量进项默认初始化,即赋零值,再进行显式初始化,即赋予真正的值,再执行静态代码块,至此整个类就加载完成了。
打破双亲委派机制,即在类加载的时候不是传递到父类加载器中加载,而是由自己加载。
此时需要自定义一个类加载器,继承ClassLoader类,然后重写父类的findClass方法和loadClass方法。即
1、自定义一个类加载器,继承ClassLoader类
2、重写findClass方法和loadClass方法
这里最主要的是重写loadclass方法,因为双亲委派机制的实现都是通过这个方法实现的,先找父加载器进行加载,如果父加载器无法加载再由自己来进行加载,源码里会直接找到根加载器,重写了这个方法以后就能自己定义加载的方式了
评论