5.虚拟机类加载机制
虚拟机类加载机制
一、类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
特性:运行期动态加载和动态连接。
类的加载、连接和初始化过程都是在程序运行期完成的,虽然增加了一些性能开销,但是提供了高度灵活性。
二、类加载的时机
三、类加载的过程
1. 加载
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用 类的main()方法,new对象等等
主要完成以下三件事情
1.1 通过一个类的全限定名来获取定义此类的二进制字节流。
1.2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
1.3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2. 验证
目的是为了确保Class文件的正确性,字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2.1 文件格式验证:字节流是否符号class文件格式规范,且能被当前版本虚拟机处理
以魔数0xCAFEBABE开头;
主、次版本号是否在当前虚拟机处理范围内
常量池中的常量是否有不被支持的类型;
指向常量的各种索引值是否与指向不存在的常量或不符合类型的常量
...
2.2 元数据验证:字节码描述的信息进行语义分析,保证其描述的信息符号java规范的要求
这个类是否有父类,除了Object,其余都有父类;
这个类的父类是否继承了不允许被继承的类(final修饰的类)
若不为抽象类,所有方法是否都被实现了;
类中的字段、方法是否与父类矛盾,例如覆盖了父类的final字段,或者不符合规则的方法重载等
...
2.3 字节码验证:分析控制流与数据流,确定程序语义是否合理、符合逻辑。(最复杂阶段)
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似操作数栈放置了int类型的数据,使用却按照long类型加载到本地变量表中
保证跳转指令不会跳转到方法体以外的字节码指令上
保证方法体的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,反之或者赋值给其他数据类型则不合法
...
2.4 符号引用验证:实际作用发生在解析阶段,对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验
符号引用中通过字符串描述的类的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法与字段
符号引用中的类、字段、方法的访问性(public、protected、private、default)是否可以被访问到
...
3. 准备
正式为类变量(静态变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
注意:1.此时进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
2.这里所说的初始值“通常情况”下是数据类型的零值。如 public static int value = 123; 此时赋值的是初始值0而不是123,赋值123是在程序被编译后,存放在类构造器<clinit>()方法,也就是在后边的初始化阶段执行。
基本数据类型的初始值
数据类型 | 零值 2 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
4. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:在Class文件格式中,CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info以这些常量出现。在解析阶段,以一组符合来描述所引用的目标,符合可以是任何形式的字面量(明确定义在java虚拟机规范的Class文件格式中),只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量、或一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。有了直接引用,那引用的目标必定在内存中存在。
4.1 类或接口的解析
假设当前代码所处的类为D,如果要把一个从未解析过的"符号引用N"解析为一个类或接口C的直接引用,解析过程分为3步:
1.如果C非数组类型,虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。加载过程中由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如其父类或实现的接口。加载过程中出现异常,解析则宣告失败
2.如果C是数组类型,并且对象类型为对象,首先会同1规则加载数组元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
3.以上无异常,此时C在虚拟机实际上已经成为一个有效的类或接口了,但解析完成前还要进行<u>符号引用验证</u>,确认D是否具备对C的访问权限。java.lang.IllegalAccessError
4.2 字段解析
要解析一个从未被解析过的字段符号引用,首先对字段表内字段所属的类或接口****的符号引用进行解析,将该字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C的后续字段搜索:
1.如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则直接返回这个字段的直接引用,查找结束。---搜索本身类字段
2.否则,如果C中实现了接口,将会按照继承关系从下网上递归搜索各个接口与它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。---搜索接口****字段
3.否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。 ---搜索父类****字段
4.否则,查找失败。java.lang.NoSouchFieldError
5.以上无异常,查找过程成功返回了引用,对该字段进行权限检验。java.lang.IllegalAccessError
4.3 类方法解析
首先解析类方法表中方法所属的类或接口的符号引用,使用C表示该类,虚拟机将会按照如下步骤进行后续的类方法搜索。
1.类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法中发现C是个接口,直接抛异常。java.lang.IncompatibleClassChangeError --搜索本身类方法
2.在类C中查找是否有简单名称和描述符都与目标想匹配的方法,有则返回该方法的直接引用,查找结束。
3.否则,在类C的父类中递归查找是否由简单名称和描述符都与目标相匹配的方法,有则返回这个方法的直接引用,查找结束。 ---递归搜索父类****方法
4.否则,在类C实现的接口列表以及它们的父接口中递归查找是否由简单名称和描述符都与目标相匹配的方法,如在匹配的方法,说明类C是一个抽象类,查找结束。java.lamg.AbstractMethodError ---****递归****搜索接口****方法
5.否则,查找失败。java.lang.NoSuchMethodError
6.如果查找过程返回了直接引用,对这个防范进行权限验证。java.lang.IllegalAccessError
4.4 接口方法解析
首先解析接口****方法表中方法所属的类或接口的符号引用,使用C表示该接口,虚拟机将会按照如下步骤进行后续的接口方法搜索。
1.接口方法表中发现索引C是个类而不是接口,直接抛异常。java.lang.IncompatibleClassChangeError
2.在接口C中查找是否有简单名称和描述符都与目标想匹配的方法,有则返回该方法的直接引用,查找结束。 --搜索本身接口方法
3.否则,在类C的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,有则返回这个方法的直接引用,查找结束。 ---****递归****搜索父接口方法
4.否则,查找失败。java.lang.NoSuchMethodError
5.接口中所有方法默认public,不存在访问权限问题。
5.初始化
初始化阶段是类加载过程的最后一步,前面的类加载过程,除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导与控制。
在准备阶段,变量已经赋值过一次系统初始化值。在初始化阶段,则是根据程序中的赋值语句主动为类变量和其他资源赋值,或者说初始化阶段是在执行类构造器<clinit>()方法的过程。
初始化规则:
1.<clinit>()方法是由编译器自动收集的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量在前边的静态语句块可以赋值,但是不能访问。
2.<clinit>()方法与类构造器(实力构造器<init>())不同,它不需要显式的调用父类构造器,虚拟机会保证子类的****<clinit>()方法执行前父类的****<clinit>()方法已经执行完毕。因此第一个执行<clinit>()方法的类肯定是java.lang.Object。
3.由于父类的<clinit>()方法先执行,所以父类定义的静态语句块要优先于之类的变量赋值操作。
4.<clinit>()方法对类或接口不是必须的,如果一个类中没有静态语句块,即没有对变量的赋值操作,即不需要<clinit>()方法。
5.接口不能使用静态语句块,但是仍然有变量初始化的赋值操作,因此接口与类一样都会生成****<clinit>()方法。但接口与类不同的是,执行接口的****<clinit>()方法不需要线执行父接口的****<clinit>()方法。只有父接口定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的****<clinit>()方法。
6.虚拟机保证一个类的<clinit>()方法在多线程环境被正确的加锁、同步,如果多个线程同时初始化一个类,那么只有一个线程去执行这个类的<clinit>()方法,其他线程需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
有且只有以下5种情况会立即对类进行初始化(对一个类进行主动引用)
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。(如果是接口初始化,无此要求,只有在使用到父接口时,如引用接口定义的常量,这时才会初始化)
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个类。
5、当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
初始化实例:https://blog.csdn.net/wanghao112956/article/details/90902610
四、类加载器
类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序可以自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”
类加载器主要应用场景:类层次划分,OSGI,热部署,代码加密等。
1. 类与类加载器
对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在虚拟机的唯一性,每一个类加载器,都由一个独立的类名称空间。
2. 双亲委派机制
JVM提供了以下3种系统的类加载器:
启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是程序中默认的类加载器。
双亲委派模型层次关系:
双亲委派模型要求除了最顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承(Inheritance)关系实现,而是使用组合(Composition)关系复用类加载器的代码
双亲委派的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader中。当父类加载器反馈无法处理这个加载请求,子加载器才hi尝试自己去加载。
好处:Java类具备了一种带有优先级的层级关系
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
双亲委派模型实现代码:
/**
* 先检查是否已经被加载过,如没有加载则调用父加载器的loadClass()方法,
* 若父加载器为空则默认使用启动类加载器作为父加载器。
* 如果父类加载失败,抛出ClassNotFoundException异常后,
* 再调用自己的findClass()方法进行加载
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 先检查要加载的类是否已经被加载,这里说明同一个类只会被加载一次
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//先判断父类加载器是否为空,不为空则用父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果父类加载器也为空则用启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//当父类加载器无法加载时,调用自定义的ClassLoader本身的findClass
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
3. 破坏双亲委派模型
3.1 自定义类加载器
实现逻辑:jdk1.2后已不提倡用户去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass方法的逻辑里如果父类加载失败,则调用自己的findClass的方法来完成加载。这样可以保证新写出来的类加载器是符号双亲委派规则的。
3.2 双亲委派很好的解决了各个类加载器的基础类的统一问题,但是如果基础类想调用回用户的代码,怎么办?
典型的例子JNDI服务(对资源及性能集中管理和查找),它需要调用 由独立厂商实现并部署在应用程序的classpath下的JNDI接口提供者(SPI)的代码,但是启动类加载器不可能认识这些代码。
为了解决这个问题,java设计了“线程上下文类加载器(Thread Context ClassLoader)”。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法及性能设置,如果线程创建时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是程序类加载器。
JNDI服务使用线程上下文类加载器去加载需要的SPI代码,也就是父类加载器其请求子类加载器去完成类加载的动作。java设计SPI的加载动作基本采用这种方式,如JDBC、JNDI、JCE、JAXB、JBI等。
关于JDBC打破双亲委派模型
参考:https://www.jianshu.com/p/09f73af48a98
https://www.jianshu.com/p/78f5e2103048
3.3 用户对程序动态性的追求,如代码热替换(HotSwap)、模块热部署(Hot Deplyment)
OSGI(业界java模块化标准)实现模块化热部署的关键则是它自定义的类加载器机制的实现,每一个程序模块(Boundle)都有一个自己的类加载器,当需要更换一个Boundle时,就把Boundle联通类加载器一起换掉以实现代码的热替换。
在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更为复杂的网状结构,当收到类加载请求时OSGI按照下边的顺序进行类搜索:
1.将以java.*开头的类委派给父类加载器加载。
2.否则,将委派列表名单内的类委派给父类加载器加载
3.否则,将Import列表中的类委派给Export这个类的Boundle的类加载器加载
4.否则,查找当前Boundle的classpath,使用自己的类加载器加载
5.否则,查找类是否在自己的Fragment Boundle中,如果在,则委派给Fragment Boundle的类加载器加载
6.否则,查找Dynamic Import列表的Boundle,委派给对应Boundle的类加载器加载
7.否则,类查找失败