写在前面
在 Java 面试中,在考察完项目经验、基础技术后,我会根据候选人的特点进行知识深度的考察,如果候选人简历上有写 JVM(Java 虚拟机)相关的东西,那么我常常会问一些 JVM 的问题。JVM 的类加载机制是一个很经典的知识点,围绕这个知识点可以有下面这些难度不同的问题。
- 简单讲下 JVM 中的类加载过程
- JVM 中的类加载和卸载的时机?
- 如何理解 JVM 中不同类加载器的概念和作用?
- 简单讲下 JVM 中的双亲委派模型?
- 什么情况下会破坏双亲委派模型?为什么?可否举个例子?
- Tomcat 中的类加载机制有了解吗?为什么这么设计?
- 实际开发中有遇到哪些类加载器相关的问题?你又是如何解决的?
- JVM 之上的弱类型语言例如 Groovy 是如何实现?简单讲下动态类加载机制?
在接下来的几篇文章,我将跟读者一起重新梳理一遍类加载器的相关知识,争取能够妥善解答上面列出的这些问题。
基本概念篇
类的加载和卸载
JVM 是虚拟机的一种,它的指令集语言是字节码,字节码构成的文件是 class 文件。平常我们写的 Java 文件,需要编译为 class 文件才能交给 JVM 运行。可以这么说:C 语言代码——> 二进制文件——> 计算机硬件,就相当于 Java 代码——> 字节码文件——>JVM。JVM 将指定的 class 文件读取到内存里,并运行该 class 文件里的 Java 程序的过程,就称之为 类的加载 ;反之,将某个 class 文件的运行时数据从 JVM 中移除的过程,就称之为 类的卸载。
class 文件的运行时数据就是 C ++ 对象,也称为 kclass 对象,这些运行时数据在 JDK7 之前是放在永久代(PermGen),JDK8 之后则放在元空间(Metaspace)。
类的生命周期
Java 类从被虚拟机加载开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段;其中验证、准备和解析又统称为连接(Linking)阶段。
类的加载的时机
虚拟机规范并未严格规定类加载的时机,跟具体的 JVM 虚拟机有关。类加载的最佳时机是解析 Java 字节码类文件中常量池符号的时候,Class.forName()、ClassLoader.loadClass()、反射 API 和 JNI_FindClass 都可以触发类加载,Hot JVM 自身启动的时候也会触发类加载。
通过 JVM 参数中加-verbose:class
,可以在应用启动的时候打印类加载的过程,如下图所示:
初始化这个阶段,JVM 虚拟机给出了 5 种必须对类进行“初始化”的情况
- 使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段的时候、调用一个类的静态方法的时候;
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要先触发其初始化;
- 当初始化一个类的时候,如果发现其父类还没有被初始化,则要先初始化其父类;
- 当虚拟机启动时,用户需要指定一个执行的主类(包含 main 方法的那个类),则虚拟机会优先初始化这个主类;
- 在 JDK1.7 以后,动态语言支持的时候,如果一个 java.lang.invoke.MethodHandle 实例最后的结果是要执行第 1 种情况的操作,则也要进行初始化。
类的卸载时机
类的卸载跟采用的垃圾收集算法有关,在 CMS 中有两种方法卸载不必要的类,一种是等到元空间(Metaspace)满了的时候触发 FGC,另一种是使用跟 CMS 并发收集算法类似的方式,不过对于元空间的阈值和触发 CMS 并发收集的阈值是独立的。更具体的可以参考之前的文章:CMS 学习笔记。在这里,我们只需要记住,JVM 中一个类的卸载要满足下面这 3 个条件:
- 该类所有的实例对象都已被回收;
- 该类的类加载器对象已经被回收;
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
类加载器的作用
类的加载是需要类加载器完成的,但是类加载器在 JVM 中的作用可不止这些。在 JVM 中,一个类的唯一性是需要这个类本身和类加载一起才能确定的,每个类加载器都有一个独立的命名空间。
不同的类加载器,即使是同一个类字节码文件,最后再 JVM 里的类对象也不是同一个,下面的代码展示了这个结论:
package jvm;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if (inputStream == null) {return super.loadClass(name);
}
try {byte[] b = new byte[inputStream.available()];
inputStream.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {throw new ClassNotFoundException();
}
}
};
Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof jvm.ClassLoaderTest);
ClassLoaderTest classLoaderTest = new ClassLoaderTest();
System.out.println(classLoaderTest.getClass());
System.out.println(classLoaderTest instanceof jvm.ClassLoaderTest);
}
}
上述代码的运行结果是:
可以看出,代码中使用自定义类加载器(myLoader)加载的 jvm.ClassLoaderTest 类和通过应用程序类加载器加载的类不是同一个类。综上,类加载器在 JVM 中的作用有:
- 将类的字节码文件从 JVM 外部加载到内存中
- 确定一个类的唯一性
- 提供隔离特性,为中间件开发者提供便利,例如 Tomcat
总结
今天的文章,应该可以回答文章开始提出的前两个问题,下篇再会。
参考资料
- https://jrebel.com/rebellabs/do-you-really-get-java-classloaders/
- https://stackoverflow.com/questions/2424604/what-is-a-java-classloader
- https://docs.oracle.com/javase/9/docs/api/java/lang/ClassLoader.html
- https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
- https://blogs.oracle.com/sundararajan/understanding-java-class-loading
- 《深入理解 Java 虚拟机》
- 《揭秘 Java 虚拟机》
- 《Java 性能权威指南》
本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。