虚拟机类加载机制
JVM将 xxx.class 字节流加载到内存,对其进行校验,转换解析,初始化,最终形成可以被JAVA虚拟机直接使用的数据类型,这个过程称为虚拟机的类加载。其中xxx.class字节流的来源包括但不局限于文件,也可以从网络,数据库中获取,还可以由程序在运行时动态生成。
1、类的加载过程
类的加载过程总共分为加载,验证,准备,解析,初始化,使用,卸载这几个步骤。
其中验证,准备,解析,统称为连接阶段。
类型的加载,连接,初始化都是在程序运行期间完成的,这个特点给予了JAVA语言很大的动态扩展性,和灵活性。其中在什么时候进行类的加载,虚拟机规范并没有作出明确的规定,但是对于初始化,虚拟机却规定在一下六种情况必须进行,因此初始化之前的加载,验证,准备,解析也必然被执行。
图示:
1.1 、加载
这里的加载不代表类的加载的全过程,它比较局限,只是将字节流load到内存中,而且该过程是和连接阶段是交替执行的,总共分为三个步骤:
- 通过类的全名称(包名.类名),将与之对应的字节流读取到内存中。
- 将字节流中的存储结构,转化为方法区特定的数据结构。
- 在Java堆中生成该类的Class对象,作为程序访问方法区这个类的数据的访问入口。
注:第二个和第三个步骤都需要连接阶段的执行,连接阶段主要要对字节流文件进行检验,判断是否符合规则,会不会对虚拟机造成破坏。
类的加载是由类加载器来完成的。
1.1.1 类加载的时机
虚拟机规范并没有规定什么时候进行类的加载,但是却规定类什么时候进行类的初始化,而类型的加载必须在类的初始化之前完成,因此相当于也相当于规定了什么时候进行类型的加载。而且类加载采用的是懒加载模式,在用的时候才会进行加载。
- 遇到new,getStatic,putStatic,invokeStatic这些指令时时候。
- 进行反射调用的时候。
- 子类的初始化必须先进行父类的初始化。
- 程序启动的时候 main方法所在的类会被初始化。
- …
1.2、连接
包括验证,准备和解析三个步骤:
-
验证 : 首先需要进行文件格式验证,判断是否是符合class文件格式规范,此处不是通过后缀名验证,而是采取一种更安全的方式,即判断字节流的魔数,因为文件名的后缀可以随意更改。再验证版本号,低版本的虚拟机不能处理高版本的字节流。元数据验证,如final的合规性、类,字段,方法的权限,名称,修饰符等的正确性,继承,覆写,重载等是否正确进行验证,字节码验证,确保程序中的方法执行,跳转是合乎规范的,验证是JVM保护自己的一项重要手段,其中验证不通过的字节码将抛出 java.lang.VerifyError 错误。
-
准备:为类中的静态变量设置默认值 。即时存在代码,
private static int num = 10
。在这个阶段,num这个变量的值还只是0,至于什么时候变成10,需要在初始化阶段,执行< cinit >方法。这个过程与普通的对象没有关系。而如果执行private static final int num = 10
这个num的值,在准备阶段就会设置为10. -
解析阶段:使用直接引用代替符号引用,完成内存布局。符号引用仅仅是一个符号,不代表具体的偏移地址,和内存布局无关,而且引用的目标也不一定已经加载到了内存当中,而直接引用是代表直接的偏移地址的,是可以具体找到该引用所指向的对象的,直接引用的对象是必须已经存在到内存当中的。其中解析阶段包括类,接口,字段,方法,属性等的解析。
1.3、 初始化
使用类的构造方法< cinit > 对静态属性进行初始化,执行类中的静态块中的代码等,其中同一个类中这个顺序只和写的顺序有关,其中父类的初始化在子类的初始化之前。其中Java可以保证在初始化过程中只有一个线程完成< cinit >方法的执行,其他线程都会阻塞等待。
2、 类加载器ClassLoader
类加载器的作用:在执行类加载过程中第一步工作,将字节流读进内存中的这个过程由类加载器完成。 所以说类加载器不是完成加载类的整个过程,它只是将字节流load到内存中而已。对于一个类的唯一性确定,是由该类和加载该类的类加载器来共同完成的。JDK中默认有三种类加载器;
- BootStrap ClassLoader(启动类加载器):该加载器由C++语言实现,是虚拟机的一部分,主要负责加载JDK\jre\lib\rt.jar类文件。(文件很大)。Java程序员不能直接使用该类加载器。
- Extension ClassLoader(扩展类加载器) :Java语言实现,独立于JVM。主要加载\JDK\jre\lib\ext的文件。
- Application ClassLoder(应用程序类加载器):Java语言实现,独立于JVM。加载ClassPath下的指定类库。开发者可以直接使用的类加载器,若没有自定义类加器,一般情况就是程序的默认类加载器。
代码测试:
class Test{}
public class TestClassLoader {
public static void main(String[] args) {
Class<Test> cls = Test.class;
System.out.println(cls.getClassLoader());
System.out.println(cls.getClassLoader().getParent());
System.out.println(cls.getClassLoader().getParent().getParent());
}
}
运行结果:
最顶层的BootStrap类加载器有C++实现,因此返回null。
2.1 双亲委派模型
2.1.1 什么是双亲委派模型
一个类加载器收到类加载的请求,首先不会自己去加载这个类,而是将这个请求委派给父加载器完成,每一层都这样,因此这个类加载请求会来到最高层,若父加载器可以加载这个类,父加载器优先加载。若不在父的加载范围之内或者父加载器加载不了,此时才由子类加载器加载。这个过程便是双亲委派模型。
2.1.2 代码分析
这个方法在ClassLoader类中,逻辑大概是这样的:
- 判断该类是否已经被加载完毕
- 如果没有被加载,使用parent类加载器进行加载,parent加载器不为null的话,继续执行上述逻辑,如果为null,由BootStrap类加载器进行加载。
- 执行完上述逻辑候,如果此时c还是为null说明父加载器也没有完成该类的加载,执行findClass方法,由自己进行加载。
其中这个loadClass方法是protected修饰且没有加final的,因此用户可以通过覆写打破这个逻辑。
一般用户自定义类加载器的话,覆写其中的findClass方法即可,这样不会打破双亲委派模型。
2.2、 双亲委派模型的作用
如果用户自定义一个java.long.String类,放在ClassPath下,则按照双亲委派模型的流程,交由最顶层的BootStrap加载器加载,因为在它的加载范围内有这个类,因此它会加载JDK中的String类,而自定义的String类不会被加载。换言之,如果没有双亲委派模型,自定义的String类,被Application加载,自己定义的String类,又没有JDK提供的String类的功能,而其它系统提供的类都使用了String这个类,这样会导致整个程序一片混乱。
简单而言:通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,最主要的是防止恶意覆盖Java核心API。
2.2.1 、图示:
2.2.2、验证双亲委派模型
1.自定义java.long.String
package java.lang;
/**
* @auther plg
* @date 2019/5/6 19:09
*/
public class String {
}
2.输出该类的类加载器
public class Test {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
}
}
输出结果
可以看出即使自定义String类,包名类名全一样,String类也由BootStrap(最顶层)类加载加载。自己的String类不会被加载。
3、 自定义类加载器
3.1、定义一个类,继承ClassLoader。
public class MyClassLoader extends ClassLoader
3.2、将Class文件读进来,存储在一个byte数组中。(使用文件的输入流,内存的输出流及自动关闭的try-resource语法)
private byte[] loadClassData(String name){
name = path + ".class";
try(InputStream in = new FileInputStream(name);ByteArrayOutputStream out = new ByteArrayOutputStream()){
int len = 0;
while((len = in.read()) != -1){
out.write(len);
}
return out.toByteArray();
}catch(IOException e){
e.printStackTrace();
}
return null;
}
3,3.覆写findClass方法,在方法中,调用defineClass,使用之前的byte数组,产生一个Class对象
@Override
@Deprecated
public Class<?> findClass(String name) {
byte[] b = loadClassData(name);
return super.defineClass(b,0,b.length);
}
测试
自定义的Test类
package com.github.excellent;
/**
* @auther plg
* @date 2019/5/7 17:34
*/
public class Test {
static{
System.out.println("hello");
}
}
主方法测试
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("C:\\Users\\Administrator.lenovo-PC\\Desktop\\Test");
Class c = m.findClass("com.github.excellent.Test.class");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
c.newInstance();
}
运行结果:
可见,这个类由自定义的MyclassLoader加载。它处于类加载器的最底层,它的父类加载器为AppClassLoader.
4、 打破双亲委派模型
4.1 Tomcat打破双亲委派模型
在一个Tomcat中是可以部署多个项目的,当这多个项目需要依赖相同的第三方jar包的不同版本的时候,如果采用JDK默认的类加载机制,自然是不能实现的,因为虽然它们的版本不一致,但是类的全路径是一样的,在加载的过程中,只会加载一次。因此Tomcat中引入了webAppClassLoader,这个类加载器覆写了loadClass方法,在加载classpath的.class文件时默认不会交给父加载器加载,自己加载完毕直接返回,因此打破了双亲委派模型。如图所示:
这种情况下,确实会在内存中存在全路径一样的多个class,但是它们的classloader不一样,因此也不会发生冲突。
4.2 SPI的类加载机制
Java 中有一个 SPI 机制,全称是 Service Provider Interface。
SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,使用 java.util.ServiceLoader 类进行动态装载。
以典型的JDBC 编程为例:
DriverManager是位于rt.jar中的,但实际的驱动Driver实现类是在mysql的jar中的,因为DriverManager是由BootStrap类加载器完成加载的,但是BootStrap类加载器没有能力去加载mysql这个jar包中的类。
因此JDK使用ServiceLoad这个类,通过线程上下文类加载来完成加载,代码分析如下:
那么当前的线程上下文类加载是谁呢?
在launcher类中有这么一些代码,可以看到当前的线程上下文类加载器是AppclassLoader。
因此这个过程是父加载器主动委托线程上下文类加载器去加载驱动类,因为它没有加载jar包中类的能力,这个也打破了双亲委派模型。