概述
目录
- 前言
- 一、双亲委派
- 1.1 类加载器结构
- 1.2 双亲委派
- 二、自定义类加载器
- 2.1 全盘委派
- 2.2 覆盖核心类?
- 三、TCCL
- 四、spring的类加载
前言
在深入openjdk源码全面理解Java类加载器(上 – JVM源码篇)我们分析了JVM是如何启动,并且初始化BootStrapClassLoader的,也提到了sun.misc.Launcher被加载后会创建ExtClassLoader和AppClassLoader。关于类加载的基础知识请参考虚拟机类加载机制(上)。这篇文章主要从Java源码层面总结一下双亲委派、TCCL的应用等,然后再聊聊自定义类加载器的注意事项。
一、双亲委派
1.1 类加载器结构
直接在idea里看看AppClassLoader的继承关系(ExtClassLoader一样):
AppClassLoader和ExtClassLoader都继承自URLClassLoader,URLClassLoader继承自SecureClassLoader,最终继承自ClassLoader。类加载的核心方法以private native定义在ClasssLoader中,只能由ClassLoader调用,所以所有的自定义类加载器都必须直接或间接继承ClassLoader。
1.2 双亲委派
加载类的核心方法是loadClass,默认实现在ClassLoader中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//同步锁,可能是一个和name对应的Object,也可能是this
//取决于类加载器是否具备并行能力
//首先检查类是否被本类加载器加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有找到需要加载的类
long t0 = System.nanoTime();
try {
//使用父类加载器加载类
//如果parent不为null,说明设置了父加载器,直接用parent
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果parent为null,使用BootStrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果父类加载器没能加载到类,使用本类加载器加载
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;
}
}
注:关于getClassLoadingLock,可参考:关于类加载的并发控制锁。
从loadClass的逻辑中可以很清晰的看到双亲委派的实现:首先查看类是否已经加载,如果未加载则委派给父类加载,如果父类加载器没能加载成功,那么才由本类加载器加载。
通常情况下,所有Java实现的类加载器都是调用ClassLoader的这个loadClass方法,所以本类加载和父类加载器都是这个逻辑:本类加载器委托父类加载器,父类加载器委托租父类加载器等等。顶层类加载器如果无法加载则依次回溯。
二、自定义类加载器
自定义类加载器需要直接或间接继承ClassLoader,最简单的一个自定义类加载器就是继承ClassLoader,重写其findClass方法,通过ClassLoader.defineClass方法创建一个Class类(defineClass最终会调用ClassLoader的native方法):
public class MyClassLoader extends ClassLoader {
private URLClassPath ucp;
public MyClassLoader(String path, ClassLoader parent) throws Exception {
super(parent);
this.ucp = new URLClassPath(new URL[]{new File(path).toURI().toURL()});
}
static {
ClassLoader.registerAsParallelCapable();
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String usePath = name.replace('.', File.separatorChar).concat(".class");
Resource resource = ucp.getResource(usePath, false);
if (resource != null) {
try {
byte[] bytes = resource.getBytes();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException var) {
return null;
}
} else {
return null;
}
}
}
MyClassLoader从我们指定的路径搜寻类文件,如果没有找到,那么父类ClassLoader的加载逻辑会遵循双亲委派交给我们指定的父类加载器加载,若未指定,那么寻找BootStrapClassLoader。
正是由于我们只重写了findClass方法,类加载的过程还是双亲委派的逻辑,这也是Java官方建议的自定义类加载的方式。但是如果我们需要打破双亲委派规则,就必须重写loadClass方法,比如:
public class MyClassLoader2 extends ClassLoader {
private URLClassPath ucp;
public MyClassLoader2(String path, ClassLoader parent) throws MalformedURLException {
super(parent);
this.ucp = new URLClassPath(new URL[]{new File(path).toURI().toURL()});
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("com.demo")) {
Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false);
if (resource != null) {
try {
byte[] bytes = resource.getBytes();
Class clazz = defineClass(name, bytes, 0, bytes.length);
if (resolve) {
resolveClass(clazz);
}
return clazz;
} catch (IOException e) {
throw new ClassNotFoundException(e.getMessage());
}
} else {
throw new ClassNotFoundException();
}
} else {
return super.loadClass(name, resolve);
}
}
}
这个类加载器对于com.demo包的类都由自己加载,其余的才委托给父类。测试一下:
public class JavaMain {
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader());
Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
System.out.println(clazz.getClassLoader());
}
}
//输出 com.demo.classloader.MyClassLoader2@5cad8086
2.1 全盘委派
在我们的这个工程中,有一个问题,如果运行以下代码:
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader());
Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
System.out.println(clazz.getClassLoader());
System.out.println(TestClass1.class.getClassLoader());
TestClass1 testClass1 = (TestClass1) clazz.newInstance();
}
//输出:
com.demo.classloader.MyClassLoader2@5cad8086
sun.misc.Launcher$AppClassLoader@18b4aac2
ClassCastException
类型强转操作会抛出java.lang.ClassCastException异常。造成这个的原因已经在输出结果中体现了,clazz是由自定义类加载加载的,而TestClass1.class是由AppClassLoader加载的。可以打印看看AppClassLoaer的加载目录:
System.out.println(System.getProperty("java.class.path"));
在mac下结果如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/lib/tools.jar:/Users/loren/work/github/test/out/production/test:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar
输出的目录包含了当前项目目录,所以目录中的class可以被AppClassLoader加载。
注:除了当前项目目录,还有很多系统jar包,包括rt.jar、jce.jar等,当然由于AppClassLoader遵循双亲委派,路径包含这些jar包也不会有什么问题。
那么TestClass1是什么时候被AppClassLoader加载的呢?对于上述代码中的:
......
1.Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
2.System.out.println(clazz.getClassLoader());
3.System.out.println(TestClass1.class.getClassLoader());
......
当代码执行到第三行打印TestClass1.class.getClassLoader的时候,会检查TestClass1.class是否已经被加载,如果没有加载则需要触发类加载的逻辑。这里需要注意的是,当前类(JavaMain)是被AppClassLoader加载的,它所依赖的类默认也会使用加载当它的类加载器(也就是AppClassLoader)去检查,这个叫做“全盘委派机制”(我也不知道官方是不是叫这个名字)。
为了验证这一点,我们再新建一个TestClass2.java,在构造方法中打印类加载器:
public class TestClass2 {
public TestClass2() {
System.out.println("testClass2.classLoader:" + this.getClass().getClassLoader());
}
}
然后在TestClass1中创建一个方法触发TestClass2的实例化:
public class TestClass1 {
public void run() {
new TestClass2();
}
}
由于main方法中使用TestClass1会被AppClassLoader加载,所以我们不能强转类型,只能通过反射调用该方法:
public class JavaMain {
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
MyClassLoader2 myClassLoader = new MyClassLoader2(classPath, JavaMain.class.getClassLoader());
Class clazz = myClassLoader.loadClass("com.demo.classloader.TestClass1", false);
Object obj = clazz.newInstance();
Method method = obj.getClass().getDeclaredMethod("run", null);
method.setAccessible(true);
method.invoke(obj, null);
}
}
输出如下:
testClass2.classLoader:com.demo.classloader.MyClassLoader2@5cad8086
2.2 覆盖核心类?
如果用户自定义一个全路径相同的Java核心类,能否有办法覆盖原版呢?正常情况下,根据双亲委派机制是没办法的:根据委派规则,加载动作会委派到BootStrapClassLoader,而BootStrap能加载这些核心类。既然如此,那么我们打破双亲委派尝试一下。
首先在项目中创建一个java.util.HashMap:
package java.util;
public class HashMap {
}
然后创建一个自定义类加载器,这个和之前类似:
public class MyClassLoader3 extends ClassLoader {
private URLClassPath ucp;
public MyClassLoader3(String path, ClassLoader parent) throws MalformedURLException {
super(parent);
this.ucp = new URLClassPath(new URL[]{new File(path).toURI().toURL()});
}
static {
ClassLoader.registerAsParallelCapable();
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Resource resource = ucp.getResource(name.replace('.', File.separatorChar).concat(".class"), false);
if (resource != null) {
try {
byte[] bytes = resource.getBytes();
Class clazz = defineClass(name, bytes, 0, bytes.length);
if (resolve) {
resolveClass(clazz);
}
return clazz;
} catch (IOException e) {
throw new ClassNotFoundException(e.getMessage());
}
} else {
throw new ClassNotFoundException();
}
}
}
在main方法中创建自定义类加载器,加载路径为当前项目路径,然后尝试加载java.util.HashMap:
public static void main(String[] args) throws Exception {
String classPath = JavaMain.class.getClassLoader().getResource("").getPath();
MyClassLoader3 myClassLoader = new MyClassLoader3(classPath, JavaMain.class.getClassLoader());
Class clazz = myClassLoader.loadClass("java.util.HashMap", false);
}
当然不出意外的是,有异常堆栈抛出:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.util
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
at com.demo.classloader.MyClassLoader3.loadClass(MyClassLoader3.java:29)
at com.demo.classloader.JavaMain.main(JavaMain.java:13)
提示禁止加载包:java.util,看堆栈信息异常是ClassLoader.preDefineClass抛出来的,看看相应的代码:
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd){
......
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
......
}
源码写的很清楚,java.打头的包都不允许加载,所以我们项目中建包还是不要以java打头。
既然检查工作是在preDefineClass中完成的,那么我们能否绕过predefineClass方法呢?
现在回到类加载的流程,我们先通过findClass找到需要加载的字节码文件,这一步没有问题。找到字节码文件之后,需要调用defineClass方法生成Class,defineClass定义在ClassLoader中:
protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
该方法是一个final方法,我们无法重写,那能在自定义类加载器中调用defineClass1方法吗?defineClass1方法定义在ClassLoader中,是一个private native方法:
private native Class<?> defineClass0(String name, byte[] b, int off, int len,ProtectionDomain pd);
private native Class<?> defineClass1(String name, byte[] b, int off, int len,ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,int off, int len, ProtectionDomain pd,String source);
所以我们只能通过父类的defineClass创建Class,也就没法绕过preDefineClass方法的检查。既然如此,那能不能从本地方法入手呢?理论上是可行的,但是需要修改动态链接文件。但是都能操作dll了,还需要费尽心思去覆盖核心类库吗?
三、TCCL
Thread Context ClassLoader(TCCL),即线程上下文类加载器。对于一些场景,可能会需要父类加载器调用子类加载器的情况,一个典型的例子就是SPI。
对于某些功能,比如日志、JDBC等等,Java本身只提供接口,由用户自己实现或选择第三方提供的实现类,这样遵循了可插拔的特性。为了支持这点,Java提供了一种服务发现机制:为一些接口寻找具体的实现类。当作为服务提供者实现了某个服务接口之后,需要在jar包的META-INF/services/目录下创建一个以服务接口全限定名命名的文件,将接口实现类全限定名配置在该文件中。JDK提供了一个根据此规则寻找服务实现者的工具:ServiceLoader。使用ServiceLoader可以找到指定接口的实现类,进而完成服务实现者的加载。
这其中出现的问题就是ServiceLoader是由启动类加载器加载,而服务实现者并不在其能加载的文件允许范围内,于是便出现了冲突。TCCL便能够解决这个问题,Thread类有一个contextClassLoader成员变量:
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
通过相应的set方法:
Thread.currentThread().setContextClassLoader(classloader);
将一个类加载器和线程绑定。这样在一个线程中,需要加载当前类加载器无法加载的类的时候,可以从当前线程中获取TCCL进行加载:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
TCCL默认为AppClassLoader,初次在sun.misc.Launcher的构造方法中设置:
public Launcher() {
......
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//TCCL默认为AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
......
}
四、spring的类加载
对于一个servlet容器来说,还是以Tomcat为例。一个webapps可以同时部署多个应用,而每个应用可能引用相同的jar包,在没有版本冲突的情况下,可以把这些jar包放到shared目录,由SharedClassLoader加载(不考虑高版本合并到lib目录),以达到让每个WebAppClassLoader共享的目的。
对于每个webapp来说,其字节码文件默认由各自的WebAppClassLoader加载。但是像spring这种bean工厂来说,它要管理bean,就要能加载这些类,但是如果spring的jar包放在上层目录,其类加载器是无法加载webapp下的类的,该如何是好呢?
其实这也是一个父类加载器需要反向调用的例子,使用TCCL就可以解决:spring在加载一个类的时候从当前线程获取TCCL,而servlet容器将TCCL设置为WebAppClassLoader。这样不论哪个webapp使用spring,spring使用的都是各自的WebAppClassLoader。就像这样:
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
//spring在webapp下,类加载器相同
currentContext = this.context;
} else if (ccl != null) {
//加载spring的类加载器和TCCL不同,将classLoader和WebApplicationContext用map
//保存起来,用的时候根据classLoader获取context
currentContextPerThread.put(ccl, this.context);
}
当然,如果在SpringBoot中使用内嵌servlet容器的时候,就不会出现一个servlet容器包含多个应用的情况了,也就不用再用map维护不同的context了,直接使用TCCL即可:
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl;
最后
以上就是单薄鸡为你收集整理的深入OpenJDK源码全面理解Java类加载器(下 -- Java源码篇)前言一、双亲委派二、自定义类加载器三、TCCL四、spring的类加载的全部内容,希望文章能够帮你解决深入OpenJDK源码全面理解Java类加载器(下 -- Java源码篇)前言一、双亲委派二、自定义类加载器三、TCCL四、spring的类加载所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复