Java类加载机制
类加载过程
类加载过程包含了加载、验证、准备、解析、初始化五个阶段。其中加载、验证、准备、初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。需要注意的是,这几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或**另一个阶段。
1. 加载
加载是类加载过程的第一个阶段,JVM需要完成以下几件事情:
- 通过一个类的全限定名来获取二进制字节流
- 将二进制字节流所代表的的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中该运行时数据结构的访问入口
注意: 类的二进制字节流不仅可以从Class文件中获取,还可以从jar包、网络等其他途径获取。
2. 链接
2.1 验证
验证的目的是保证Class流的格式是正确的,主要分为文件格式验证、元数据验证、字节码验证、符号引用验证等。
②版本号是否合理
③......
②继承了final类?
③非抽象类实现了所有的抽象方法
④......
②栈数据类型和操作码数据参数吻合
③跳转指令指定到合理的位置
④......
②访问的方法或字段是否存在,且有足够的权限(public、private等描述符)
2.2 准备
准备阶段为类变量分配内存(在方法区中分配),并为类变量设置零值。
比如:
public static int v = 1;
在准备阶段,v会被设置为0,在初始化阶段的<clinit>中才会被设置为1。
对于static final修饰的常量,在准备阶段即会被赋上正确的值(即准备阶段v就会被设置为1),比如:
public static final v = 1;
2.3 解析
解析阶段是将符号引用替换为直接引用的过程。
符号引用:字符串,引用对象不一定被加载
直接引用:指针或地址偏移量,引用对象一定在内存中
3. 初始化
初始化实际上就是执行类构造器<clinit>的过程(注意与实例构造器<init>区分),主要是类变量的赋值,包括static变量赋值、static{}语句块执行等。
两个特性:
- 子类的<clinit>调用前保证父类的<clinit>执行完毕
- <clinit>是线程安全的
类加载器ClassLoader
ClassLoader是一个抽象类,它的实例负责读取Java字节码文件并装载到JVM中。在加载阶段,我们说类的二进制流可以从多个途径获取,实际上就是通过定制不同的ClassLoader实现的,而ClassLoader也仅仅负责类加载过程中的加载阶段。
/**
* 载入并返回一个Class
*/
public Class<?> loadClass(String name) throws ClassNotFoundException
/**
* 通过二进制流,定义一个Class对象
*/
protected final Class<?> defineClass(byte[] b, int off, int len)
/**
* loadClass会回调该方法
*
* 自定义ClassLoader时推荐重写该方法,不会破坏双亲委派模型
*/
protected Class<?> findClass(String name) throws ClassNotFoundException
/**
* 在当前类加载器中寻找已经加载的类
*/
protected final Class<?> findLoadedClass(String name)
类加载器主要分为以下几类:
- BootStrap ClassLoader(启动类加载器)
- Ext ClassLoader(扩展类加载器)
- App ClassLoader(应用类加载器 / 系统类加载器)
- Custom ClassLoader(自定义类加载器)
每个类加载器都有一个Parent作为父亲,结构如下图:
这种结构关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前类加载器的父加载器,但它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用类加载器中的代码。
注意:扩展类加载器与启动类加载器不存在父子关系,即Launcher#ExtClassLoader类中持有的parent属性为null。
双亲委派模式的工作流程如图所示:当使用某个类时,从当前类加载器开始自底向上检查类是否已经加载,如果某个类加载器已经加载了该类,则直接返回Class对象;如果直到启动类加载器也无法找到该类,则自顶向下尝试加载类,任意类加载器加载成功则直接返回。如果直到当前类加载器尝试加载但仍然失败时,抛出ClassNotFoundException异常。
public static Class<?> checkAndLoadMain(boolean var0, int var1, String var2) { initOutput(var0); String var3 = null; switch(var1) { case 1: var3 = var2; break; case 2: var3 = getMainClassFromJar(var2); break; default: throw new InternalError("" + var1 + ": Unknown launch mode"); } var3 = var3.replace('/', '.'); Class var4 = null; try { // scloader为App ClassLoader实例,调用scloader.loadClass方法跳转到ClassLoader抽象类中 ① var4 = scloader.loadClass(var3); } catch (ClassNotFoundException | NoClassDefFoundError var8) { if (System.getProperty("os.name", "").contains("OS X") && Normalizer.isNormalized(var3, Form.NFD)) { try { var4 = scloader.loadClass(Normalizer.normalize(var3, Form.NFC)); } catch (ClassNotFoundException | NoClassDefFoundError var7) { abort(var8, "java.launcher.cls.error1", var3); } } else { abort(var8, "java.launcher.cls.error1", var3); } } appClass = var4; if (!var4.equals(LauncherHelper.FXHelper.class) && !LauncherHelper.FXHelper.doesExtendFXApplication(var4)) { validateMainClass(var4); return var4; } else { LauncherHelper.FXHelper.setFXLaunchParameters(var2, var1); return LauncherHelper.FXHelper.class; } }
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. 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; } }
假设场景为应用程序new一个对象Student,但Student类未加载,具体步骤:
- App ClassLoader加载Student类,对应①
- App ClassLoader查找是否加载了Student类,对应②
- App ClassLoader未加载,委托其父加载器(Ext ClassLoader)进行加载,对应③
- Ext ClassLoader查找是否加载了Student类,对应②
- Ext ClassLoader未加载,其parent为null,委托BootStrap ClassLoader进行加载,对应④
- BootStrap ClassLoader没有加载到,则Ext ClassLoader尝试加载,对应⑤
- Ext ClassLoader没有加载到,App ClassLoader尝试加载,对应⑤
如何验证自顶向下加载
package com.example.demo;
// 实际路径为D:\WorkSpaces\demo\src\main\java\com\example\demo
public class Test {
public static void main(String[] args) {
Student student = new Student();
student.sayHello();
}
}
package com.example.demo; // 实际路径为D:\WorkSpaces\demo\src\main\java\com\example\demo public class Student { public void sayHello() { System.out.println("App ClassLoader"); } }
package com.example.demo; // 实际路径为D:\temp\com\example\demo,包名保持一致,并单独编译成class文件 public class Student { public void sayHello() { System.out.println("BootStrap ClassLoader"); } }
不带任何参数,执行Test#main()方法,打印如下:
App ClassLoader
添加JVM参数 -Xbootclasspath/a:D:/temp,修改BootStrap ClassLoader加载路径,打印如下:
BootStrap ClassLoader
双亲委派模型有何弊端?
双亲委派模型存在一个弊端:顶层ClassLoader无法加载底层ClassLoader的类。比如,Java框架(核心包rt.java)如何加载应用的类?举个例子,javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface SPI 位于rt.jar ,即接口在启动ClassLoader中,而SPI的实现类,在App ClassLoader。
JDK为我们提供了一个上下文加载器的概念,它只是一个角色,任何类加载器都可以被设置为上下文加载器。其基本思路是在顶层ClassLoader中传入底层ClassLoader的实例,用以解决顶层ClassLoader无法访问底层ClassLoader的类。
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
// 获取上下文ClassLoader
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
// 使用上下文ClassLoader
return cl.loadClass(className);
}
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}
......
打破双亲委派模型,自定义类加载器,实现自底向上加载
import java.net.URL;
import java.net.URLClassLoader;
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls) {
super(urls);
}
// 打破双亲模式,保证自己的类会被自己的classloader加载
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class c = findLoadedClass(name);
if (c == null) {
try {
// 先使用当前类加载器查找
c = findClass(name);
} catch (Exception e) {
}
}
// 若当前类加载器查找不到,使用父加载器查找
if (c == null) {
c = super.loadClass(name, resolve);
}
return c;
}
}
参考
- 【深入Java虚拟机】之四:类加载机制
- 《深入JVM内核—原理、诊断与优化》系列课程 - 葛一鸣老师