类加载器
根据 JVM 规范分为启动类加载器(Bootstrap ClassLoader)和非启动类加载器。
自定义一个类加载器
类加载器的命名空间:每个类加载器都有属于自己的命名空间,一个限定名类理论上只会被一个类加载器加载。通过以下代码验证,同一个限定名类被不同类加载器加载后,被 JVM 认为这是两个完全不一样的 Class
public static void main(String[] args) throws Exception {
// 自定义实现 classloader ,通过重写 java.lang.ClassLoader 的 loadClass 方法然后调用父类的 defineClass 进行加载
ClassLoader customClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
// 获取文件名
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 读取文件流
InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
if (resourceAsStream == null) {
return super.loadClass(name);
}
byte b[] = new byte[resourceAsStream.available()];
resourceAsStream.read(b);
// defineClass 的修饰符为 final,因此自定义 ClassLoader 只能在加载阶段搞事情,
// 说明一件事情,除了 Bootstrap ClassLoader,所有通过继承 java.lang.ClassLoader 的类加载器
// 只能自定义其加载范围和加载方法,最终都得去执行 defineClass
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
};
// 使用自定义的 customClassLoader 和 默认的 ApplicationClassLoader 分别对 Test_2 进行加载并进行类型判断
Object obj = customClassLoader.loadClass("com.dailycode.jvm.Test_2").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.dailycode.jvm.Test_2);
}
loadClass
加载字节码文件
defineClass
将给定的字节数组转化为 Class 实例
双亲委派模型
为什么要使用双亲委派模型
- 避免重复加载
- 防止核心类库的破坏,万一有人重写了 Object 类并放置了恶意代码
源码解读
// java.lang.ClassLoader.loadClass 源代码
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); // 1)验证是否已经被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 2)parent != null 则尝试由 parent 去加载
c = parent.loadClass(name, false);
} else { // 3)parent = null 说明 parent 为 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) { // 4)parent 无法加载,由当前 ClassLoader 去加载
// 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;
}
}
- 会先验证是否已经被加载过,如果没有加载过则会先去找父加载器进行加载,如果父加载器无法加载则由当前
ClassLoader
进行加载。
在向上寻找加载器的过程中,当发现获取的 parent 为null
,此时 parent 为Bootstrap ClassLoader
,启动类加载器由 JVM 管理无法被应用程序作为对象应用所以为 null。 - 关于父加载器的说法,它们之间并不是继承关系,仅仅代表的是层级,越是顶层的类库越是会被高层级的类加载器进行加载。
双亲委派模型的破坏
第一次的破坏产生
上文通过重写 loadClass
自定义类加载器,导致一个限定名类被两个类加载器加载,这就造成了双亲委派模型在初始阶段就遭到了破坏。loadClass
在 JDK
最开始就有了,而双亲委派模型是 JDK1.2
引入的,因为 JDK
是向下兼容的,也就导致这个方法不可以像 defineClass
一样用 final
修饰。为了弥补 loadClass
的造成的缺陷,在引入双亲委派模型的同时添加了一个 findClass
的方法。
public static void main(String[] args) throws Exception {
// 自定义实现 classloader ,通过重写 java.lang.ClassLoader 的 findClass 方法然后调用父类的 defineClass 进行加载
// 验证是否会破坏双亲委派模型
ClassLoader customClassLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 获取文件名
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 读取文件流
InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
if (resourceAsStream == null) {
return super.loadClass(name);
}
byte b[] = new byte[resourceAsStream.available()];
resourceAsStream.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
};
// 使用自定义的 customClassLoader 和 默认的 ApplicationClassLoader 分别对 Test_2 进行加载并进行类型判断
Object obj = customClassLoader.loadClass("com.dailycode.jvm.Test_2").newInstance();
System.err.println(obj.getClass());
System.err.println(obj instanceof com.dailycode.jvm.Test_2);
}
findClass
是在 JVM 内部实现的查找,依托于双亲委派模型自下而上的找寻 ClassLoader 去加载 .class 文件。loadClass
是实现双亲委派模型的核心代码,它在最后调用了findClass
方法。所以重写findClass
不会破坏双亲委派模型
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// 分别打印出 loadClass 和 findClass 两种方式使用的类加载器
Class<?> aClass = customClassLoader.loadClass("com.dailycode.jvm.Test_2");
System.out.println(aClass.getClassLoader());
第二次的破坏产生
JAVA 的 SPI 机制,可理解为一种服务发现机制亦各类服务都注册到 JDK 提供的接口上,上层在调用 JDK 接口时去选择用哪一个实现来加载,亦或理解为一种策略模式,根据配置来决定运行时接口的实现类是哪个。
假设 JDBC 就是 SPI 机制下的一类接口,然后 MySQL、H2 都实现了这一个接口,在加载的过程中,Bootstrap ClassLoader 委派了 Application ClassLoader 去加载,根据双亲委派模型的设计,是采用自下而上的模式进行加载的,而在 SPI 的设计中采用了上层加载器委派了下层加载器。
第三次破坏产生
热部署
思考
Class.forName(xx)、loadClass(xx) 的区别
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
Class.forName()
调用了一个 native
方法 forName0(String name, boolean initialize, ClassLoader loader, Class<?> caller)
可以看出,入参中有一个 initialize
代表是否初始化是写死的为 true
,也就意味着一定会加载静态变量和静态代码块。