本文来探讨编译后的字节码文件加载进入JVM的过程,以及加载中的双亲委派机制,并通过自定义的一个类加载破坏双亲委派机制,实现从目的目录读取字节码文件。
前置知识:Java内存区域划分
Java内存划分的核心是JVM运行时数据区,这是JVM在执行Java程序时使用的内存空间的总称。与操作系统物理内存不同,这是JVM抽象出的逻辑内存模型。
它有以下几个部分:
-
线程私有区域(生命周期和线程相同)
- 程序计数器:当前线程执行的字节码的行号指示器
- Java虚拟机栈:每个方法调用对应一个栈帧
- 本地方法栈:为Native方法(如C/C++实现的方法)服务
-
线程共享区域(生命周期和JVM相同)
- 堆:所有对象实例和数组的存放点,也是垃圾回收的主要区域
- 方法区:类信息,常量,静态变量,运行时常量池
类加载器
类加载器是JVM用于动态加载类文件到内存并转换为java.lang.Class对象的子系统。它是Java动态性的基石,实现了"按需加载"的原则。
java类加载器体系如下:
-
启动类加载器(
Bootstrap ClassLoader)- 用于加载核心
java库(JAVA_HOME/jre/lib)
- 用于加载核心
-
平台类加载器(
Platform ClassLoader)- 加载平台扩展库(
JAVA_HOME/jre/lib/ext)
- 加载平台扩展库(
-
应用程序类加载器(
Application ClassLoader)- 加载用户类路径(
CLASSPATH)
- 加载用户类路径(
-
自定义类加载器
- 用户自定义的类加载器
JDK8之前称为扩展类加载器(Extension ClassLoader),JDK9+更名为平台类加载器(Platform ClassLoader)。
Java类加载的生命周期
Java类加载的生命周期,指的是一个.class文件(字节码)从被加载到JVM内存,到最终被卸载出内存所经历的完整过程。
完整的生命周期包含:
加载 -> 连接 (验证 -> 准备 -> 解析) -> 初始化 -> 使用 -> 卸载
-
加载
- 通过类加载器以及其子类完成,通过类的全限定名获取定义此类的二进制字节流(来源不限:文件、网络、ZIP包、运行时生成等)
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在堆内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
-
验证
- 作用:确保被加载的类的字节流符合JVM规范,是保护JVM安全的重要屏障。包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 为什么要验证:防止黑客篡改
.class文件,或编译器生成的有问题的字节码危害JVM。
-
准备
- 作用:为类变量(
static变量)在方法区分配内存,并设置初始零值。 - 一般设置的是数据类型的零值,而不是代码中赋予的值,但如果类变量是
final static常量(编译期常量),则准备阶段会直接赋值为代码中的值。
- 作用:为类变量(
示例:
public class TestPreparation {
public static int staticValue = 123; // 准备阶段后,值为 0
public static final int CONSTANT = 456; // 准备阶段后,值为 456
}
4. 解析 * 作用:将常量池内的符号引用替换为直接引用的过程。 * 常量池 是Java类文件(.class文件)中的一张“资源索引表”,它存储了该类所用到的所有常量、方法名、字段名、类名等符号信息。 * 符号引用:用一组符号(如全限定名)来描述所引用的目标。与JVM内存布局无关。 * 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。与JVM内存布局相关。
示例:
// Test.java
public class Test {
public void testMethod() {
// 这里引用了Object的toString方法
Object obj = new Object();
String str = obj.toString();
System.out.println(str);
}
}
查看编译后的字节码文件
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Methodref #2.#8 // java/lang/Object.toString:()Ljava/lang/String;
#8 = NameAndType #9:#10 // toString:()Ljava/lang/String;
#9 = Utf8 toString
#10 = Utf8 ()Ljava/lang/String;
// ... 其他常量
这些就是常量池中的符号引用。将这些符号引用变成实际的指针,就是解析过程。
-
初始化
- 作用:执行类构造器
<clinit>()方法 的过程。<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。 <clinit>()方法会保证父类的 ()先执行。- 虚拟机会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步。这确保了类初始化是线程安全的。
- 作用:执行类构造器
-
使用
类完成初始化后,就可以在程序中被正常使用,创建对象、调用方法等。
-
卸载
当一个类的
Class对象不再被引用,并且其对应的类加载器也被回收时,该类的方法区数据会被JVM在垃圾回收时卸载。在由系统类加载器加载的普通应用中,类的卸载几乎很少发生。但在使用自定义类加载器的场景(如OSGi、热部署)下,卸载变得重要。
类加载器的设计
类加载器有一些特点:
- 按需加载:Java程序启动时不会一次性加载所有类,而是在首次使用时才加载,减少内存开销。
- 保证核心类库安全:通过双亲委派模型,防止用户自定义类替换核心Java类(如
java.lang.String)。 - 实现类隔离:不同类加载器加载的类,即使全限定名相同,也被视为不同的类,这在Web容器、模块化系统中至关重要。
- 支持热部署:通过自定义类加载器,可以在不重启JVM的情况下重新加载类,实现热部署功能。
双亲委派机制
为什么要使用双亲委派机制?
- 防止核心类被篡改,确保安全: 比如,如果有人自定义了一个
java.lang.Object类,里面写了恶意代码。如果没有双亲委派,这个类可能被加载,从而污染整个JVM。但在双亲委派下,加载java.lang.Object的请求会一直向上委派给启动类加载器,由它去加载rt.jar中的、真正的、官方的Object类。这样,自定义的恶意Object类永远没有机会被加载,保证了Java核心API的纯洁与安全。 - 避免类的重复加载: 如果一个类已经被父加载器加载过了,那么子加载器在收到加载请求时,会发现父加载器已经加载,从而直接返回已存在的Class对象。这保证了在JVM中,同一个类加载器+全限定类名能唯一确定一个Class对象,避免了内存中存在多份相同的类结构,防止了行为混乱。
- 保证基础类的统一行为:像
java.lang.String、java.util.HashMap这样的核心类,在任何地方(用户的代码、第三方库中)被引用时,最终指向的都是由启动类加载器加载的同一个Class对象。这确保了程序运行时,对这些基础类的理解和使用方式是全局一致的。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查类是否已加载,只能检查由当前这个类加载器实例自己加载的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 委托给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器找不到该类
}
// 3. 父类加载器找不到,调用自己的findClass
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
// 记录统计信息
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
// 4. 如果需要,进行解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过用户自定义的类加载可以打破双亲委派机制,以下给出一个从目的目录中加载类的FileSystemClassLoader
public class FileSystemClassLoader extends ClassLoader {
private final String classPath;
public FileSystemClassLoader(String classPath) {
this.classPath = classPath;
}
public FileSystemClassLoader(String classPath, ClassLoader parent) {
super(parent);
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException("类未找到: " + name);
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String className) {
// 将包名转换为文件路径
String path = className.replace('.', File.separatorChar) + ".class";
Path classFile = Paths.get(classPath, path);
try {
return Files.readAllBytes(classFile);
} catch (IOException e) {
System.err.println("加载类文件失败: " + classFile);
return null;
}
}
// 测试方法
public static void main(String[] args) throws Exception {
// 假设在D:/myclasses目录下有com.example.TestClass.class
String classPath = "D:/myclasses";
FileSystemClassLoader loader = new FileSystemClassLoader(classPath);
try {
Class<?> clazz = loader.loadClass("com.example.TestClass");
System.out.println("类加载成功: " + clazz.getName());
System.out.println("类加载器: " + clazz.getClassLoader().getClass().getName());
System.out.println("父加载器: " + clazz.getClassLoader().getParent().getClass().getName());
// 创建实例
Object instance = clazz.newInstance();
System.out.println("实例创建成功: " + instance);
} catch (ClassNotFoundException e) {
System.err.println("类未找到: " + e.getMessage());
}
}
}