起点:最朴素的标记-清除
G1再牛,它的基础还是垃圾回收的老祖宗——标记-清除。想象内存里一堆对象,GC的任务是清垃圾。简单点就是:从根(栈变量、全局对象)开始,标记活对象,没标记的扫掉。
Demo举例
假设内存2GB,20万个对象,每个占10KB,总共2GB。根引用1000个,这1000个连着5000个活对象。GC标记这6000个(60MB),剩下19.4万个(1.94GB)清掉,内存回收。
问题1:停顿时间长得离谱
标记20万个对象得扫描所有引用链,假设每次检查1微秒,总共200毫秒,程序全停(Stop-The-World)。用户那边卡0.2秒,游戏都得掉帧,体验直接崩。
问题2:碎片一大堆
清掉1.94GB后,60MB活对象散在内存里,像个破麻袋。假设下次要分配50MB连续空间,可能愣是找不到,得挪半天,效率惨兮兮。
优化方向
得让GC停顿短点,别全扫;碎片也得收拾。这俩目标就是G1的灵魂,咱们一步步逼近。
第一步进化:分代收集加点料
朴素的全扫太笨,咱们引入分代收集。大部分对象活不久(幼年死亡假设),那就分新生代和老年代,新生代快清,老年代慢点。
怎么搞?
新生代用标记-复制,活的挪走,死的不留痕迹;老年代用标记-清除,悠着来。假设2GB内存,新生代400MB,老年代1.6GB。新生代20万个对象,GC一跑,18万死(90%),2万活(20MB)挪到Survivor,花40毫秒。老年代晚点再清。
问题1:老年代还是拖后腿
新生代快了,但老年代1.6GB,4万个活对象(40MB),标记-清除全扫得150毫秒,停顿还是长。
问题2:碎片没跑
老年代清完,40MB活对象散成1000块,下次分配30MB大对象,照样卡壳。
优化方向
老年代得再细分,别一次全清;停顿得可控,碎片得收拾。这就指向G1的分区思路了。
G1的真面目:分区+优先回收+停顿控制
G1(Garbage-First)把内存切成小块(Region),每个区域独立管理,按“垃圾最多”的优先级回收,目标是停顿时间可预测,碎片也少。咱们细拆一下。
内存布局
假设2GB内存分成2000个1MB区域。新生代占400个(400MB),老年代1600个(1.6GB)。新生代再细分:
- Eden:新生对象出生地,占300MB(300个区域)。
- Survivor:活下来的待观察,占100MB(100个区域)。 老年代就是1600个1MB区域,动态调整。
回收流程
G1分两类回收:Young GC(新生代)和Mixed GC(混合回收)。
-
Young GC
- Eden满时触发,比如300MB塞满30万个对象(10KB/个)。
- 标记活对象,假设3万活(30MB,10%存活率),复制到Survivor。
- 耗时:标记30万对象约60毫秒,复制3万对象约10毫秒,总共70毫秒停顿。
- 结果:300MB回收,碎片全无。
-
Mixed GC
- 当老年代占用超一定比例(默认45%,720MB)时,触发混合回收。
- G1挑垃圾最多的区域清。比如老年代1600个区域,选20个,每区域活200KB(20%),垃圾800KB。
- 标记+复制:20个区域共4MB活对象挪走,16MB垃圾回收,停顿50毫秒。
- 优势:只清局部,停顿可控。
停顿控制
G1有个“目标停顿时间”(默认200毫秒)。假设程序每秒分配100MB对象,G1得每秒回收100MB。200毫秒内回收20MB(20个区域),一秒跑5次,刚刚好。用户几乎无感。
碎片管理
G1用标记-复制,活对象挪到新区域,旧区域全清,碎片自然没了。比如20个区域回收,4MB活对象挤一块,16MB连续空间直接可用。
数字敏感点
- 2GB内存,2000区域,每区域1MB。
- 新生代400MB,存活率10%,每次Young GC回收360MB。
- 老年代1.6GB,Mixed GC挑20区域,回收16MB,停顿50毫秒。
- 每秒分配100MB,G1跑5次Mixed GC,稳住内存。
G1的内在机制:细节拉满
G1不简单,靠几个关键设计撑场面:
1. 记忆集(Remembered Set)
区域化后,对象跨区域引用咋办?G1给每个区域配个记忆集,记着谁指向自己。比如区域A有对象指向区域B,B的记忆集记下A。Young GC时,只扫新生代的根+记忆集,不用全扫老年代。假设老年代1600区域,平均每区域50个跨引用,总共8万指针,扫起来也就16毫秒。
2. 并发标记
老年代垃圾多时,G1跑并发标记:
- 初始标记:停顿,标记根,10毫秒。
- 并发标记:和程序一起跑,标记全老年代,500毫秒,用户无感。
- 重新标记:短停顿,修正漏标,20毫秒。
- 结果:知道哪些区域垃圾多,下次Mixed GC有的放矢。
3. 动态调整
G1会根据停顿目标调整新生代大小。比如目标50毫秒,但Young GC超了,就缩Eden到200MB,降负载。
G1的坑和优化空间
G1很强,但也有软肋。
问题1:记忆集维护成本
跨区域引用多,记忆集就膨胀。假设老年代1600区域,每区域100个跨引用,记忆集得记16万指针,更新时CPU忙不过来,拖慢程序。
问题2:吞吐量短板
G1追求低停顿,频繁小回收。假设每50毫秒跑一次,一秒20次,CPU花30%时间在GC。而Parallel GC可能一秒跑一次,吞吐量更高。G1适合Web服务,高吞吐任务就吃力。
优化方向
- 并发更强:标记、清理全并发,记忆集优化,像ZGC那样极致低停顿。
- 碎片再压:大对象分配更顺畅,朝超大内存优化。
总结:从朴素到G1的推演
- 标记-清除 → 停顿长、碎片多,得改。
- 分代收集 → 效率高,但老年代坑。
- G1分区+优先 → 停顿可控、碎片少,现代GC标杆。
- 未来趋势 → 并发更牛、内存更优,跟ZGC接轨。