Java JVM调优:内存模型与GC

理解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)

目标是最短停顿时间,适合对响应延迟敏感的应用。

工作流程:

  1. 初始标记 (Initial Mark) —— STW,标记GC Roots直接关联的对象,很快
  2. 并发标记 (Concurrent Mark) —— 与用户线程并发,遍历对象图
  3. 重新标记 (Remark) —— STW,修正并发标记期间变动的引用
  4. 并发清除 (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

调优思路

  1. 先确定目标:吞吐量优先还是延迟优先?
  2. 观察GC日志:关注GC频率、每次停顿时间、Full GC次数
  3. 合理设置堆大小:太小则频繁GC,太大则单次GC时间长
  4. Young区比例:如果Minor GC后大量对象存活,说明Young区太小或对象存活时间较长
  5. 避免Full GC:Full GC的常见原因是Old区不足、Metaspace不足、显式调用System.gc()

记住:调优不是背参数,而是根据GC日志和监控数据做针对性调整。