GC 原理
1. GC 日志与常用参数
在分析 GC 行为时,最直观的方法是查看 GC 日志。常见的参数组合如下:
bash复制编辑-verbose:gc
-XX:HeapDumpPath=.
-Xloggc:gc.log
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+UseGCLogFileRotation
-XX:+DisableExplicitGC
-XX:+PrintTenuringDistribution
-verbose:gc:打印简要的 GC 信息
-Xloggc:gc.log:指定 GC 日志输出文件路径
-XX:+PrintGCDetails:打印详细的 GC 信息
-XX:+PrintGCDateStamps:在日志中输出时间戳
-XX:+DisableExplicitGC:忽略显式调用
System.gc()
-XX:+PrintTenuringDistribution:打印对象晋升(Tenuring)阈值分布信息
-XX:HeapDumpPath:指定发生 OOM 时生成 heap dump 的路径
-XX:+UseGCLogFileRotation:开启 GC 日志轮转功能(需结合
-Xloggc
一起使用)
通过观察 GC 日志,可以分析 GC 触发原因、对象存活率、Minor/Full GC 次数与时间、晋升阈值 等,从而制定或调整相应的 GC 策略和参数。
2. 对象分配机制
2.1 对象优先分配在 Eden 区
大多数情况下,使用 new
创建的新对象会被分配到 新生代(Young Generation) 中的 Eden 区。当 Eden 区满时,会触发一次 Minor GC,回收不再使用的对象,存活下来的对象会被移动(或复制)到 Survivor 区(通常是 s0
或 s1
)。
2.2 大对象直接进入老年代
如果有大对象(如超大数组)需要分配,而 Eden 区(包括 Survivor)即使经过 Minor GC 也无法容纳,则该对象会直接分配到 老年代(Old Generation)。
可通过参数
-XX:PretenureSizeThreshold
(如3M
)来指定大对象阈值,大于该值的对象直接进入老年代。
3. 对象晋升规则
3.1 年龄阈值(Tenuring Threshold)
新生对象在 Eden 中,年龄为 0。
每经历一次 Minor GC,存活对象的年龄
age++
。当年龄超过
-XX:MaxTenuringThreshold
(默认 15)时,便会晋升到老年代。这一过程可以通过
-XX:+PrintTenuringDistribution
参数在日志中查看各年龄段对象分布。
3.2 动态年龄晋升
为了在新生代腾出更多空间给新对象,JVM 会在 Minor GC 阶段中进行统计:
如果某一年龄段的所有对象大小总和已经超过 Survivor 区的一半(或一定比例),则该年龄及以上的对象直接晋升到老年代。
这种机制可以避免大量对象在 Survivor 区重复“挤占”空间,从而提高内存利用率。
4. 生死抉择:可达性分析
4.1 GC Roots 可达性分析
判断对象是否存活,JVM 采用 可达性分析(Reachability Analysis)而非简单的引用计数:
GC Roots 通常包含:
虚拟机栈(栈帧)中的本地变量引用
方法区中类静态属性引用的对象
方法区中常量引用的对象(如字符串常量)
本地方法栈中 JNI 的引用
从这些 GC Roots 出发,进行“图遍历”,能被遍历到的对象是“可达对象”,否则判定为“无用对象”。
4.2 引用计数法(已被淘汰)
早期一些语言(如部分 C/C++ 库、Python)用引用计数判断对象存活,但存在无法解决循环引用的问题,Java 默认使用可达性分析来决定对象的去留。
5. 垃圾回收算法概述
垃圾回收算法本质上就是如何高效地回收无用对象并整理内存。在 JVM 中,最常见的有以下几类:
标记-清除(Mark-Sweep)
标记阶段:从 GC Roots 出发,标记所有可达对象。
清除阶段:遍历堆,把没有标记的对象回收。
缺点:容易产生内存碎片,后续分配可能需要维护复杂的空闲列表。
标记-整理(Mark-Compact)
在标记完成后,对存活对象做压缩或整理,将其向一端移动,保证内存的连续性。
避免了碎片问题,但对象移动开销较高。
复制算法(Copying)
将内存分为两块(或多块)等大小区域,实际只使用其中的一块(From 区)。当该块用满时,将存活对象复制到另一块(To 区),然后清理 From 区。
效率高且没有碎片,但实际可用内存被削减到一半。
在新生代常用此算法,因为多数对象“朝生夕灭”,存活率低,复制成本也低。
6. 分代回收算法
Java 垃圾回收最大特征在于分代:将堆划分为 新生代、老年代,以及方法区(Java 8 之后为 Metaspace)。
6.1 新生代
Eden + Survivor 0 + Survivor 1
使用复制算法(Copying),在 Minor GC 时,从 Eden + 一块 Survivor 复制存活对象到另一块 Survivor,然后清空原 Survivor。
存活率普遍较低,回收效率高且停顿时间短。
6.2 老年代
对象从新生代晋升而来,存活时间较长或大对象直接放入。
采用标记-清除或标记-整理等算法。
当老年代满或出现特殊情况(如调用
System.gc()
)时触发 Major GC / Full GC,停顿时间相对较长。
6.3 永久代(方法区 / Metaspace)
早期 JVM 中的“永久代(PermGen)”存放类元数据、常量池、静态变量等。在 Java 8 之后,改为使用本地内存的 Metaspace。
永久代/Metaspace 也可能发生回收,包括 废弃常量、无用的类。
回收条件:
该类所有实例已经回收;
该类的
Class
对象没有被引用;加载该类的
ClassLoader
已被回收;
-Xnoclassgc
可以禁止类卸载,适用于大量使用动态代理或 CGLib 的场景,以避免频繁的类加载卸载导致方法区溢出。
7. 分区收集算法(G1、ZGC 等)
随着大型内存应用的需求,出现了 G1、ZGC、Shenandoah 等更先进的收集器,它们都采取某种形式的分区(Region)或分块管理方式。
7.1 分区思想
将整堆划分为若干大小相等的 Region,既可以放新生代,也可以放老年代对象。
回收时并行和并发地做标记,优先回收垃圾最多的 Region,减少一次性清理大块内存所需的时间,并控制停顿时间。
7.2 G1(Garbage-First)
经典的分区收集器,在 JDK 9+ 成为默认的服务端收集器。
有年轻代 GC(Young GC) 和 混合 GC(Mixed GC) 两种模式,通过并发标记找出大量垃圾的 Region,集中回收。
目标是可预测的低延迟,适合较大堆内存、对停顿敏感的应用。
7.3 ZGC / Shenandoah
进一步降低 STW(Stop-The-World)时间,通过并发标记、染色指针(colored pointers)、读/写屏障等手段,实现超低延迟(如停顿时间不超过 10ms)
更适合超大内存(如 TB 级别)场景。
8. 常用 JVM/GC 参数
-Xms / -Xmx:设定 JVM 初始堆和最大堆大小
-Xmn:设置新生代大小(有时也可以用 G1 的
-XX:NewRatio
进行调整)-XX:SurvivorRatio:Eden 与 Survivor 区大小比例
-XX:MaxTenuringThreshold:最大晋升年龄阈值
-XX:PretenureSizeThreshold:大对象直接进老年代
-XX:MetaspaceSize / -XX:MaxMetaspaceSize:控制 Metaspace 大小(Java 8+)
-XX:+UseConcMarkSweepGC / -XX:+UseG1GC / -XX:+UseZGC:选择垃圾收集器
-XX:+PrintGCDetails / -Xloggc:输出详细 GC 日志
在实际生产环境中,需要结合业务吞吐量、延迟、内存大小等因素来选择和调优收集器,并合理设置各项参数。
9. 总结与优化思路
分代思想:根据对象存活周期,把堆划分为新生代与老年代,让 GC 针对不同区域使用最合适的回收算法。
Minor GC 频繁但停顿短,Full GC 较少但停顿长;尽量减少 Full GC 发生。
大对象直接进入老年代,避免在新生代中频繁复制。
晋升规则主要包括年龄阈值与动态年龄判断,以保证 Survivor 区的可用性。
可达性分析判断对象生死,标记-清除/复制/标记-整理等多种算法相结合,在不同区域/收集器中使用。
分区收集(G1 / ZGC / Shenandoah) 是未来趋势,适合大堆内存、低延迟需求。
GC 参数调优 需基于 GC 日志 和 性能监控 进行针对性调整,切忌盲目使用默认或某些“通用”参数。
通过充分理解以上原理和机制,结合实际应用场景的需求(如吞吐量、延迟、内存使用等),才能对 GC 行为进行更加合理有效的优化,从而提升应用的稳定性和整体性能。
Last updated
Was this helpful?