Java ClassLoader 类加载详解
类加载的过程
Java
中有一个类ClassLoader
,它的主要职责就是负责加载各种class
文件到JVM
中,ClassLoader
是一个抽象的class
。- 给定一个
.class
文件,ClassLoader
会尝试加载并且在JVM
中生成这个类的各个数据结构,然后使其分布在JVM对应的内存区域中。
类的主动使用和被动使用
-
每个类或者被
Java
程序首次主动使用时才会对其进行初始化。JVM
同时规范了以下6
种主动使用类的场景:- 通过
new
关键字会导致类的初始化,它肯定会导致类的加载并且最终初始化; - 访问类的变态变量,包括读取和更新会导致类的初始化;
- 访问类的静态方法,会导致类的初始化;
- 对某个类进行反射操作,会导致类的初始化;
- 初始化子类会导致父类的初始化;这里需要注意的是,通过子类使用父类的静态变量只会导致父类的初始化,子类则不会被初始化。
- 启动类: 也就是执行
main
函数所在的类会导致该类的初始化。
- 通过
-
除了以上
6
种情况,其余的都称为被动使用,不会导致类的加载和初始化。 -
注意以下几种情况不会造成类的初始化:
- 构造某个类的数组时并不会导致该类的初始化。
- 引用类的静态常量不会导致类的初始化。但是注意区分以下两种情况:
public class A { static { System.out.println("The A will be initialized."); } // 在其它类中使用MAX不会导致A的初始化,静态代码块不会执行 public final static int MAX = 10; // 虽然RANDOM是静态变量,但是由于计算复杂,只有初始化之后才能得到结果,因此在其他类中使用时会初始化 public final static int RANDOM = new Random().nextInt(); }
类加载过程及详解
-
加载阶段:主要负责查并且加载类的二进制数据文件,其实就是
class
文件。将字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class
对象,作为访问方法区数据结构入口。如下图所示:
类加载的最终产物是堆内存中的class
对象。类的加载时通过包名 + 类名来获取二进制数据,但是并没有硬性规定必须通过哪种方式去获取。这里需要注意的是:类的加载与连接两个阶段并不是先后有序执行的,而是存在一定的交叉进行的。 -
连接阶段:主要分三个阶段执行:
- 验证:主要是确保类文件的正确性,如
class
的版本等。 - 准备:为类的静态变量分配内存,并且为其初始化默认值。
- 解析:把类中的符号引用转换为直接引用。
- 验证:主要是确保类文件的正确性,如
验证
-
主要目的是确保
class
文件的字节流所包含的内容符合当前JVM
的规范要求,并且不会出现危害JVM
自身安全的代码,当字节流的信息不符合要求时,则会抛出VerifyError
这样的异常或者子异常。它验证的内容主要如下:- 验证文件格式
验证这个文件是否是
class
文件,字节码指令中,class
文件的标志是0xCAFEBABE
;主次版本号。查看当前的
class
文件版本是否符合当前JDK
所处理的范围。比如JDK8
编译的class
文件是不能在JDK7
中运行的。构成
class
文件的字节流是否存在残缺或者其它附加信息。常量池中的常量是否存在不被支持的变量类型。
指向常量中的引用是否指到了不存在的常量或者该常量的类型不被支持。等。
- 元数据的验证
就是为了确保
class
字节流符合JVM
规范的要求。检查这个类是否存在父类,是否实现了某个接口。并且这些接口或者类是否真实存在且合法。
检查该类是否存在继承了被
final
修饰的类。检查该类是否是抽象类。
检查方法重载的合法性,比如相同的方法名称、方法参数但是返回类型不相同,这都是不被允许。
- 字节码验证
主要验证程序的控制流程,比如循环、分支等。
保证当前线程在程序计数器中的指令不会跳转到不合法的字节码指令中去。
保证类型的转换是否合法。
保证在任意时刻,虚拟机栈中的操作栈类型与指令代码都能正确地被执行。
- 符号引用验证
符号引用的验证,其主要作用就是验证符号引用转换为直接引用时的合法性。从而保证解析动作的顺利执行。
准备
- 为对象的静态变量分配内存并且设置初始值了,该内存会被分配到方法区中,不同于实例变量被分配到堆内存之中。 所谓设置初始值,其实就是为相应的类变量给定一个相关类型在没有被设置值的默认值。注意区分以下情况
public class B {
private static int a = 10;
private final static int b = 10;
}
其中static int a = 10;
在准备阶段不是10
,而不是初始值0
,当然final static int b;
仍然是10
。
解析
-
所谓解析就是在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程。
-
类接口解析
- 以类
C
为例,不是一个数组类型,则在加载的过程经历所有的类加载阶段。 - 如果类
C
是一个数组类型,则只需要生成一个能代表该类型的数组对象,并且在堆内存中开辟一片连续的地址空间即可。 - 在类接口的解析完成之后,还需要进行符号引用的验证。
- 以类
-
字段的解析
- 在解析类或者变量的时候,如果该字段不存在,或者出现错误,就会抛出异常,不再进行后续的解析。
- 首先要对字段所属的类进行加载。
- 如果类
C
中不存在该字段,则根据继承关系,层层往上寻找并加载,直到java.lang.Object
还没有,报出异常。 - 在哪层找到该字段,即返回,不会继续往上寻找。
-
类方法的解析
- 类方法可以直接使用该类进行调用,而接口方法必须要有相应的实现类实现才能调用;
- 如果发现是一个接口,不是一个类,直接返回错误;
- 如果查找的方法与目标方法完全一致,直接返回不会再往上继续查找。
- 如果父类及超类中仍然没有找到,则报
NoSuchMethodError
; - 如果是一个抽象类,则也会抛出一个
AbstractMethodError
这个异常;
-
接口方法的解析
- 接口不仅可以定义方法,而且还可以继承接口;解析除说明出来的,其余基本与类方法解析一致。
-
类初始化阶段
- 在初始化阶段做的最主要的一件事情就是执行
<clinit>()
方法的过程,在<clinit>()
中所有的类变量都会被赋予正确的值,也就是在程序编写的时候指定的值; <clinit>()
中包含了所有类变量的赋值动作和静态语句块的执行代码,且它是能保证顺序性的。需要注意的是,静态语句块只能对后面的静态变量进行赋值,但是不能对其进行访问;- 另外,
JVM
会优先保证父类的<clinit>()
方法最先执行,所以父类的静态变量总是能够得到优先赋值。 <clinit>()
方法虽然是真实存在的,但是它只能被虚拟机执行,在主动使用触发了类的初始化之后就会调用这个方法。
- 在初始化阶段做的最主要的一件事情就是执行
JVM类加载器
-
JVM
为我们内置了三大类加载器,Java
的类加载器组织结构如下图所示:
-
Java
内置的三大类加载器,这里就不在赘述了。这里着重详解下自定义类加载器。.
自定义类加载器
- 所有的自定义类加载器都是
ClassLoader
的直接子类或者间接子类。ClassLoader
是一个抽象类,务必重写findClass
方法。因为翻看源码可知,该类中findClass
方法抛出ClassNotFoundException
异常。 - 如下代码,是一个自定义的类加载器,继承了
ClassLoader
类,重写了findClass
方法。
/**
* 自定义类加载器必须是ClassLoader的直接或间接子类
*/
public class MyClassLoader extends ClassLoader {
// 定义默认的class存放路径
private final static Path DEFAULT_CLASS_DIR = Paths.get("E:", "classloader1");
private final Path classDir;
// 使用默认的class路径
public MyClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
// 允许传入指定路径的class路径
public MyClassLoader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
// 指定class路径的同时,指定父加载器
public MyClassLoader(String classDir, ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
// 重写父类的findClass方法,非常至关重要的步骤
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 读取class的二进制数据
byte[] classBytes = this.readClassBytes(name);
// 如果数据为null,或者没有读取到任何信息,则抛出ClassNotFoundException异常
if (classBytes == null || classBytes.length == 0) {
throw new ClassNotFoundException("Can not load this class " + name);
}
// 调用defineClass方法定义class
return this.defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] readClassBytes(String name) throws ClassNotFoundException {
// 将包名分隔符转换为文件路径分隔符
String classPath = name.replace(".", "/");
Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
if (!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("The class " + name + " not found.");
}
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Files.copy(classFullPath, baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("load the class " + name + " occur error.", e);
}
}
@Override
public String toString() {
return "MyClassLoader{" +
"classDir=" + classDir +
'}';
}
}
- 通过类得全名称转换成文件的全路径重写
findClass
方法,然后读取class
文件的字节流数据,最后使用ClassLoader
的defineClass
方法对class
完成了定义。 - 全路径格式有如下几种情况:
java.lang.String
:包名.类名
;java.util.Map$Entry
:包名.类名$内部类
。java.kuraki.A$B$1
:包名.类名$内部类$内部类$匿名内部类
。
双亲委托机制
- 先自定义一个
HelloWorld
类;
public class HelloWorld {
// 用于查看HelloWorld类是否已经进行加载
static {
System.out.println("Hello World类已经被加载类!");
}
public String welcome() {
return "Hello world";
}
}
- 双亲委托机制:当一个类加载器被调用了
loadClass
之后,它并不会直接将其加载,而是先交给当前类加载器的父加载器尝试加载,直到最顶层的父加载器,然后再依次向下尝试加载。ClassLoader
中的相应源码如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,先检查这个类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在父加载器,则调用父加载器的loadClass方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果为null,也就直接调用BootStrap类加载器进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 这里抛出异常,说明当前及其父的类加载器都不能对该类进行加载
}
if (c == null) {
// 如果父加载器中都不能加载,则调用自己的findClass方法进行加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
-
这里介绍两种绕过系统类加载器,调用自定义类加载器的加载自定义类的方式:
- 绕过系统类加载器,直接将扩展类加载器作为
MyClassLoader
的父加载器,代码如下
ClassLoader extClassLoader = MyClasLoaderTest.class.getClassLoader().getParent(); MyClasLoader classLoader = new MyClassLoader("E:\\classLoader1", extClassLoader); Class<?> aClass = classLoader.loadClass("类全路径名"); System.out.println(aClass); System.out.println(aClass.getClassLoader());
- 在构造
MyClassLoader
的时候指定父的类加载器为null
。
- 绕过系统类加载器,直接将扩展类加载器作为
-
这样绕过了系统类加载器,无论是根类加载器和扩展类加载器都无法对我们自定义的类进行加载,根据上述,自然会调用自定义的类加载器进行加载了。
打破双亲委托机制
- 应用场景:热部署,在程序运行时进行某个模块的升级,或者在不停止服务的前提下增加新的功能等。
- 如下是继承
MyClassLoader
类,重写其中的loadClass
方法的代码:
public class BrokerDelegateClassLoader extends MyClassLoader {
public BrokerDelegateClassLoader() {
}
public BrokerDelegateClassLoader(String classDir, ClassLoader parent) {
super(classDir, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1.根据类的全路径名称进行加锁,确保每一个类在多线程的情况下只被加载一次
synchronized (getClassLoadingLock(name)) {
// 2.到已加载类的缓存中查看该类是否已经被加载,如果已经加载则直接返回
Class<?> klass = findLoadedClass(name);
// 3.若缓存中没有被加载的类,则需要对其进行首次加载
if (klass == null) {
// 4.如果类的全路径以java和javax开头,则直接委托给系统类加载器对其进行加载
if (name.startsWith("java.") || name.startsWith("javax")) {
try {
klass = getSystemClassLoader().loadClass(name);
} catch (Exception e) {
//ignore
}
} else {
// 5.如果类不是以java和javax开头,则尝试用我们自定义的类加载进行加载
try {
klass = this.findClass(name);
} catch (ClassNotFoundException e) {
// ignore
}
// 6.若自定义类加载仍旧没有完成对类的加载,
// 则委托给其父加载器进行加载或者系统类加载器进行加载
if (klass == null) {
if (getParent() != null) {
klass = getParent().loadClass(name);
} else {
klass = getSystemClassLoader().loadClass(name);
}
}
}
}
// 7.经过若干次的尝试之后,如果还是无法对类进行加载,则抛出异常
if (null == klass) {
throw new ClassNotFoundException("The class " + name + " not found.");
}
if (resolve) {
resolveClass(klass);
}
return klass;
}
}
}
类加载器命名空间
- 命名空间是由该类加载器及其所有父的类加载器所构成的,因此每个类加载器中同一个
class
都是独一无二的。 - 如下是三组不同的测试方法:
public class NameSpace {
// 相同类加载器加载
public static void main(String[] args) throws ClassNotFoundException {
// 获取系统类加载器
ClassLoader classLoader = NameSpace.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("com.kuraki.MyClassLoaderTest");
Class<?> bClass = classLoader.loadClass("com.kuraki.MyClassLoaderTest");
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass == bClass);
}
// 相同类加载器加载同一个class
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader1 = new MyClassLoader("D:\\kuraki", null);
MyClassLoader classLoader2 = new MyClassLoader("D:\\kuraki", null);
Class<?> aClass = classLoader1.loadClass("com.kuraki.HelloWorld");
Class<?> bClass = classLoader2.loadClass("com.kuraki.HelloWorld");
System.out.println(aClass.getClassLoader());
System.out.println(bClass.getClassLoader());
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass == bClass);
}
// 不同类加载器加载同一个class
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader1 = new MyClassLoader("D:\\kuraki", null);
BrokerDelegateClassLoader classLoader2 = new BrokerDelegateClassLoader("D:\\kuraki", null);
Class<?> aClass = classLoader1.loadClass("com.kuraki.HelloWorld");
Class<?> bClass = classLoader2.loadClass("com.kuraki.HelloWorld");
System.out.println(aClass.getClassLoader());
System.out.println(bClass.getClassLoader());
System.out.println(aClass.hashCode());
System.out.println(bClass.hashCode());
System.out.println(aClass == bClass);
}
}
- 分别运行上面的三个
main
方法,会发现只有第一个main
方法执行输出的true
;这就说明- 相同类加载器的相同实例对同一个类进行加载,会在堆上产生相同的
Class
对象; - 相同类加载器的不同实例对同一个类进行加载,会在堆上产生不同的
Class
对象; - 不同类加载器对同一个类进行加载,会在堆上产生不同的
Class
对象;
- 相同类加载器的相同实例对同一个类进行加载,会在堆上产生相同的
初始类加载器和类的卸载
- 如果某个类
C
被类加载器CL
加载,那么CL
就被称为C
的初始类加载器。JVM
为每一个类加载器维护了一个列表,该列表记录了将该类加载器作为初始类加载器的所有class
,在加载一个类时,JVM
使用这些列表来判断该类是否已经被加载过了,是否需要首次加载。 - 类的卸载:
JVM
规定了一个Class
只有在满足下面三个条件才会被GC
回收,也就是类卸载。- 该类所有的实例都已经被
GC
。 - 加载该类的
ClassLoader
实例被回收。 - 该类的
class
实例没有在其它任何地方被引用。
- 该类所有的实例都已经被