想象你是一家餐厅的老板:厨房储物空间有限,食材原料源源不断进货——就像 Java 程序不断创建新对象。但厨房必须腾地方,于是你雇了一支保洁团队(垃圾回收器),定期清理用过的食材包装。尴尬的是,每次保洁都需要把厨房暂时关门(Stop-The-World),客人体验直线下降。如果储物空间布局不合理——备菜台太小,食材频繁往冷藏室塞;或者保洁策略太激进——动不动就全员大扫除——餐厅都会陷入崩溃边缘。 GC 调优,就是重新设计厨房的布局与保洁策略:备菜台该摆多大(新生代),冷藏室该扩到多少(老年代),是选择“营业结束后大扫除”还是“分区错峰保洁”,如何用监控报表持续发现隐患。本篇将围绕七个核心主题,用这个厨房比喻,带你系统掌握 JVM GC 调优的完整方法论。

岗位关联

掌握 GC 调优能让你在业务出现接口超时、内存飙升时快速定位根因,并通过 JVM 参数和诊断工具系统化解决问题,是 Java 后端工程师进阶性能专家的必备技能。

目录
  1. 可视化演示
  2. 调优方法论
  3. 堆内存配置
  4. GC算法选择
  5. 停顿时间调优
  6. 吞吐量优化
  7. 监控分析工具
  8. 效果验证与复盘
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过堆内存分代模型、GC收集器对比和停顿-吞吐权衡三个维度,展示JVM垃圾回收调优的核心机制与选型策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

堆内存分代模型 Eden→S0/S1→Old三级分代,对象经多次GC晋升 Minor GC清理Young区,Full GC清理整个堆 Eden满→Minor GC→存活对象进Survivor→多次存活晋升Old 分代策略利用对象生命周期差异,提升GC效率

调优方法论

开一家餐厅,你不会一上来就砸钱扩厨房。你会先想清楚:顾客最在意上菜快(低延迟),还是翻台率高(高吞吐)?目标定了,才知道该优化出餐流程还是加桌子。GC 调优的第一步,同样不是动参数,而是定目标——这是所有调优工作的原点。

JVM GC 调优的核心目标有两个维度:停顿时间(每次 GC 暂停业务多久)和吞吐量(CPU 花在业务上的时间比例)。这两个维度往往此消彼长,必须结合业务场景做取舍。系统化调优遵循四步循环:明确目标 → 定位瓶颈 → 制定方案 → 验证效果,每一步都有对应的工具与指标支撑。

整个调优过程中,吞吐量是一个贯穿始终的度量指标:

就像餐厅统计“有效营业占比”——一天开门 10 小时,若 2 小时在保洁无法接待客人,有效营业率就是 80%。调优方法论的意义,就是帮你系统性地找出这 2 小时浪费在哪里、如何压缩。

吞吐量=TappTapp+Tgc\text{吞吐量} = \frac{T_{\text{app}}}{T_{\text{app}} + T_{\text{gc}}}
该公式表示 CPU 花在“正事”上的时间占比。GC 暂停时间越长,分母越大,吞吐量越低。
  1. T_app应用真正执行“正事”——处理请求、计算、读写——的总耗时,单位毫秒
    TappT_{\text{app}}
  2. T_gc垃圾回收导致的一切暂停总时长,这是调优中要重点“压榨”的对象
    TgcT_{\text{gc}}
  3. 分母求和应用的总运行时间 = 业务时间 + GC 时间,两者互相挤占资源
    Tapp+TgcT_{\text{app}} + T_{\text{gc}}
  4. 除法比值算出业务时间占总时间的比例,越接近 1(100%)说明系统越高效
    TappTapp+Tgc\frac{T_{\text{app}}}{T_{\text{app}} + T_{\text{gc}}}

堆内存配置

厨房空间就那么大,备菜台(新生代)和冷藏库(老年代)的比例怎么分?备菜台太小,食材频繁往冷藏库塞,冷藏库很快爆满;备菜台太大,冷藏库就装不下长期存货。这就是 JVM 堆配置的核心矛盾。

JVM 堆内存分为新生代和老年代。新生代又细分为 Eden 区和两个 Survivor 区(S0/S1),默认比例 Eden:S0:S1 = 8:1:1。新生代与老年代的空间比例由 -XX:NewRatio 控制,默认值为 2,即老年代是新生代的 2 倍。

新生代大小的计算公式如下:

除了代际比例,还有两个重要配置:大对象阈值(-XX:PretenureSizeThreshold)决定超过多大直接进老年代,避免大对象在新生代反复拷贝;逃逸分析(-XX:+DoEscapeAnalysis)则让不逃逸的对象在栈上分配,根本不进堆,从源头减轻 GC 压力。备菜台与冷藏库的大小、特殊食材的处理流程,都是厨房空间管理的艺术——调好它们,GC 的节奏自然会温和许多。

新生代大小=堆总大小NewRatio+1\text{新生代大小} = \frac{\text{堆总大小}}{\text{NewRatio} + 1}
当 -XX:NewRatio=2 时,新生代占堆的 1/3,老年代占 2/3。这个比例决定了对象“早夭”(被快速回收)和“长寿”(进入老年代)的空间分配。
  1. NewRatio 参数JVM 启动参数 -XX:NewRatio 的取值,表示老年代是新生代的多少倍
    NewRatio\text{NewRatio}
  2. 加 1 的含义因为“新生代 + 老年代 = 堆总大小”,老年代占 NewRatio 份,新生代占 1 份,总份数是 NewRatio + 1
    NewRatio+1\text{NewRatio} + 1
  3. 除法把堆均分成 NewRatio + 1 份,新生代占其中 1 份
    堆总大小NewRatio+1\frac{\text{堆总大小}}{\text{NewRatio} + 1}

GC算法选择

不同规模的餐厅选择不同的保洁方式:小餐馆请一个保洁阿姨慢慢打扫(Serial);快餐连锁组保洁团队并行作业(Parallel);高档餐厅在营业时分区错峰保洁(CMS/G1)。选错保洁策略,再勤快的打扫也会拖垮营业。

JVM 提供多种垃圾回收器,核心差异在“何时停业、停多久”:

  • Serial(-XX:+UseSerialGC):单线程回收,适合几百 MB 的小堆、单核环境,停顿时间较长但实现简单;
  • Parallel(-XX:+UseParallelGC):多线程并行回收,追求高吞吐,JDK8 的默认回收器;
  • CMS(-XX:+UseConcMarkSweepGC):并发标记清除,减少停顿,但会产生碎片,JDK9 起废弃;
  • G1(-XX:+UseG1GC):分区管理 + 可预测停顿,JDK9+ 的默认回收器。

无论哪种回收器,一次 GC 的暂停时间都可以拆解为三个阶段:

选择回收器就像给餐厅定保洁制度:小堆低并发用 Serial 反而简单高效;追求吞吐量不怕偶发停顿选 Parallel;延迟敏感型业务优先 G1。没有最好的回收器,只有最适合你“餐厅规模”的方案。

STW总=t根扫描+t标记+t清理\text{STW}_{\text{总}} = t_{\text{根扫描}} + t_{\text{标记}} + t_{\text{清理}}
STW(Stop-The-World)指 GC 时应用线程全部冻结的全局暂停。公式说明暂停时间由三部分构成,不同回收器针对这三阶段做了不同的减负策略。
  1. 根扫描从 GC Roots 出发寻找引用链的时间——栈帧、常量池、静态变量等都是根
    t根扫描t_{\text{根扫描}}
  2. 标记阶段顺着引用链遍历并标记所有存活对象,是 GC 的核心耗时环节
    t标记t_{\text{标记}}
  3. 清理阶段回收死亡对象占用的内存,必要时还要整理碎片、压缩空间
    t清理t_{\text{清理}}
  4. 三阶段求和三者之和就是一次 GC 的 STW 总暂停,优化每一段都能缩短停顿
    t根扫描+t标记+t清理t_{\text{根扫描}} + t_{\text{标记}} + t_{\text{清理}}

停顿时间调优

G1 回收器就像一个“分区保洁团队”——把厨房分成很多小区域,每次只打扫几个最脏的区域,而不是一闭店就全屋大扫除。通过精准排期,把每次暂停控制在可接受范围内。这就是“停顿时间调优”的精髓。

G1(Garbage First)把堆划分为多个 Region,优先回收“垃圾最多”的区域,并通过 -XX:MaxGCPauseMillis=100 设置期望停顿目标。G1 会依据历史统计数据,动态决策本次回收多少个 Region,尽量把停顿控制在目标附近——但请记住,这是一个软目标,而非硬性保证。

G1 的停顿时间预测模型可以简化为:

停顿调优的关键在“拆”:把一次大停顿拆成多次小停顿,每次只影响少量业务请求。配合并发标记(在应用运行的同时标记对象,避免 STW),G1 能将单次停顿从秒级降到百毫秒级。此外,合理设置 Region 大小(-XX:G1HeapRegionSize)和年轻代大小(通过 -XX:G1NewSizePercent),也能显著改善停顿表现——就像厨房分区设计好了,保洁自然又快又不扰客。

期望停顿≈∑i∈Rcosti\text{期望停顿} \approx \sum_{i \in \mathcal{R}} \text{cost}_i
其中 $\mathcal{R}$ 是本次选择的待回收 Region 集合,$\text{cost}_i$ 是回收单个 Region 的预估耗时。G1 通过调节集合 $\mathcal{R}$ 的大小来控制总停顿。
  1. Region 集合G1 每次从堆中挑选若干 Region 进行回收,只挑“垃圾最多”的,性价比最高
    R\mathcal{R}
  2. 单 Region 成本每个 Region 的回收耗时与存活对象数量相关,G1 用历史数据做统计回归
    costi\text{cost}_i
  3. 求和总停顿时间 ≈ 所有选中 Region 的回收成本之和,Region 选得越少停顿越短
    ∑i∈Rcosti\sum_{i \in \mathcal{R}} \text{cost}_i
  4. 约等号预测模型基于历史均值,实际停顿可能上下浮动,因此目标是“期望”而非“保证”
    ≈\approx

吞吐量优化

快餐连锁追求的是“翻台率”——单位时间接待更多客人。为了这个目标,可以接受“午市结束后集中保洁一次”,也不愿在营业高峰频繁打扫。这就是 Parallel GC 的设计哲学:牺牲部分响应时间,换取更高吞吐。但如果 GC 太频繁,再快的保洁也会拖垮营业。

吞吐量优化的核心是双管齐下:一是降低 GC 频率(减少保洁次数),二是缩短单次停顿(每次保洁更快)。GC 频率取决于对象分配速率和堆空间大小,两者之间存在一个直观的关系:

Parallel GC 提供了两个直接参数:-XX:MaxGCPauseMillis(停顿目标)和 -XX:GCTimeRatio(吞吐量目标,默认 99%)。吞吐量优化的另一面是充分利用 CPU 核数——并行回收线程数默认为 CPU 核数,多核环境下的并行清扫能大幅缩短单次停顿。对于批处理型任务(如夜间报表、离线计算),还可以考虑批处理模式:在业务低峰期主动触发 GC,把停顿的影响降到最低。餐厅的翻台率上去了,客人的绝对等待时间也许略增,但整体接待能力却大幅提升。

GC频率≈对象分配速率×存活率新生代大小\text{GC频率} \approx \frac{\text{对象分配速率} \times \text{存活率}}{\text{新生代大小}}
GC 频率与新生代大小成反比:新生代越大,能容纳的对象越多,触发 GC 的间隔越长。但新生代过大会压缩老年代空间,导致 Full GC 频率上升,需要权衡。
  1. 对象分配速率应用每秒新建对象占用的内存量(MB/s),由业务特征决定,比如频繁创建中间对象的代码会显著推高分配速率
    对象分配速率\text{对象分配速率}
  2. 存活率Minor GC 后依然存活的对象比例(通常 10%~30%),存活对象会晋升或留在 Survivor
    存活率\text{存活率}
  3. 分子乘积每次 GC 要“搬运”的存活对象量 = 分配速率 × 存活率,这个值越大,GC 工作量越大
    对象分配速率×存活率\text{对象分配速率} \times \text{存活率}
  4. 分母新生代可用空间,增大它会直接拉长 GC 触发间隔,从而降低频率
    新生代大小\text{新生代大小}

监控分析工具

餐厅装了后厨摄像头和点单系统,老板才能看清哪道菜出得慢、是哪个环节堵了。JVM 也有一套“监控仪表盘”,调优必须靠数据说话,否则就像蒙着眼睛炒菜——全凭感觉。

常用的 JVM 监控工具有:jstat(实时统计 GC 次数与耗时)、jmap(堆转储,分析对象分布与内存泄漏)、jstack(线程快照,排查死锁与线程阻塞)、jcmd(综合诊断命令),以及 GC 日志(最详细的调优证据)。

从 GC 日志中,可以提取两个最核心的量化指标:

实际使用中,jstat -gcutil <pid> 1000 可以每秒输出一次堆使用率与 GC 统计;jmap -dump:format=b,file=heap.bin <pid> 导出堆快照后用 MAT 分析对象分布;GC 日志则通过 -Xlog:gc*(JDK9+)或 -XX:+PrintGCDetails(JDK8)开启。可视化工具如 GCViewer 或 GCEasy 能把日志变成图表,让趋势一目了然。厨房有了监控,保洁效率的每一次波动都尽在掌握。

tˉGC=1N∑i=1Nti,fGC=NT运行\bar{t}_{\text{GC}} = \frac{1}{N}\sum_{i=1}^{N} t_i, \qquad f_{\text{GC}} = \frac{N}{T_{\text{运行}}}
左式是平均单次 GC 停顿时间,右式是 GC 触发频率。这两个指标构成了调优效果的“体量表”,每次调整参数前后都要对比它们。
  1. 单次停顿GC 日志中每条 pause 记录的时间戳差值,记为 t_i,是分析的最小颗粒
    tit_i
  2. 平均停顿把 N 次 GC 停顿求和再除以 N,衡量单次 GC 对业务的平均影响
    1N∑i=1Nti\frac{1}{N}\sum_{i=1}^{N} t_i
  3. GC 次数统计窗口内的 GC 总次数 N,可以直接从日志中 grep 计数
    NN
  4. 频率GC 次数除以运行时长,衡量 GC 的密集程度——频率越高,业务被“打断”越频繁
    NT运行\frac{N}{T_{\text{运行}}}

效果验证与复盘

新菜单好不好,不能拍脑袋说“感觉不错”,要对比试点前后的营业额、客人好评率。GC 调优同样需要“对比实验”——没有验证的调优,只是自我安慰。

效果验证的核心流程是:先采集优化前的基线指标(GC 停顿、频率、吞吐量、TP99 延迟),再实施参数调整,最后在相同负载下对比前后数据,确认提升真实、稳定、无副作用。

性能提升率的计算公式简单而实用:

验证之后还要做回归测试:确认没有引入内存泄漏、对象分配异常等新问题,并在全链路压测下观察长期稳定性。最后是持续观测——业务负载会变化(比如节假日流量暴涨),今天的优秀配置明天可能失效。让监控告警常态化,定期复盘 GC 趋势,才能让调优成果持久保鲜。厨房的改进不是一劳永逸的,而是持续迭代的精细管理。

提升率=优化前−优化后优化前×100%\text{提升率} = \frac{\text{优化前} - \text{优化后}}{\text{优化前}} \times 100\%
这个公式适用于任何可量化指标(平均停顿、GC 频率、TP99 延迟等),将绝对差异归一化为百分比,便于横向比较不同优化手段的收益。
  1. 优化前指标基线阶段的测量值,例如平均 GC 停顿 120ms,必须在负载稳定时采集
    优化前\text{优化前}
  2. 优化后指标调整参数后的测量值,例如平均停顿降到 45ms,需在相同业务负载下复测
    优化后\text{优化后}
  3. 差值优化减少的绝对量,体现优化带来的直接收益(120 - 45 = 75ms)
    优化前−优化后\text{优化前} - \text{优化后}
  4. 归一化百分比差值除以基线值再乘 100%,得到相对提升幅度,便于与其他指标横向对比
    优化前−优化后优化前×100%\frac{\text{优化前} - \text{优化后}}{\text{优化前}} \times 100\%

AI 应用场景

AI推理服务的GC调优

阶段:模型部署与在线推理

解决的问题:在线 AI 服务(推荐系统、智能客服、图像识别)对延迟极度敏感。默认 JVM 配置下,G1 的停顿可能达到 200ms 以上,导致接口 P99 延迟飙高、用户明显卡顿。通过设置 -XX:MaxGCPauseMillis=100、调整堆大小与新生代比例,可以将 GC 停顿压缩到 40ms 以内。

具体案例:某智能客服系统每秒处理 2000 个推理请求,频繁 Full GC 导致超时率 5%。工程师使用 jstat 定位到老年代频繁占满,随后将堆从 4GB 扩至 8GB、设置 G1 停顿目标 100ms,并开启并发标记优化,最终超时率降至 0.2%,P99 延迟从 800ms 降到 150ms。

大数据特征工程的JVM优化

阶段:数据预处理与特征计算

解决的问题:Spark/Flink 等大数据框架的 Executor 进程是典型的长时运行 JVM,GC 停顿会直接拖慢整个数据管道。默认 Parallel GC 虽吞吐高,但堆配置不合理时 GC 时间可占运行时间的 30% 以上。通过统一 -Xms 与 -Xmx 避免动态扩缩容、调大新生代比例,可显著提升作业效率。

具体案例:某特征计算任务处理 10 亿样本,运行 2 小时,其中 GC 耗时 36 分钟。将堆固定为 16GB、NewRatio 调至 1(新生代 8GB),并开启并行 Full GC 后,GC 耗时降至 12 分钟,整体作业提速 22%。

模型训练平台的元数据服务调优

阶段:训练任务调度与元数据管理

解决的问题:训练平台的元数据服务(记录任务状态、模型版本、数据集索引)面对高并发读写,JVM 中产生了大量短生命周期对象,导致 Minor GC 频繁。结合监控工具分析对象分配,针对性优化代码并调整堆配置,能有效缓解 GC 压力。

具体案例:某训练平台的模型注册服务每分钟处理 5 万次元数据操作,GC 频率高达每秒 3 次。通过 jmap 堆转储发现 70% 的对象来自日志框架的字符串拼接,优化后 GC 频率降到每秒 0.5 次。

Python 演示

python67 行
"""JVM GC调优模拟器:对比不同堆大小与代际配置下的GC表现"""  class GCSimulator:    """模拟JVM堆中对象的分配与回收过程"""     def __init__(self, heap_mb=512, young_ratio=0.33, gc_cost=0.01):        self.heap_mb = heap_mb        self.young_quota = heap_mb * young_ratio        self.old_quota = heap_mb - self.young_quota        self.gc_cost = gc_cost        self.alloc_rate = 50        self.runtime_ms = 0        self.gc_total_ms = 0        self.gc_count = 0        self.young_used = 0        self.old_used = 0     def run_seconds(self, secs=60):        for _ in range(secs):            self.young_used += self.alloc_rate            self.runtime_ms += 1000            if self.young_used >= self.young_quota:                self.minor_gc()        return self.summary()     def minor_gc(self):        survive = self.young_used * 0.15        self.young_used = survive * 0.3        self.old_used += survive * 0.7        self.gc_count += 1        pause = self.young_used * self.gc_cost        self.gc_total_ms += pause        if self.old_used >= self.old_quota * 0.95:            self.old_used -= self.old_used * 0.4            pause = self.old_used * self.gc_cost * 3            self.gc_total_ms += pause            self.gc_count += 1     def summary(self):        total = self.runtime_ms + self.gc_total_ms        return {            "GC次数": self.gc_count,            "GC总耗时(ms)": round(self.gc_total_ms, 1),            "应用运行(ms)": self.runtime_ms,            "吞吐量": round(self.runtime_ms / total * 100, 2),            "平均停顿(ms)": round(self.gc_total_ms / max(self.gc_count, 1), 2),        }  def benchmark():    configs = {        "小堆-256MB": {"heap_mb": 256},        "中堆-512MB": {"heap_mb": 512},        "大堆-1GB": {"heap_mb": 1024},        "大堆+大新生代": {"heap_mb": 1024, "young_ratio": 0.5},    }    for name, cfg in configs.items():        sim = GCSimulator(**cfg)        result = sim.run_seconds(60)        print(f"-- {name} --")        for key, val in result.items():            print(f"  {key}: {val}")  if __name__ == "__main__":    benchmark()
目的:模拟不同堆内存配置在相同业务负载下的 GC 表现,直观展示“堆越大、新生代越大,GC 频率越低但单次 Full GC 成本越高”的规律。
  1. self.young_quota = heap_mb * young_ratio按比例分配新生代空间,相当于备菜台的面积
  2. self.young_used += self.alloc_rate模拟每秒业务产生 50MB 新对象(食材持续进场)
  3. if self.young_used >= self.young_quota:备菜台满了就触发 Minor GC(保洁开始)
  4. survive = self.young_used * 0.1515% 的对象存活下来,其余被回收(大部分食材是“一次性包装”)
  5. self.old_used += survive * 0.7大部分存活对象晋升到老年代(需要长期冷藏的食材)
  6. pause = self.young_used * self.gc_costGC 停顿时间与堆使用量成正比(越脏打扫越久)
  7. total = self.runtime_ms + self.gc_total_ms总时间 = 业务时间 + GC 时间,这个分母决定了吞吐量
  8. "吞吐量": round(self.runtime_ms / total * 100, 2)用公式验证:业务时间占比越高越好
  9. "大堆+大新生代": {"heap_mb": 1024, "young_ratio": 0.5}对比实验设计:改一个变量,才能看出因果

常见误区

误区堆内存设得越大,GC 就越不频繁,系统性能就越好。反正内存现在很便宜,直接把 -Xmx 设成 32GB 准没错。

正解堆过大会导致一次 Full GC 的耗时呈超线性增长——就像厨房太大,一次大扫除反而要花更久。堆内存增大后,GC 频率确实下降,但每次 GC 的停顿时间显著上升,可能直接击穿延迟阈值。合理的做法是根据对象的“存活曲线”找到堆大小的甜点区,让 GC 频率和单次停顿的乘积最小。

为何常见:直觉上“空间大=少清理”,但忽略了 GC 的扫描成本与堆大小正相关,尤其是 Full GC 需要遍历整个老年代。

误区G1 是最先进的回收器,所有应用都应该换成 G1,立刻就能解决 GC 问题。

正解G1 的优势在大堆(4GB+)和可预测停顿场景。对于几百 MB 的小堆、单机低并发应用,Parallel 甚至 Serial 可能表现更好——因为 G1 的 Region 管理和并发标记本身有额外开销,小堆上反而拖慢性能。选择回收器要看堆大小、硬件配置、延迟敏感度,不能唯“新”是举。

为何常见:技术文章都在吹捧 G1,让人误以为“新=好”,忽略了 JVM 提供多种回收器的设计初衷是适配不同场景。

误区调优完成后,GC 参数就一劳永逸了,下次再出现性能问题就是别的原因。

正解业务负载是动态的——大促流量翻倍、新功能引入大量缓存、数据量持续增长,都会改变对象分配模式和存活率。昨天的甜点区配置,明天可能就是毒药。正确的做法是把监控告警和定期复盘纳入日常,让调优成为一个持续迭代的循环。

为何常见:人们倾向于把复杂问题简化为“一次性解决方案”,而忽略了系统环境的动态性。

误区网上说 G1 停顿低,我就照着配;网上说堆设 8GB 好,我就改成 8GB。至于为什么,不管。

正解没有日志数据支撑的调优,等于猜。GC 日志记录了每次停顿的耗时、原因、内存变化,是定位问题的第一手证据。先看日志,确认瓶颈是 Minor GC 频繁还是 Full GC 停顿长,再对症下药。盲目套用网上的参数,可能把原本健康的应用调得更糟。

为何常见:读懂 GC 日志需要一些经验积累,初学者容易走捷径——直接抄别人的配置,不愿花时间做数据分析。

面试题

Q1. 进行 JVM GC 调优时,第一步应该做什么?

单选题 · easy

  • A.直接调大堆内存
  • B.明确性能优化目标← 正确答案
  • C.立刻更换 GC 算法
  • D.重启应用看效果

答案:B。调优的第一步永远是明确目标——是追求低停顿(延迟敏感)还是高吞吐(批处理)?没有目标,后续的参数调整就是无的放矢。就像餐厅先决定要做私房菜还是快餐,才能规划厨房布局。

Q2. JVM 新生代中,默认的 Eden 与 Survivor 比例是多少?

单选题 · easy

  • A.1 : 1
  • B.2 : 1
  • C.8 : 1← 正确答案
  • D.3 : 2

答案:C。JVM 默认 -XX:SurvivorRatio=8,即 Eden:S0:S1 = 8:1:1。Eden 区是对象的主要出生地,两个 Survivor 区轮流存放 Minor GC 后的幸存者,比例为 8:1:1 是经过大量实践验证的默认值。

Q3. 以下哪个垃圾回收器在 JDK9 及以上版本中成为默认选项?

单选题 · mid

  • A.Serial 回收器
  • B.Parallel 回收器
  • C.CMS 回收器
  • D.G1 回收器← 正确答案

答案:D。从 JDK9 开始,G1 取代 Parallel 成为默认垃圾回收器。G1 通过分区管理和可预测停顿模型,在延迟敏感型业务中表现更好。CMS 在 JDK9 被标记废弃,Serial 和 Parallel 仍可作为备选但不再是默认。

Q4. 关于 G1 回收器的特点,以下说法正确的是哪些?

多选题 · mid

  • A.支持设定停顿时间目标← 正确答案
  • B.采用分 Region 管理堆← 正确答案
  • C.全程串行执行回收
  • D.支持并发标记阶段← 正确答案

答案:ABD。G1 的核心特点是分区(Region)管理和并发标记,并能通过 -XX:MaxGCPauseMillis 设定期望停顿目标。它并非全程串行——标记阶段与应用线程并发执行,只有部分阶段会 STW。因此 C 错误。

Q5. 在 JVM GC 中,STW 的含义是什么?

单选题 · easy

  • A.服务器主动停机维护
  • B.全局暂停应用线程← 正确答案
  • C.内存溢出的错误标识
  • D.线程安全的并发机制

答案:B。STW 即 Stop-The-World,指 GC 执行时所有应用线程被冻结的全局暂停。STW 期间业务完全停滞,是 GC 停顿时间的直接来源。优化 GC 的核心就是压缩 STW 的时长和频率。

Q6. 某应用 GC 频繁且吞吐量低,分析发现 Minor GC 每 2 秒触发一次,下列哪项调整最有可能直接降低 GC 频率?

单选题 · hard

  • A.减小堆内存总量
  • B.增大新生代空间← 正确答案
  • C.改用 Serial 回收器
  • D.减少 CPU 分配核数

答案:B。GC 频率与新生代空间近似成反比——新生代越大,Eden 区能容纳的对象越多,Minor GC 的触发间隔越长。减小堆和减少 CPU 核数都会让情况更糟,换 Serial 只会让单次停顿更久。

Q7. 以下哪些工具或手段可以用于分析 JVM GC 问题?

多选题 · mid

  • A.jstat 实时统计← 正确答案
  • B.jmap 堆转储← 正确答案
  • C.JD-GUI 反编译
  • D.GC 日志分析← 正确答案

答案:ABD。jstat 可实时查看 GC 次数与耗时,jmap 可导出堆快照分析对象分布,GC 日志是最详细的调优证据。JD-GUI 是 Java 反编译工具,与 GC 分析无关,属于混淆项。

Q8. 关于 Major GC 和 Full GC,下列说法正确的是?

单选题 · mid

  • A.两者完全等价,没有区别
  • B.Major GC 只回收老年代← 正确答案
  • C.Major GC 只回收新生代
  • D.Full GC 只回收元空间

答案:B。Major GC 特指针对老年代的回收,Full GC 则通常指对整个堆(新生代+老年代+元空间)的回收,两者并不完全等价。实际中很多人混用这两个词,面试时要注意区分。

Q9. 验证 GC 调优效果时,应该重点对比以下哪些指标?

多选题 · hard

  • A.平均 GC 停顿时间← 正确答案
  • B.应用侧 P99 延迟← 正确答案
  • C.GC 触发频率← 正确答案
  • D.日志文件大小

答案:ABC。平均停顿、GC 频率、P99 延迟是衡量调优效果的核心指标——它们分别从单次影响、打断频次、用户体感三个维度评价。日志文件大小只反映日志量,与性能无关,是干扰项。

Q10. 设置 -XX:MaxGCPauseMillis=100 后,G1 会如何表现?

单选题 · extreme

  • A.硬性保证每次不超过 100ms
  • B.作为期望目标尽力靠近← 正确答案
  • C.控制 GC 线程数不超过 100
  • D.限制堆内存为 100MB

答案:B。MaxGCPauseMillis 是 G1 的软性停顿目标,G1 通过调节回收 Region 数量尽量使停顿接近该值,但极端情况下(如并发标记失败、晋升失败)仍可能超时。它不是硬性限制,更与线程数、堆大小无关。