1. JVM 类加载机制 学习笔记
1.1. 类加载的基本概念
类加载机制是 JVM 将类的字节码文件(.class)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。
1.1.1. 类加载的时机
JVM 并不是在程序启动时就把所有类都加载到内存中,而是按需加载。类加载的时机包括:
- 创建类的实例(new)
- 访问类的静态变量或静态方法
- 反射调用(Class.forName())
- 初始化子类时,父类会被先加载
- JVM 启动时指定的主类
1.2. 类加载的过程
类加载过程分为以下阶段:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
1.2.1. 加载(Loading)
首先加载代码中main()
方法主类,然后对主类引用的类不断的进行加载。
public class Kafka {
// 从该入口开始加载
public static void main(String[] args) {
System.out.println("加载中......");
// 主方法使用到的类,也会进行加载
ReplicaManager rm = new ReplicaManager();
}
}
加载阶段主要完成三件事:
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
1.2.2. 验证(Verification)
验证的是上一步加载进来的.class
文件内容,判断是否符合规范,是否被人篡改。符合规范才会进行下一步操作。
验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,包括:
- 文件格式验证(魔数、版本号等)
- 元数据验证(语义分析)
- 字节码验证(程序逻辑)
- 符号引用验证(常量池中的各种符号引用)
1.2.3. 准备(Preparation)
进行到该步骤,就是将代码中使用的变量等,分配内存空间,没有初始值的,附上初始默认值。
public class ReplicaManager {
// 准备阶段会对变量分配空间,设置默认初始值
public static int flush_interval;
}
准备阶段为类变量(static 变量)分配内存并设置初始值(零值):
- 基本类型:int=0, long=0L, boolean=false 等
- 引用类型:null
- 常量(final static)会直接赋值为代码中指定的值
1.2.4. 解析(Resolution)
该阶段是把符号引用替换为直接引用的过程,这个部分很复杂,涉及到JVM的底层。现在不做过深的解读,所以暂时知道有这个阶段就可以了。
解析阶段将常量池内的符号引用替换为直接引用:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
1.2.5. 初始化(Initialization)
该阶段会执行类的初始化代码,例如,从配置项中获取参数数据、static 静态代码块等。
public class ReplicaManager {
// 初始化会从配置文件中获取配置数据
public static int flush_interval = Configuration.getInt("replica.flush.interval");
public static Map<String, Replica> replicas;
// 初始化时,会执行静态代码块
static {
loadReplicaFromDish();
}
public static void loadReplicaFromDish() {
this.replicas = new HashMap<String, Replica>();
}
}
初始化一个类的时候,如果其父类,没初始化,必须先初始化其父类。
// 如果有父类,在初始化的时候,要先初始化父类
public class ReplicaManager extends AbstractDataManager {
// 初始化会从配置文件中获取配置数据
public static int flush_interval = Configuration.getInt("replica.flush.interval");
public static Map<String, Replica> replicas;
// 初始化时,会执行静态代码块
static {
loadReplicaFromDish();
}
public static void loadReplicaFromDish() {
this.replicas = new HashMap<String, Replica>();
}
}
初始化阶段是执行类构造器 <clinit>()
方法的过程:
<clinit>()
方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块合并产生- JVM 保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕 - 接口的实现类初始化时不会执行接口的
<clinit>()
方法
1.3. 类加载器
上述类加载的过程,通过类加载器来实现。
1.3.1. 类加载器分类
JVM 加载器种类(前三种为 JVM 提供的):
启动类加载器(Bootstrap ClassLoader)
- 由 C++ 实现,是 JVM 的一部分
- 负责加载 Java 核心类库(JAVA_HOME/lib 目录下的类)
- 是唯一没有父加载器的加载器
扩展类加载器(Extension ClassLoader)
- Java 实现,继承自 java.lang.ClassLoader
- 负责加载 JAVA_HOME/lib/ext 目录下的类
- 父加载器是 Bootstrap ClassLoader
应用程序类加载器(Application ClassLoader)
- 也称为系统类加载器(System ClassLoader)
- 负责加载用户类路径(ClassPath)上的类库
- 父加载器是 Extension ClassLoader
自定义类加载器
- 自定义加载器,根据自己的需求加载类
1.3.2. 双亲委派模型
双亲委派模型的工作流程:
- 当一个类加载器收到类加载请求时,首先不会自己尝试加载,而是委托给父加载器
- 只有当父加载器反馈无法完成加载请求时,子加载器才会尝试自己加载
双亲委派模型的优点:
- 避免类的重复加载
- 保证 Java 核心 API 不被篡改(如 java.lang.Object 始终由 Bootstrap ClassLoader 加载)
双亲委派模型的结构:
JVM的类加载器是有亲子层级结构的,启动类加载器是最上层,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器。
1.3.3. 破坏双亲委派模型
在某些场景下需要打破双亲委派模型:
- SPI(Service Provider Interface)机制:如 JDBC 驱动加载
- OSGi:模块化热部署
- 热部署:如 Tomcat 的 Web 应用类加载器
Tomcat类加载机制图
Tomcat需要破坏双亲委派模型的原因:
(1)tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离
(2)同一个第三方类库的相同版本在不同web应用可以共享
(3)tomcat自身依赖的类库需要与应用依赖的类库隔离
(4)jsp需要支持修改后不用重启tomcat即可生效 为了上面类加载隔离和类更新不用重启,定制开发各种的类加载器
1. 按委派模型的流程图
2. 实际模型的流程图
1.4. 自定义类加载器
实现自定义类加载器的步骤:
- 继承 java.lang.ClassLoader
- 重写 findClass() 方法(推荐)或 loadClass() 方法(破坏双亲委派)
- 在 findClass() 中实现类加载逻辑
示例代码:
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
private byte[] loadClassData(String className) throws IOException {
String path = classPath + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try (InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
return baos.toByteArray();
}
}
}
1.5. 类加载的常见问题
1.5.1. ClassNotFoundException vs NoClassDefFoundError
- ClassNotFoundException:在类加载阶段找不到类定义(如 Class.forName() 调用)
- NoClassDefFoundError:在类解析阶段找不到类定义(如类已加载但被删除)
1.5.2. 类初始化死锁
当两个线程同时初始化一个类时可能发生死锁:
- 线程1:持有类A的锁,等待类B初始化
- 线程2:持有类B的锁,等待类A初始化
1.5.3. 类卸载条件
一个类可以被卸载的条件:
- 该类的所有实例都已被回收
- 加载该类的 ClassLoader 已被回收
- 该类对应的 java.lang.Class 对象没有被引用
1.6. 类加载机制的应用
- 热部署:通过自定义类加载器实现代码的热替换
- 模块化:OSGi 等框架利用类加载机制实现模块化
- 代码加密:自定义类加载器加载加密的类文件
- 沙箱安全:限制某些类的加载和访问
1.7. 类加载机制的性能优化
- 预加载:在应用启动时预先加载常用类
- 类缓存:缓存已加载的类避免重复加载
- 并行加载:利用多线程加速类加载过程
- 类共享:多个应用共享已加载的类(如 Tomcat 的共享类加载器)