深入理解JVM类加载与垃圾回收机制
一、引言:为什么类加载与GC是Java核心?
Java虚拟机的类加载机制和垃圾回收机制是Java体系的基石,理解它们对于编写高性能应用、诊断线上问题以及通过技术面试都至关重要。据统计,在中高级Java面试中,类加载和GC相关问题的出现概率超过90%!本文将带你深入理解这两大核心机制,从基础到进阶,从理论到实战。
二、类加载机制深度解析
2.1 类加载全过程
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作类的加载。
// 示例:观察类加载过程
public class ClassLoadingDemo {static {System.out.println("主类初始化");}public static void main(String[] args) {System.out.println("开始执行main方法");// 主动引用示例new StaticFieldAccess();}
}class StaticFieldAccess {static {System.out.println("StaticFieldAccess类初始化");}static final String CONSTANT = "常量";static String staticField = "静态字段";
}
2.2 类加载的五个阶段
2.2.1 加载(Loading)
通过类的全限定名获取定义此类的二进制字节流
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成一个代表这个类的Class对象
2.2.2 验证(Verification)
文件格式验证:验证字节流是否符合Class文件格式规范
元数据验证:对类的元数据信息进行语义校验
字节码验证:通过数据流和控制流分析,确定程序语义是合法的
符号引用验证:发生在解析阶段,确保解析动作能正常执行
2.2.3 准备(Preparation)
// 准备阶段示例
public class PreparationPhase {// 准备阶段:value=0(零值)// 初始化阶段:value=123public static int value = 123;// 准备阶段:CONSTANT=123(直接赋值)public static final int CONSTANT = 123;
}
2.2.4 解析(Resolution)
将常量池内的符号引用替换为直接引用的过程
2.2.5 初始化(Initialization)
执行类构造器<clinit>()
方法的过程,真正开始执行类中定义的Java程序代码
2.3 类加载器体系
// 类加载器层次结构示例
public class ClassLoaderHierarchy {public static void main(String[] args) {// 获取系统类加载器ClassLoader systemLoader = ClassLoader.getSystemClassLoader();System.out.println("系统类加载器: " + systemLoader);// 获取扩展类加载器ClassLoader extLoader = systemLoader.getParent();System.out.println("扩展类加载器: " + extLoader);// 获取启动类加载器(通常为null,由C++实现)ClassLoader bootstrapLoader = extLoader.getParent();System.out.println("启动类加载器: " + bootstrapLoader);// 查看当前类的类加载器System.out.println("当前类加载器: " + ClassLoaderHierarchy.class.getClassLoader());}
}
2.4 双亲委派模型
工作原理:
当前类加载器首先检查请求的类是否已被加载
若未加载,委派给父类加载器去完成
只有当父类加载器反馈无法完成时,子加载器才会尝试加载
代码实现:
// 双亲委派模型代码体现(ClassLoader.loadClass方法)
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先检查类是否已加载Class<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {// 委托给父加载器c = parent.loadClass(name, false);} else {// 委托给启动类加载器c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父加载器无法完成加载}if (c == null) {// 自己尝试加载c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
}
三、垃圾回收机制全面剖析
3.1 对象存活判断算法
3.1.1 引用计数法(Reference Counting)
// 引用计数法的循环引用问题
public class ReferenceCountingGC {public Object instance = null;private static final int _1MB = 1024 * 1024;private byte[] bigSize = new byte[2 * _1MB];public static void main(String[] args) {ReferenceCountingGC objA = new ReferenceCountingGC();ReferenceCountingGC objB = new ReferenceCountingGC();// 相互引用objA.instance = objB;objB.instance = objA;// 无法被回收,但如果使用引用计数法则无法识别objA = null;objB = null;System.gc(); // 但Java使用可达性分析,可以正确回收}
}
3.1.2 可达性分析算法(Root Searching)
GC Roots包括:
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
Java虚拟机内部的引用
3.2 垃圾收集算法
3.2.1 标记-清除算法(Mark-Sweep)
优点:实现简单
缺点:产生内存碎片,分配大对象时效率低
3.2.2 标记-复制算法(Mark-Copy)
// 新生代Eden区和Survivor区的复制过程演示
public class CopyAlgorithmDemo {private static final int _1MB = 1024 * 1024;/*** VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails*/public static void main(String[] args) {byte[] allocation1 = new byte[2 * _1MB];byte[] allocation2 = new byte[2 * _1MB];byte[] allocation3 = new byte[2 * _1MB];byte[] allocation4 = new byte[4 * _1MB]; // 出现Minor GC}
}
3.2.3 标记-整理算法(Mark-Compact)
适合老年代,避免内存碎片问题
移动对象需要更新引用,效率相对较低
3.3 经典垃圾收集器
收集器 | 区域 | 算法 | 特点 | 适用场景 |
---|---|---|---|---|
Serial | 新生代 | 复制 | 单线程,Stop The World | 客户端模式 |
ParNew | 新生代 | 复制 | 多线程版Serial | 服务端模式 |
Parallel Scavenge | 新生代 | 复制 | 吞吐量优先 | 后台运算 |
Serial Old | 老年代 | 标记-整理 | Serial老年代版 | 客户端模式 |
Parallel Old | 老年代 | 标记-整理 | Parallel Scavenge老年代版 | 吞吐量优先 |
CMS | 老年代 | 标记-清除 | 低延迟,并发收集 | B/S系统 |
G1 | 全堆 | 标记-整理+复制 | 分区收集,可预测停顿 | 大内存服务 |
四、面试高频问题精讲
Q1:什么是类加载的双亲委派模型?有什么好处?
答:双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
好处:
安全性:防止核心API被篡改
避免重复加载:保证类的唯一性
结构清晰:类加载器层次关系明确
Q2:什么情况下会触发类的初始化?
答:
遇到new、getstatic、putstatic、invokestatic字节码指令
使用反射对类进行反射调用时
初始化一个类时发现其父类还没有初始化
虚拟机启动时指定的主类
使用JDK7动态语言支持时的方法句柄
Q3:如何判断一个对象是否可以回收?
答:Java使用可达性分析算法。从GC Roots对象作为起点,向下搜索引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Q4:CMS和G1垃圾收集器的区别?
答:
特性 | CMS | G1 |
---|---|---|
区域划分 | 新生代+老年代 | 将堆划分为多个Region |
算法 | 标记-清除 | 整体标记-整理,局部复制 |
停顿时间 | 较短但不可预测 | 可预测的停顿时间模型 |
内存碎片 | 会产生 | 整体上避免了碎片问题 |
适用场景 | 响应速度要求高的系统 | 大内存、多处理器系统 |