JVM 原理与调优基础
JVM 原理与调优基础
理解 JVM(Java Virtual Machine)是成为高级 Java 开发者的必经之路。JVM 管理内存、加载类、执行字节码,是 Java “一次编写,到处运行”的核心。本文深入讲解 JVM 内存结构、类加载机制、垃圾回收算法以及常用调优工具。
JVM 内存结构
JVM 运行时数据区分为以下几个部分:
┌─────────────────────────────────────────────┐
│ JVM 进程 │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ PC Register│ │ VM Stack │ │ Native Stack │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ 线程私有 │
│ ┌─────────────────────────────────────┐ │
│ │ Heap(堆)线程共享 │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │ Eden │ S0 │ S1 │ │ Old Gen │ │ │
│ │ │ 伊甸区│ Survivor │ │ 老年代 │ │ │
│ │ └────────┘ └────────┘ │ │
│ └─────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ Metaspace(元数据区)线程共享 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘各区域职责
线程私有区域(每个线程独享):
- PC Register(程序计数器):记录当前线程正在执行的字节码指令地址
- VM Stack(虚拟机栈):每个线程的方法调用栈,存放栈帧,栈帧包含局部变量表、操作数栈、方法返回地址
- Native Stack:用于本地方法(C/C++)调用
线程共享区域:
- Heap(堆):存放对象实例和数组,是 GC 的主要管理区域
- Metaspace(元数据区):Java 8+ 替代 PermGen(永久代),存放类的元信息(Java 17+ 使用 ZGC 后默认无限大)
栈帧结构
public class StackDemo {
public int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归调用
}
}
// 每次递归调用 factorial() 都会在栈上压入一个新栈帧
// 递归深度过大会导致 StackOverflowError// 栈溢出示例
public class StackOverflow {
public static void recurse() {
recurse(); // 无限递归 → StackOverflowError
}
public static void main(String[] args) {
// 默认栈大小约 1MB(可通过 -Xss 参数调整)
// 如果方法调用链路深(如深层递归),栈会溢出
recurse();
}
}堆内存分配
public class HeapDemo {
public static void main(String[] args) {
// -Xms256m -Xmx512m:最小堆 256MB,最大堆 512MB
// 对象在堆上分配
Object obj = new Object(); // obj 引用在栈上,对象在堆上
// 数组也是对象
int[] arr = new int[1_000_000]; // 约 4MB 的堆内存
// 大对象直接进入老年代(通过 -XX:PretenureSizeThreshold 设置阈值)
byte[] large = new byte[100 * 1024 * 1024]; // 100MB 的大数组
// 查看 GC 日志,了解对象分配情况
// java -Xms256m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ...
}
}类加载机制
Java 程序启动时,JVM 通过类加载器将 .class 文件加载到内存中,并执行初始化。
双亲委派模型
类加载器按层级关系工作,先向上委托,再向下加载:
启动类加载器(Bootstrap ClassLoader)
└── 扩展类加载器(Extension ClassLoader)
└── 应用类加载器(Application ClassLoader)
└── 自定义类加载器(User ClassLoader)public class ClassLoaderDemo {
public static void main(String[] args) {
// 查看类的加载器
ClassLoader cl = String.class.getClassLoader();
System.out.println(cl); // null → Bootstrap ClassLoader(Java 核心类)
ClassLoader cl2 = ClassLoaderDemo.class.getClassLoader();
System.out.println(cl2); // AppClassLoader(应用类加载器)
System.out.println(ClassLoaderDemo.class.getClassLoader().getParent()); // PlatformClassLoader
}
}类加载过程
public class ClassLoadingProcess {
// 1. 加载(Loading):读取 .class 文件,生成 Class 对象
// 2. 链接(Linking)
// - 验证(Verification):检查字节码格式是否正确
// - 准备(Preparation):为静态变量分配内存,赋默认值(0/null)
// - 解析(Resolution):将符号引用替换为直接引用
// 3. 初始化(Initialization):执行静态代码块,给静态变量赋初值
static int value = 10; // 准备阶段=0,初始化阶段=10
static {
value = 20; // 静态代码块在初始化阶段执行
}
public static void main(String[] args) {
System.out.println(value); // 20
}
}垃圾回收(GC)
GC 是 JVM 最重要的自动内存管理机制——自动回收不再使用的对象,防止内存泄漏。
如何判断对象可回收
引用计数法(有缺陷,无法处理循环引用):
// 引用计数:对象每多一个引用,计数器+1;引用失效-1
// 问题:objA → objB → objA 循环引用时,两个对象计数器都是1,无法回收可达性分析算法(GC Roots)(主流):
从一组根对象(GC Roots)出发,通过引用链遍历所有可达对象,其余对象即为可回收垃圾:
// GC Roots 包括:
// - 虚拟机栈(栈帧中的本地变量表)中引用的对象
// - 方法区中类静态属性引用的对象
// - 方法区中常量引用的对象
// - 本地方法栈中 JNI 引用的对象
// - 虚拟机内部的引用(ClassLoader、异常对象等)
public class GCrootsDemo {
static class A { B b; } // A 持有 B 的引用
static class B { A a; } // B 持有 A 的引用,形成循环引用
public static void main(String[] args) {
A a = new A(); // a 是 GC Root
B b = new B(); // b 是 GC Root
a.b = b;
b.a = a;
a = null; // a 引用断开
b = null; // b 引用断开
// 此时 A 和 B 循环引用,但没有任何 GC Root 可达
// → 可被 GC 回收
}
}分代回收理论
不同生命周期的对象适合不同的回收策略:
┌─────────────────────────────────────────┐
│ Heap │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Young Gen │ │ Old Gen │ │
│ │ │ │ │ │
│ │ ┌───┐ ┌───┐ │ │ │ │
│ │ │Eden│S0│S1│ │ │ 大对象/长寿命 │ │
│ │ └───┘ └───┘ │ │ │ │
│ │ 8:1:1 │ │ │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ Minor GC(频繁) Major/Full GC │
│ 回收 Young Gen 回收 Old Gen + Metaspace│
└─────────────────────────────────────────┘垃圾回收算法
标记-清除(Mark-Sweep):先标记所有存活对象,再清除垃圾。缺点是产生内存碎片。
复制(Copying):将内存分为两半,每次只用一半;存活对象复制到另一半,然后清空当前半。缺点是只能使用一半内存。
标记-整理(Mark-Compact):标记存活对象,将它们移向一端,然后清理边界外的内存。解决碎片问题,但移动成本高。
主流 GC 收集器
| 收集器 | 算法 | 适用场景 | 特点 |
|---|---|---|---|
| Serial GC | 复制(Young)+ 标记整理(Old) | 单核/客户端 | 简单高效,Stop The World |
| Parallel GC(吞吐量优先) | 复制(Young)+ 标记整理(Old) | 多核/批处理 | 吞吐量最高,Stop The World |
| CMS GC | 初始标记→并发标记→重新标记→并发清除 | 低延迟需求 | 已废弃,Java 14 移除 |
| G1 GC(默认) | 标记整理 | 服务端(Java 9+默认) | 可预测延迟,区域化回收 |
| ZGC | 着色指针 + 读屏障 | 超大堆(TB级) | 亚毫秒级停顿 |
| Shenandoah GC | 着色指针 + 读屏障 | 低延迟 | 非 JDK 官方(Red Hat) |
G1 GC 详解
G1(Garbage First) 是 Java 9+ 的默认 GC,将堆划分为多个大小相等的 Region,并发执行回收:
# G1 GC 调优参数
java -Xms4g -Xmx4g \ # 堆大小 4GB
-XX:MaxGCPauseMillis=200 \ # 目标最大停顿时间 200ms
-XX:+UseG1GC \ # 使用 G1 GC
-XX:G1HeapRegionSize=4m \ # Region 大小(1/2/4/8/16/32 MB)
-XX:InitiatingHeapOccupancyPercent=45 \ # 堆占用 45% 时触发 Mixed GC
-XX:G1NewSizePercent=5 \ # Young Gen 最小比例 5%
-XX:G1MaxNewSizePercent=60 \ # Young Gen 最大比例 60%
-XX:+PrintGCDetails \ # 打印详细 GC 日志
-XX:+PrintGCDateStamps \ # 打印时间戳
-Xlog:gc*:file=gc.log \ # 输出到文件
MyAppZGC:亚毫秒级停顿
ZGC 是 Java 11+ 实验特性、Java 15+ 正式版,支持 TB 级堆内存的同时,将停顿时间控制在亚毫秒级:
# ZGC 调优参数
java -Xms16g -Xmx16g \
-XX:+UseZGC \
-XX:MaxGCPauseMillis=1 \ # 目标最大停顿 1ms
-XX:+ZCollectionInterval=60 \ # 每 60 秒触发一次 GC
MyApp// ZGC 适合的场景:
// - 堆内存 8GB 以上
// - 对停顿时间敏感(如 Web 应用、交易系统)
// - 吞吐量要求不是极端高
// ZGC 的代价:
// - 吞吐量略低于 G1(约 5-15%)
// - 更高的内存开销(着色指针占用)
// - 不支持类卸载(Java 21+ 已支持)常用调优参数
# 内存参数
java -Xms256m -Xmx256m # 固定堆大小(生产环境推荐:-Xms=-Xmx)
java -Xss2m # 每个线程栈大小(默认 1MB)
java -XX:MetaspaceSize=256m # 元数据区初始大小
java -XX:MaxMetaspaceSize=512m # 元数据区最大大小
# GC 参数
java -XX:+UseG1GC # 使用 G1 GC
java -XX:+UseZGC # 使用 ZGC(Java 15+)
java -XX:MaxGCPauseMillis=100 # 最大 GC 停顿目标(仅 G1)
java -XX:+PrintGC # 打印 GC 概要信息
java -XX:+PrintGCDetails # 打印详细 GC 信息
# OOM 时导出堆转储
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/heap.hprof \
MyApp诊断工具
jstat:GC 统计
# 每 1000ms 输出一次 GC 统计,共输出 10 次
jstat -gcutil <pid> 1000 10
# 输出:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 12.45 45.23 67.89 94.12 91.23 123 5.678 12 2.345 8.023
# 字段说明:
# S0/S1:Survivor 区使用率
# E:Eden 区使用率(高且频繁 → Minor GC 频繁)
# O:Old 区使用率(高且上升 → 对象晋升过快)
# YGC:Young GC 次数(过多 → Minor GC 太频繁)
# FGCT:Full GC 总耗时(过长 → Old 区回收成本高)Arthas:线上问题诊断
Arthas(阿尔萨斯)是阿里巴巴开源的 Java 诊断工具,无需重启 JVM 即可实时诊断:
# 启动 Arthas
java -jar arthas-boot.jar
# 常用命令
dashboard # 查看 JVM 概览(线程、内存、GC)
thread # 查看所有线程状态
thread <n> # 查看指定线程的堆栈
thread -b # 查找阻塞其他线程的线程
heapdump /tmp/heap.hprof # 导出堆转储(排查 OOM)
# 监控方法调用
watch com.example.UserService login '{params, returnObj}' -x 3
# 反编译类
jad com.example.UserService
# 查看方法耗时
profiler start --event cpu
profiler stop --format htmljmap:堆内存分析
# 查看堆摘要
jmap -heap <pid>
# 导出堆转储(用于 MAT/JProfiler 分析)
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
# 统计类实例数量(有助于发现内存泄漏)
jmap -histo <pid> | head -30小结
- JVM 内存分为线程私有(PC Register / VM Stack / Native Stack)和线程共享(Heap / Metaspace)区域
- 堆是 GC 的主要管理区域,采用分代回收策略(Young Gen → Old Gen)
- 类加载遵循双亲委派模型,避免类的重复加载和核心类的篡改
- G1 GC 是 Java 9+ 默认收集器,适合大多数服务端场景;ZGC 适合超大堆和超低延迟需求
jstat、Arthas、jmap是日常诊断的三大工具- GC 调优的核心思路:先观察,后调整——通过 GC 日志定位瓶颈,再针对性调整参数
JVM 是 Java 后端工程的核心基础设施。掌握本文内容后,你对 Java 程序的运行机制将有一个系统性的认识,能够更好地进行性能分析和问题排查。
至此,Java 完整学习路线已全部覆盖——从基础语法到高级主题,期待你将这些知识运用到实际项目中!
评论
Written by
AI-Writer
Related Articles
Java 17+ 新特性精讲
全面讲解 Java 17 LTS 的核心新特性:Records、Sealed Classes、Pattern Matching for instanceof、Text Blocks、Switch Expressions,附详细代码示例
Read MoreJVM 原理与调优基础
深入讲解 JVM 内存结构(堆/栈/方法区)、类加载机制、垃圾回收算法(GC),以及 Java 17+ G1GC、ZGC、常用调优参数和诊断工具
Read More