JVM深入理解¶
🎯 学习目标¶
完成本章学习后,你将能够: - 深入理解JVM体系结构与各组件职责 - 掌握类加载机制与双亲委派模型 - 理解JVM内存模型与各区域特点 - 掌握主流垃圾回收算法与收集器的原理及适用场景 - 具备GC调优与内存泄漏排查的实战能力 - 理解JIT编译优化策略
1. JVM体系结构总览¶
┌─────────────────────────────────────────────────────┐
│ Java源代码(.java) │
│ ↓ javac编译 │
│ 字节码文件(.class) │
└──────────────────────────┬──────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ JVM │
│ ┌──────────────────────────────────────────────┐ │
│ │ 类加载子系统 (Class Loader) │ │
│ │ Bootstrap → Extension → Application │ │
│ └──────────────────┬───────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 运行时数据区 (Runtime Data Areas) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────────────┐ │ │
│ │ │程序计数器│ │虚拟机栈 │ │ 本地方法栈 │ │ │
│ │ │(线程私有)│ │(线程私有)│ │ (线程私有) │ │ │
│ │ └─────────┘ └─────────┘ └───────────────┘ │ │
│ │ ┌────────────────────┐ ┌────────────────┐ │ │
│ │ │ 堆 Heap │ │ 方法区/元空间 │ │ │
│ │ │ (线程共享) │ │ (线程共享) │ │ │
│ │ └────────────────────┘ └────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 执行引擎 (Execution Engine) │ │
│ │ 解释器 + JIT编译器 + 垃圾收集器(GC) │ │
│ └──────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 本地方法接口 (JNI) │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
2. 类加载机制¶
2.1 类的生命周期¶
加载(Loading) → 验证(Verification) → 准备(Preparation) → 解析(Resolution)
→ 初始化(Initialization) → 使用(Using) → 卸载(Unloading)
↑______ 连接(Linking) ______↑
- 加载:读取
.class文件字节流,创建java.lang.Class对象 - 验证:检查字节码是否合法(文件格式、元数据、字节码、符号引用验证)
- 准备:为类变量(static)分配内存并赋零值(注意:
static final常量在此阶段赋实际值) - 解析:将常量池中符号引用替换为直接引用
- 初始化:执行
<clinit>()方法(类变量赋值 + static代码块)
2.2 双亲委派模型¶
// 类加载器层级
// Bootstrap ClassLoader ← 加载 jre/lib 核心类库(C++实现,Java中表现为null)
// ↑
// Platform ClassLoader ← 加载平台模块类库(Java 9+,替代原Extension ClassLoader)
// ↑
// Application ClassLoader ← 加载 classpath 下的用户类(默认类加载器)
// ↑
// Custom ClassLoader ← 用户自定义
// 双亲委派流程:
// 1. 收到类加载请求 → 先委派给父加载器
// 2. 父加载器无法加载 → 才由自身尝试加载
// 优点:保证核心类安全(防止用户篡改java.lang.String等)
// ClassLoader.loadClass() 源码核心逻辑
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载
}
if (c == null) {
// 3. 自身尝试加载
c = findClass(name);
}
}
return c;
}
}
2.3 自定义类加载器¶
import java.io.*;
import java.nio.file.*;
/**
* 自定义类加载器 — 加载指定目录下的.class文件
* 典型应用:热部署、加密类加载、模块隔离(如Tomcat)
*/
public class CustomClassLoader extends ClassLoader {
private final String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String fileName = classPath + File.separator
+ name.replace('.', File.separatorChar) + ".class";
byte[] data = Files.readAllBytes(Path.of(fileName));
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException("无法加载类: " + name, e);
}
}
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader("/opt/classes");
Class<?> clazz = loader.loadClass("com.example.MyPlugin");
Object instance = clazz.getDeclaredConstructor().newInstance();
System.out.println("类加载器: " + clazz.getClassLoader());
// 输出: 类加载器: com.example.CustomClassLoader@...
}
}
打破双亲委派的场景: - SPI机制(JDBC、JNDI):Bootstrap加载接口,Application加载实现 → 使用Thread Context ClassLoader - Tomcat:每个Web应用独立ClassLoader,优先加载Web应用自身类 - OSGi:网状类加载结构,按Bundle组织
3. 运行时数据区详解¶
3.1 线程私有区域¶
// ==================== 程序计数器 (PC Register) ====================
// - 当前线程执行的字节码指令行号指示器
// - 唯一不会发生OOM的区域
// - 执行Java方法时记录字节码地址,执行Native方法时为Undefined
// ==================== 虚拟机栈 (JVM Stack) ====================
// - 每个方法调用创建一个栈帧(Stack Frame)
// - 栈帧包含:局部变量表、操作数栈、动态链接、方法返回地址
// - -Xss 设置栈大小(默认512K~1M)
// - 异常:StackOverflowError(栈深度超限)、OutOfMemoryError(无法申请栈空间)
public class StackDemo {
// 触发 StackOverflowError
static int depth = 0;
public static void recursiveCall() {
depth++;
recursiveCall(); // 无限递归
}
public static void main(String[] args) {
try {
recursiveCall();
} catch (StackOverflowError e) {
System.out.println("栈溢出!递归深度: " + depth);
// 默认栈大小约可递归 5000-20000 层
}
}
}
// ==================== 本地方法栈 (Native Method Stack) ====================
// - 为Native方法(JNI调用)服务
// - HotSpot中与虚拟机栈合并实现
3.2 线程共享区域¶
// ==================== 堆 (Heap) ====================
// - 存放所有对象实例和数组
// - GC主要管理区域("GC堆")
// - -Xms 初始堆大小,-Xmx 最大堆大小(建议设为相同值,避免扩缩容开销)
// - 逻辑分代:新生代(Young) + 老年代(Old)
// 堆内存结构(以G1之前的经典分代为例)
//
// 新生代 (Young Generation) — -Xmn 或 -XX:NewRatio
// ┌────────────────────┬──────┬──────┐
// │ Eden (80%) │ S0 │ S1 │ — 默认 Eden:S0:S1 = 8:1:1
// │ 新对象分配于此 │(10%) │(10%) │ — -XX:SurvivorRatio=8
// └────────────────────┴──────┴──────┘
//
// 老年代 (Old Generation)
// ┌──────────────────────────────────┐
// │ 长期存活对象、大对象 │
// │ -XX:MaxTenuringThreshold=15 │ — 晋升年龄(默认15)
// └──────────────────────────────────┘
// ==================== 方法区 / 元空间 (Metaspace) ====================
// - 存储类信息、常量、静态变量、JIT编译后的代码
// - JDK7及之前:PermGen(永久代),受-XX:MaxPermSize限制
// - JDK8+:Metaspace(元空间),使用本地内存,-XX:MaxMetaspaceSize
// - 运行时常量池是方法区的一部分
// TLAB (Thread Local Allocation Buffer)
// - 每个线程在Eden区预分配一小块私有缓冲区
// - 避免多线程分配对象时的锁竞争
// - -XX:+UseTLAB(默认开启)
4. 垃圾回收¶
4.1 对象存活判断¶
// 方式一:引用计数法(Python使用,Java不用)
// 缺点:无法解决循环引用
// 方式二:可达性分析(Java采用)
// 从GC Roots出发,沿引用链搜索,不可达的对象判定为垃圾
// GC Roots包括:
// 1. 虚拟机栈中引用的对象(局部变量)
// 2. 方法区中静态属性引用的对象
// 3. 方法区中常量引用的对象
// 4. 本地方法栈中Native方法引用的对象
// 5. 同步锁持有的对象
// 6. JVM内部引用(基本类型包装类、系统类加载器等)
// 引用类型强度:强引用 > 软引用 > 弱引用 > 虚引用
Object strong = new Object(); // 强引用:GC绝不回收
SoftReference<Object> soft = new SoftReference<>(new Object()); // 软引用:内存不足时回收
WeakReference<Object> weak = new WeakReference<>(new Object()); // 弱引用:下次GC即回收
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue); // 虚引用:配合ReferenceQueue
4.2 垃圾回收算法¶
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 标记存活对象→清除未标记对象 | 简单 | 内存碎片 | CMS老年代 |
| 标记-复制 | 将存活对象复制到另一半空间 | 无碎片、高效 | 空间浪费一半 | 新生代(Eden/S0/S1) |
| 标记-整理 | 标记存活→向一端移动压缩 | 无碎片 | 移动对象开销 | 老年代 |
| 分代收集 | 新生代用复制、老年代用标记-整理/清除 | 综合最优 | 实现复杂 | HotSpot默认 |
4.3 GC类型¶
- Minor GC / Young GC:新生代GC,频繁但速度快
- Major GC / Old GC:老年代GC,通常伴随Full GC
- Full GC:整堆GC(新生代 + 老年代 + 元空间),STW时间最长
- Mixed GC:G1独有,回收整个新生代 + 部分老年代
4.4 对象分配与晋升¶
新对象 → Eden区(TLAB快速分配)
↓ Minor GC触发(Eden满)
存活对象 → S0/S1(年龄+1)
↓ 每次Minor GC在S0/S1之间来回复制
年龄 ≥ 15 → 晋升Old区(-XX:MaxTenuringThreshold)
大对象 → 直接进入Old区(-XX:PretenureSizeThreshold)
动态年龄判定 → 相同年龄对象总大小 > S区一半 → 该年龄及以上晋升
5. 垃圾收集器详解¶
5.1 收集器对比¶
| 收集器 | 区域 | 算法 | 线程 | STW | 特点 |
|---|---|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程 | 是 | 简单高效,Client模式默认 |
| ParNew | 新生代 | 复制 | 多线程 | 是 | Serial多线程版本,配合CMS |
| Parallel Scavenge | 新生代 | 复制 | 多线程 | 是 | 吞吐量优先,JDK8默认搭档 |
| Serial Old | 老年代 | 标记-整理 | 单线程 | 是 | Serial老年代版本 |
| Parallel Old | 老年代 | 标记-整理 | 多线程 | 是 | JDK8默认(+Parallel Scavenge) |
| CMS | 老年代 | 标记-清除 | 并发 | 短暂 | 低延迟,JDK9废弃,JDK14移除 |
| G1 | 整堆 | 分区+复制+整理 | 并发 | 可控 | JDK9+默认,平衡吞吐与延迟 |
| ZGC | 整堆 | 染色指针+读屏障 | 并发 | <1ms | 超低延迟,JDK15+生产可用 |
| Shenandoah | 整堆 | 转发指针+读屏障 | 并发 | <10ms | OpenJDK,与ZGC竞争 |
5.2 G1收集器深入¶
// G1 (Garbage-First) — JDK9+默认收集器
// 核心思想:将堆划分为大小相等的Region(1~32MB),按回收价值排序优先回收
// Region类型:
// Eden Region — 新对象分配
// Survivor Region — 新生代存活对象
// Old Region — 老年代对象
// Humongous Region — 大对象(> Region 50%)
// G1回收过程:
// 1. 初始标记(Initial Mark)— STW,标记GC Roots直接关联的对象
// 2. 并发标记(Concurrent Mark)— 与用户线程并发,遍历引用链
// 3. 最终标记(Final Mark)— STW,处理SATB(Snapshot-At-The-Beginning)遗留记录
// 4. 筛选回收(Cleanup/Evacuation)— STW,按Region回收价值排序,复制存活对象
// JVM参数
// -XX:+UseG1GC 开启G1(JDK9+默认)
// -XX:MaxGCPauseMillis=200 目标最大停顿时间(默认200ms)
// -XX:G1HeapRegionSize=4m Region大小(1~32MB,须为2的幂)
// -XX:G1NewSizePercent=5 新生代最小比例
// -XX:G1MaxNewSizePercent=60 新生代最大比例
// -XX:InitiatingHeapOccupancyPercent=45 触发Mixed GC的老年代占比阈值
5.3 ZGC(面试加分项)¶
// ZGC — 超低延迟收集器(STW < 1ms,不随堆大小增长)
// 核心技术:
// 1. 染色指针(Colored Pointers)— 在指针中存储GC元数据(标记、重映射)
// 2. 读屏障(Load Barrier)— 读取对象引用时检查指针状态
// 3. 并发整理 — 几乎所有阶段与用户线程并发
// 适用场景:
// - 超大堆(TB级别) + 要求极低延迟(金融、游戏)
// - JDK15+ 生产可用
// 启用ZGC
// -XX:+UseZGC
// -XX:+ZGenerational 分代ZGC(JDK21+,默认开启)
// -Xmx16g 设置最大堆
6. GC调优实战¶
6.1 常用JVM参数¶
# 堆内存
-Xms4g -Xmx4g # 初始/最大堆(建议设相同值)
-Xmn2g # 新生代大小
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大大小
# GC选择
-XX:+UseG1GC # 使用G1
-XX:MaxGCPauseMillis=100 # G1目标停顿时间
# GC日志(JDK9+统一日志格式)
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100m
# 线程栈
-Xss512k # 每个线程栈大小
# OOM时自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
6.2 GC日志分析¶
// 示例GC日志(G1)
// [0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
// Eden: 24M(24M)->0M(24M) Survivors: 0M->4M Heap: 24M(256M)->5M(256M)
// [0.123s][info][gc] GC(0) Pause Young (Normal) 24M->5M(256M) 3.456ms
// 关键指标:
// 1. GC频率(Young GC几秒一次算正常,Full GC应极少出现)
// 2. GC耗时(单次停顿几十ms内,累计占比<5%)
// 3. 内存回收效率(每次GC回收比例)
// 4. 堆使用趋势(是否持续增长 → 可能内存泄漏)
6.3 实用诊断工具¶
# ===== JDK自带工具 =====
# jps — 查看Java进程
jps -lv
# jstat — GC统计(每1秒打印一次,共10次)
jstat -gcutil <pid> 1000 10
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 50.2 45.3 32.1 95.0 93.2 120 1.23 2 0.45 1.68
# jmap — 堆信息 / 生成堆转储
jmap -heap <pid> # 查看堆使用概况
jmap -histo:live <pid> | head -20 # 对象统计(触发Full GC)
jmap -dump:format=b,file=heap.hprof <pid> # 生成堆转储
# jstack — 线程快照(排查死锁、线程阻塞)
jstack <pid> > thread_dump.txt
jstack -l <pid> # 包含锁信息
# ===== Arthas(阿里开源,强烈推荐) =====
# 下载: curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动: java -jar arthas-boot.jar
# dashboard — 实时面板(CPU、内存、GC、线程等)
# thread -n 3 — CPU占用最高的3个线程
# thread -b — 查找阻塞线程
# heapdump /tmp/dump.hprof — 生成堆转储
# sc -d com.example.MyClass — 查看类信息
# watch com.example.Service method '{params, returnObj}' — 观察方法调用
# trace com.example.Service method — 方法调用链耗时
# jad com.example.MyClass — 反编译
7. 内存泄漏排查¶
7.1 常见OOM类型¶
// 1. java.lang.OutOfMemoryError: Java heap space
// 原因:堆内存不足(对象过多或内存泄漏)
// 解决:增大-Xmx,排查泄漏
// 2. java.lang.OutOfMemoryError: Metaspace
// 原因:加载的类过多(如动态代理、CGLIB大量生成类)
// 解决:增大-XX:MaxMetaspaceSize
// 3. java.lang.OutOfMemoryError: GC overhead limit exceeded
// 原因:98%以上时间在GC,回收不到2%内存
// 解决:排查内存泄漏
// 4. java.lang.StackOverflowError
// 原因:栈深度超限(通常是无限递归)
// 解决:检查递归终止条件,增大-Xss
// 5. java.lang.OutOfMemoryError: Direct buffer memory
// 原因:NIO DirectByteBuffer溢出
// 解决:增大-XX:MaxDirectMemorySize
7.2 内存泄漏排查流程¶
// 常见内存泄漏场景
public class MemoryLeakExamples {
// 场景1:静态集合持续增长
private static final List<byte[]> cache = new ArrayList<>();
public void addToCache(byte[] data) {
cache.add(data); // 永远不清理 → 泄漏
}
// 场景2:未关闭的资源
public void readFile(String path) throws Exception {
InputStream is = new FileInputStream(path);
// 忘记 is.close() → 资源泄漏
// 正确做法:try-with-resources
}
// 场景3:监听器/回调未注销
public void register() {
EventBus.register(this); // 注册后从不unregister
}
// 场景4:内部类持有外部类引用
public class InnerTask implements Runnable {
// 非静态内部类隐式持有外部类引用
// 如果InnerTask被线程池持有 → 外部类无法被GC
@Override
public void run() { }
}
}
// 排查流程:
// 1. 观察 → jstat -gcutil <pid> 查看Old区持续增长
// 2. 抓堆 → jmap -dump:format=b,file=heap.hprof <pid>
// 3. 分析 → 使用Eclipse MAT或VisualVM打开hprof
// - Leak Suspects Report: 自动检测可疑泄漏点
// - Dominator Tree: 查看对象占用排名
// - GC Roots → 追踪引用链,定位是谁持有了不该持有的引用
// 4. 修复 → 清理引用、关闭资源、使用弱引用等
8. JIT编译优化¶
// HotSpot采用"解释执行 + JIT编译"混合模式
// 热点代码(方法调用计数器 / 回边计数器超过阈值)触发JIT编译为本地机器码
// ==================== 逃逸分析 ====================
// 分析对象是否逃逸到方法/线程外部
// 不逃逸的对象可进行以下优化:
// 1. 栈上分配:对象不逃逸 → 在栈帧中分配,方法结束自动回收,不进入堆
public void process() {
Point p = new Point(1, 2); // p不逃逸 → 可能在栈上分配
int sum = p.x + p.y;
System.out.println(sum);
}
// 2. 标量替换:对象拆解为基本类型
// Point p = new Point(1, 2); → 替换为 int x = 1; int y = 2;
// 3. 锁消除:对象不逃逸 → 去除无意义的同步
public String concat(String a, String b) {
StringBuffer sb = new StringBuffer(); // sb不逃逸
sb.append(a);
sb.append(b);
return sb.toString();
// JIT可能消除StringBuffer内部的synchronized
}
// ==================== 方法内联 ====================
// 将方法调用替换为方法体本身,减少调用开销
// -XX:MaxInlineSize=35 — 方法字节码 ≤ 35字节自动内联
// -XX:FreqInlineSize=325 — 热点方法字节码 ≤ 325字节自动内联
// ==================== 循环优化 ====================
// 循环展开、循环判断外提等
// JIT相关参数
// -XX:-TieredCompilation 关闭分层编译
// -XX:CompileThreshold=10000 方法调用计数阈值
// -XX:+PrintCompilation 打印JIT编译信息
✏️ 练习¶
- 基础:编写程序触发
StackOverflowError和OutOfMemoryError: Java heap space,并记录错误信息 - 中级:使用
jstat -gcutil监控一个持续创建对象的程序,观察各代内存变化和GC频率 - 高级:编写一个存在内存泄漏的程序(静态集合 + 不断添加大对象),使用
jmap生成堆转储,用MAT分析泄漏原因并修复
📋 经典JVM面试题¶
1. JVM内存结构说一下?哪些是线程私有的?¶
答:程序计数器、虚拟机栈、本地方法栈(线程私有);堆、方法区/元空间(线程共享)。堆存放对象实例,方法区存放类信息。JDK8后永久代被元空间替代,使用本地内存。
2. 什么是双亲委派模型?为什么需要?如何打破?¶
答:类加载请求先委派给父加载器,父无法加载再由子加载器处理。保证核心类安全性和唯一性。打破方式:重写loadClass()而非findClass(),如Tomcat(Web应用隔离)、SPI(TCCL)、OSGi(网状加载)。
3. 如何判断对象可以被回收?¶
答:可达性分析——从GC Roots出发,不可达的对象判定为垃圾。GC Roots包括栈帧中的引用、静态变量、同步锁持有的对象等。回收前还有finalize()自救机会(只执行一次,不推荐使用)。
4. 垃圾回收算法及其优缺点?¶
答:①标记-清除(简单,有碎片)②标记-复制(无碎片,浪费空间,适合新生代)③标记-整理(无碎片,移动开销大,适合老年代)④分代收集(综合以上,HotSpot实际采用)。
5. G1和CMS的区别?¶
答:CMS基于标记-清除,目标是最短停顿,会产生碎片;G1基于Region分区+复制整理,可预测停顿时间(-XX:MaxGCPauseMillis),无碎片。G1是JDK9+默认,CMS在JDK14被移除。
6. Full GC触发条件?¶
答:①老年代空间不足②元空间不足③System.gc()调用④CMS并发失败(Concurrent Mode Failure)⑤晋升担保失败(老年代连续空间不足以容纳新生代晋升对象)。
7. 如何排查线上OOM?¶
答:①提前配置-XX:+HeapDumpOnOutOfMemoryError②分析GC日志(jstat/GC log)③获取堆转储(jmap或自动dump)④MAT分析(Dominator Tree找大对象 → GC Roots追踪引用链)⑤定位代码修复⑥Arthas在线诊断(dashboard/thread/heapdump)。
8. JVM调优的一般流程?¶
答:①明确目标(低延迟/高吞吐)②选择合适的GC(G1/ZGC)③设定初始参数④压测 + GC日志分析⑤调整参数(堆大小、Region、停顿目标)⑥反复迭代直至达标。
9. 强引用、软引用、弱引用、虚引用的区别?¶
答:强引用不回收;软引用内存不足时回收(适合缓存);弱引用下次GC即回收(WeakHashMap);虚引用无法获取对象,仅用于回收通知(配合ReferenceQueue)。
10. 什么是逃逸分析?¶
答:JIT编译器分析对象是否逃逸出方法或线程。不逃逸可优化为:栈上分配(避免GC)、标量替换(拆解为基本类型)、锁消除(去除无意义同步)。-XX:+DoEscapeAnalysis(JDK6u23+默认开启)。
📌 下一步学习:14-Java新特性(8-21) — 掌握现代Java语法与API