文章目录
本篇系统介绍class loader 及其应用.
一 what is Classloader
笔者以为,在讨论计算机(编程) 中的概念时首先一定要确定 概念所在的维度(范畴)是什么,否则会发现,大家说的貌似都对, 但哪里总有点不对.
当我们说 Java Classloader
时,我们要搞清楚是说 Classloader
这个类/对象呢, 还是 Classloader
机制.
二 内置Classloader
这个图是网上泛滥的, 面试说烂了的 Classloader
继承机制,学名叫做 双亲委派. 这个翻译很囧, 英文其实是parent delegating
, 本意是 单亲委派…
这里留个坑,后面我们继续讲 怎么delegating
.
2.1 Bootstrap Classloader
Bootstrap Classloader
是Classloader体系中的顶层加载器. “顶层"的意思就是说” 我是祖先".
Classloader体系中的 Bootstrap Classloader
和 Object
类作为所有的类的顶级父类, 是有着异曲同工之妙的.
Bootstrap Classloader
是使用C++写的, 负责JVM最核心类库的加载,比如 java.lang 包, e.g., String.class. 可以通过-xbootclasspath
指定Bootstrap Classloader
的路径,也能通过系统属性得知当前Bootstrap Classloader
加载了哪些资源.
有意思的是 ,Bootstrap Classloader
是没法从JVM中拿到引用的.
public static void main(String[] args) {
// BootstrapClassloader=>null 根类加载器是获取不到引用的
System.out.println("BootstrapCL=>" + String.class.getClassLoader());
// E:\Java\jdk1.8.0_121\jre\lib\resources.jar;
// E:\Java\jdk1.8.0_121\jre\lib\rt.jar;
// E:\Java\jdk1.8.0_121\jre\lib\sunrsasign.jar;
// E:\Java\jdk1.8.0_121\jre\lib\jsse.jar;
// E:\Java\jdk1.8.0_121\jre\lib\jce.jar;
// E:\Java\jdk1.8.0_121\jre\lib\charsets.jar;
// E:\Java\jdk1.8.0_121\jre\lib\jfr.jar;
// E:\Java\jdk1.8.0_121\jre\classes
System.out.println(System.getProperty("sun.boot.class.path"));
// C:\WINDOWS\Sun\Java\lib\ext
System.out.println(System.getProperty("java.ext.dirs"));
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(CL.class.getClassLoader());
}
2.2 ExtClassloader
ExtClassloader
用来加载 JAVA_HOME下的 jre\lib\ext
库,类似的, 可以通过 java.ext.dirs
获取路径.
我们可以做一个小测试,写一个最简单的Hello world
, 打成jar,然后将该jar 放到
JAVA_HOME下的 jre\lib\ext
下, 再跑下列的main ,会发现 是ExtClassLoader
; 如果 再把这个jar删除了, 又是AppClassLoader
.
public static void main(String[] args) throws Exception{
// sun.misc.Launcher$ExtClassLoader@5f5a92bb
ClassLoader loader = Class.forName("com.code.hello.Hello").getClassLoader();
System.out.println(loader);
// sun.misc.Launcher$AppClassLoader@18b4aac2
}
2.3 AppClassloader
AppClassloader
,也叫 SystemClassloader
(系统classloader), 负责加载类路径下的类库资源,说白了就是你的应用的类路径. AppClassloader
是自定义Classloader的父Classloader.
那怎么获取类路径呢? System.getProperty("java.class.path")
2.4 小结
前面说的三个大佬是内置ClassLoader
, 实际在很多中间件中有自定义的ClassLoader
,比如tomcat的ClassLoader
就是个经典案例.
三 自定义ClassLoader
ClassLoader
是一个抽象类, 要实现自定义的ClassLoader
,只要 继承并override findClass
方法即可,比如下面的代码.
这个MyCL
类就是自定义的ClassLoader
, 它从磁盘读取一个class 文件, 加载进JVM, 甚至还new 出了一个对象!
public class MyCL extends ClassLoader {
private static final String defaultDir = "G:/";
public MyCL() {}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 全路径的类名
String clazzFilePath = defaultDir + name.replace(".", "/") + ".class";
byte[] clazzBytes;
try {
clazzBytes = Files.readAllBytes(Paths.get(clazzFilePath));
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("Failed to find class:", e);
}
return defineClass(name, clazzBytes, 0, clazzBytes.length);
}
public static void main(String[] args) throws Exception {
MyCL myCL = new MyCL();
Class<?> clazz = myCL.loadClass("com.code.hello.Hello");
Object instance = clazz.newInstance();
// com.code.cl.MyCL@1ff8b8f
System.out.println(instance.getClass().getClassLoader());
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(instance.getClass().getClassLoader().getParent());
}
}
3.1 单亲委派
为啥叫单亲委派,看看源码即知.
我们并没有看到 "双亲委派"中的"双亲"体现在哪里. 这种机制本质就是: 总是让父加载器优先去加载.
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// findLoadedClass()本质是个 native方法,用来记录当前这个加载器是不是已经加载过这个类
Class<?> c = findLoadedClass(name);
// 这个类并没有加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 若父加载器存在, 则委派给 父加载器 会加载
// 这里一定要理解下, parent.loadClass()同样会执行一次 loadClass
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);
// ingore....
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
3.2 如何绕过单亲委派
假如有个HelloWorld.class ,默认由 AppClassLoader
加载. 假如我想绕过去,i.e., 使用MyCL
去加载呢?
前面第 二 节给出了一种做法. 我们还有更多的方法.
第二种: 利用特性:当前类没有父类加载器的情况下,直接使用根加载器加载.根加载器下肯定没有 Hello2
这个类,自然就使用当前类加载器加载了.
以下的所有demo, 代码都是独立的.
public class MyCL2 extends ClassLoader {
private static final String defaultDir = "G:/github/code-snipptes/target/classes";
public MyCL2(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 全路径的类名
Path clazzFilePath = Paths.get(defaultDir).resolve(name.replace(".", "/") + ".class");
byte[] clazzBytes;
try {
clazzBytes = Files.readAllBytes(clazzFilePath);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("Failed to find class:", e);
}
return defineClass(name, clazzBytes, 0, clazzBytes.length);
}
public static void main(String[] args) throws Exception {
MyCL2 myCL = new MyCL2(null);
Class<?> clazz = myCL.loadClass("com.code.cl.Hello2");
Object instance = clazz.newInstance();
// com.code.cl.MyCL2@387c703b
System.out.println(instance.getClass().getClassLoader());
//打印结果是 null; 注意::: 当父加载器不存在,也就是前面我们 new MyCL2(null), 会直接使用根加载器对该类进行加载
System.out.println(instance.getClass().getClassLoader().getParent());
}
}
第三种: 利用特性:在new 自定义classloader 时传参 父加载器, 那么该自定义classloader 即为父加载器的’儿子’.
public class MyCL3 extends ClassLoader {
private static final String defaultDir = "G:/github/code-snipptes/target/classes";
public MyCL3(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 全路径的类名
Path clazzFilePath = Paths.get(defaultDir).resolve(name.replace(".", "/") + ".class");
byte[] clazzBytes;
try {
clazzBytes = Files.readAllBytes(clazzFilePath);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("Failed to find class:", e);
}
return defineClass(name, clazzBytes, 0, clazzBytes.length);
}
public static void main(String[] args) throws Exception {
ClassLoader classLoader = MyCL3.class.getClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("MyCl3 cl:" + classLoader);
// 注意: 这里使用的是 父加载器
MyCL3 myCL = new MyCL3(classLoader.getParent());
Class<?> clazz = myCL.loadClass("com.code.cl.Hello3");
Object instance = clazz.newInstance();
// com.code.cl.MyCL3@387c703b
System.out.println(instance.getClass().getClassLoader());
// sun.misc.Launcher$ExtClassLoader@574caa3f
System.out.println(instance.getClass().getClassLoader().getParent());
}
}
3.3 如何破坏单亲委派
前面说的是 “绕过去APPClassloader”,有何方法去破坏单亲委派呢?
最常见的破坏场景如: 热部署. 热部署需要卸载掉类加载器, 但当卸载一个类加载器, 会导致所有类都卸载掉. 内置的三个大佬级类加载器肯定没法卸载, 我们只能卸载自定义加载器.
破坏单亲委派主要有2种方法:
1.基于SPI, 典型如 JDBC
2. 自定义classloader重写 loadClass()方法
比如:
public static void main(String[] args) throws Throwable{
// AppClassloader 加载了 com.mysql.jdbc.Driver
Class.forName("com.mysql.jdbc.Driver");
// java.sql.xxx 则由 BootstrapClassloader加载(因为在 jdk\jre\lib\rt.jar 中)
// BootstrapClassloader加载的类 (java.sql.xxx )使用了 AppClassloader 加载的类 (com.mysql.jdbc.Driver),
// 其实底层靠 DriverManager.getConnection() 中的 Thread.currentThread().getContextClassLoader() 来破坏单亲委派
String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
}
四 类加载器 namespace
我们经常通俗地说,一个class 在一个JVM中是唯一的, 其实是不严谨的说法. 每一个类加载器都所有自己的 namespace. namespace由该加载器和父加载器构成. 同一个class实例在同一个类加载器namespace下是唯一的.
尤其我们使用的单例设计模式, 如果单例对象是通过不同类加载器的加载得到,那么本质上并不是一个单例.
五 初始类加载器
不同的运行时包彼此之间是不能访问的. 那为啥下面这个再简单不过的例子里, 我们的 APPClassloader 加载的 A 类能够访问到 根加载器加载的 String 呢?
举个栗子:
public class SimpleClass {
public static void main(String[] args) {
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null (根加载器)
ClassLoader classLoader2 = A.class.getClassLoader();
System.out.println(classLoader2); // sun.misc.Launcher$AppClassLoader@18b4aac2 (AppClassloader)
}
class A{
static String a = "a";
}
}
这个嘛,是因为JVM有个规范. 在类加载过程中,所有参与的类加载器,即使没有亲自加载该类,也会被标识为 初始类加载器.
JVM为每一个类加载器维护一个列表,其中记录了将该类加载器作为初始类加载器的所有class,在加载一个类时,JVM使用这些列表判断该类是不是已经加载过了,要不要首次加载.