理解JVM内存模型和GC机制是Java性能调优的基础,本文梳理JVM核心内存区域和主流垃圾回收器的工作原理。
JVM内存结构
JVM运行时数据区主要分为以下几块:
| 区域 | 线程共享 | 存储内容 |
|---|---|---|
| 堆 (Heap) | 是 | 对象实例、数组 |
| 方法区 (Method Area) | 是 | 类信息、常量池、静态变量 |
| 虚拟机栈 (VM Stack) | 否 | 栈帧:局部变量表、操作数栈、动态链接 |
| 本地方法栈 (Native Stack) | 否 | Native方法调用 |
| 程序计数器 (PC Register) | 否 | 当前线程执行的字节码行号 |
堆 是GC的主战场。JDK 8之后,方法区的实现从永久代(PermGen)改为元空间(Metaspace),Metaspace使用本地内存,不再受-XX:MaxPermSize限制,而是通过-XX:MaxMetaspaceSize控制上限。
堆的分代结构
+-------------------------------------------+
| 堆 Heap |
| +--------+ +---------+ +--------------+ |
| | Eden | | S0 | | Old | |
| | | | S1 | | (Tenured) | |
| +--------+ +---------+ +--------------+ |
| Young Generation Old Generation |
+-------------------------------------------+
- Young区:Eden + 两个Survivor(S0/S1),默认比例 8:1:1
- Old区:经过多次Young GC仍存活的对象晋升到此
- 大对象(超过
-XX:PretenureSizeThreshold)直接进入Old区
GC算法
标记-清除 (Mark-Sweep)
最基础的算法:先标记所有存活对象,再清除未被标记的对象。缺点是会产生内存碎片。
标记-整理 (Mark-Compact)
标记存活对象后,将存活对象向一端移动,然后清理边界外的内存。没有碎片问题,但移动对象开销较大。
复制算法 (Copying)
将内存分为两块,每次只使用其中一块,GC时将存活对象复制到另一块。Young区的Eden/Survivor就是基于此算法——Eden中存活对象复制到S0或S1,代价是浪费一个Survivor的空间。
分代收集
JVM根据对象存活周期采用不同策略:
- Minor GC (Young GC):只回收Young区。Eden满时触发,采用复制算法,速度快(通常10ms以内)
- Major GC / Full GC:回收整个堆(Young + Old)甚至方法区。Old区空间不足时触发,耗时较长
对象年龄:每经历一次Minor GC且存活,年龄+1。达到-XX:MaxTenuringThreshold(默认15)后晋升Old区。
主流垃圾回收器
CMS (Concurrent Mark Sweep)
目标是最短停顿时间,适合对响应延迟敏感的应用。
工作流程:
- 初始标记 (Initial Mark) —— STW,标记GC Roots直接关联的对象,很快
- 并发标记 (Concurrent Mark) —— 与用户线程并发,遍历对象图
- 重新标记 (Remark) —— STW,修正并发标记期间变动的引用
- 并发清除 (Concurrent Sweep) —— 与用户线程并发,清除垃圾对象
CMS使用标记-清除算法,存在碎片问题。当碎片过多无法分配大对象时,会触发Full GC并做压缩。
G1 (Garbage First)
JDK 9的默认收集器,将堆划分为多个等大的Region(默认2048个),每个Region可以是Eden、Survivor或Old。
核心思想:优先回收垃圾最多的Region(Garbage First),在可控的停顿时间内做尽可能多的回收。
关键特性:
- 通过
-XX:MaxGCPauseMillis设置目标停顿时间(默认200ms) - Mixed GC:可以同时回收Young和部分Old Region
- 使用Remembered Set解决跨Region引用问题
- 整体上是标记-整理算法,不会产生碎片
G1 vs CMS 选择
| 场景 | 推荐 |
|---|---|
| 堆 < 6GB,延迟敏感 | CMS |
| 堆 >= 6GB | G1 |
| JDK 9+ | G1(默认) |
| 需要可预测的停顿 | G1 |
常用JVM参数
内存设置:
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与Xms相同,避免动态扩缩)
-Xmn2g # Young区大小
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-Xss512k # 线程栈大小
GC选择:
-XX:+UseG1GC # 使用G1
-XX:MaxGCPauseMillis=200 # G1目标停顿
-XX:+UseConcMarkSweepGC # 使用CMS(JDK 8)
-XX:CMSInitiatingOccupancyFraction=75 # Old区使用75%时触发CMS
GC日志(JDK 8):
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/gc.log
GC日志(JDK 11+,统一日志框架):
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags
调优思路
- 先确定目标:吞吐量优先还是延迟优先?
- 观察GC日志:关注GC频率、每次停顿时间、Full GC次数
- 合理设置堆大小:太小则频繁GC,太大则单次GC时间长
- Young区比例:如果Minor GC后大量对象存活,说明Young区太小或对象存活时间较长
- 避免Full GC:Full GC的常见原因是Old区不足、Metaspace不足、显式调用
System.gc()
记住:调优不是背参数,而是根据GC日志和监控数据做针对性调整。