常见GC算法:标记-清除、复制、标记-整理
说起GC算法,很多刚入行的同学容易把它们当成理论考试题。但说实话,做性能优化的这几年,我真切体会到:选对算法,启动时间能直接砍掉一小半。
GC算法就那几种,不过每种都是个「矛盾体」——要么快但碎片多,要么整齐但慢。我习惯把这些算法看作「扫地策略」:你是想扫得干净但累死自己,还是随便扫扫但留一堆垃圾?
下面我一个个说,顺便聊聊我在项目里踩过的坑。
标记-清除算法(Mark-Sweep)
这是最朴素的算法。说白了就是两步:先标记哪些对象还活着,然后把没标记的全扔掉。JVM里的CMS收集器就用了这个思路。
| 优点 | 缺点 |
|---|---|
| 实现简单,不用移动对象 | 产生大量内存碎片 |
| 暂停时间可控(CMS的初衷) | 碎片导致大对象分配失败,触发Full GC |
| 没有复制开销 | 标记阶段需要遍历所有存活对象 |
适用场景:对延迟敏感、不介意碎片、堆内存够大的应用。比如某些实时通信中间件,GC暂停时间宁可控制在10ms内,碎片回头再用参数处理。
复制算法(Copying)
复制的思路很简单:把内存分成两块(From和To),只用一块。GC时,把存活对象从From复制到To,然后直接清空From。新生代用的就是这招。
| 优点 | 缺点 |
|---|---|
| 没有碎片,分配指针连续 | 浪费一半内存(Survivor区设计已经缓解了) |
| 分配速度快(bump the pointer) | 存活率较高时效率下降 |
| 适合少量长命 + 大量短命场景 | 需要额外内存用于复制目标区 |
我印象很深:一次广告推荐系统优化,新生代设到2GB,结果每次Minor GC都从10ms飙升到200ms。我一看,对象存活率出奇高——因为用了大量本地缓存。复制算法的本质就是「赌大多数对象都死得快」,如果赌输了,就反过来坑你。
所以,如果你发现Minor GC越来越慢,先检查幸存对象比例是不是超过30%。如果是,考虑调大老年代,或者改用标记-清除的变种。
标记-整理算法(Mark-Compact)
这算法像「标记-清除」的升级版。先标记存活对象,然后「推推挤挤」把它们挪到内存一端,剩下的空间直接回收。老年代的Old GC(比如G1的全局压缩)就用了这个思路。
| 优点 | 缺点 |
|---|---|
| 没有碎片,内存连续 | 需要移动对象,暂停时间长 |
| 大对象分配简单 | 整理过程需要计算新地址,消耗CPU |
| 适合老年代常驻对象 | 必须暂停应用线程(Stop The World) |
我个人的习惯是:如果项目启动阶段需要分配大量长生命周期对象(比如Spring容器、元数据),我会用标记-整理算法的老年代配置,比如Parallel Scavenge + Parallel Old。虽然暂停长一点,但启动后几乎不再GC。
三种算法对比:一张表看清
| 算法 | 碎片 | 暂停 | 内存利用率 | 适用区域 |
|---|---|---|---|---|
| 标记-清除 | 严重 | 低 | 高 | 老年代(CMS) |
| 复制 | 无 | 低 | 中(有浪费) | 新生代 |
| 标记-整理 | 无 | 高 | 高 | 老年代(Serial Old, Parallel Old) |
关于GC算法的选择——我的实战建议
你想想看,GC算法没有银弹。但有个规律很实用:
- 如果应用里对象全是「瞬时的」(比如无状态微服务),复制算法最香。
- 如果对象「又老又多」(比如缓存、连接池),标记-整理更稳。
- 如果既要低延迟又要防碎片,就得上G1或ZGC这类分代+分区混合算法。
其实,JVM默认就已经帮新手选了最优解(G1)。但做启动优化时,我们往往要「逆行」——比如启动阶段用Serial GC单线程整理,完成后切回G1。这个思路我在第四章会展开。
知识结构:三种算法的核心逻辑
下面这张图帮你把这三个算法的关系理清楚。记住:选择的关键在于「存活率」和「碎片容忍度」。
一点避坑指南
我曾经接手过一个大数据计算框架的启动优化,堆内存设了80GB。用了默认的G1,结果启动阶段一直在做全局标记,耗时超过20秒。后来我分析发现:启动时绝大部分对象都是「一次分配、全程存活」的(例如配置、集群元数据)。这时候复制算法毫无优势,反而标记-清除+老年代整理的组合更合适。
最后说个技巧:很多人忽略「预触发的GC」。如果启动阶段你明确知道会有大量对象消亡,可以手动触发一次Minor GC(用System.gc()并配合-XX:+DisableExplicitGC关掉?不,我是说先让它触发,然后再禁用)。嗯,这属于高级玩法,我后面章节会讲实例。
-XX:+PrintGCDetails -XX:+PrintGCApplicationStoppedTime,能帮你快速定位是哪个算法在导致长暂停。然后对症下药换算法,比调一堆玄学参数更有效。
三种算法就聊到这儿。记住它们各自的「脾气」:标记-清除怕碎片,复制怕存活率高,标记-整理怕暂停。搞懂了这些,再去选GC收集器就会豁然开朗。