一、核心垃圾回收算法

Java 8 的收集器都基于以下算法组合:

  • 标记-清除 (Mark-Sweep):标记活对象,清除未标记对象。会产生内存碎片。

  • 标记-整理 (Mark-Compact):标记后,把活对象向一端移动,解决碎片,但 STW 时间更长。

  • 复制 (Copying):将存活对象从一块内存复制到另一块,清空原区域。用于年轻代,效率高但浪费一半空间(实际通过 Eden + Survivor 优化)。

  • 分代收集:根据对象存活周期,年轻代用复制算法,老年代用标记-清除/整理。

  • 永久代被移除,换成元空间(Metaspace)存放类元数据,使用本地内存

三、Java 8 可用的垃圾收集器

1. Parallel GC(吞吐量优先收集器)

1.1吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC 暂停时间)

1.2 模式: 多线程并行执行 Minor GC 和 Full GC,仍会 STW。

区域 算法
年轻代 复制 (Parallel Scavenge)
老年代 标记-整理 (Parallel Old)

1.3 Java 8 在 Server 类机器上的默认收集器(2 GB 内存以上、多核)。
适用:注重吞吐量(业务处理时间 / (业务处理时间+GC时间))的后台批处理、科学计算。

1.4 空间管理

  • 年轻代:一块 Eden + 两块 Survivor(通常叫 S0/S1 或 From/To)

  • 老年代:一整块连续空间

  • 元空间(Metaspace,不在堆内)

其中,年轻代内部这三块的大小是通过 -XX:SurvivorRatio 参数配置的,例如默认 8,表示 Eden : S0 : S1 = 8 : 1 : 1

1.5 垃圾回收过程

整个过程主要分两个阶段:Young GC(年轻代回收) 和 Full GC(全堆回收)

(1)Young GC —— 年轻代回收(仅 Eden + From Survivor)

触发条件:当 Eden 区被新对象填满,无法再分配时,触发一次 Young GC。

过程(复制算法):

  1. 标记存活对象:从 GC Roots 出发,找出 Eden 和当前 From Survivor(S0)中所有存活的对象。

  2. 复制到目标区域

    • 将存活对象复制到 To Survivor(S1) 中,对象的“年龄”会 +1。

    • 如果对象年龄达到 -XX:MaxTenuringThreshold(默认 15),或者 To Survivor 空间不足,就直接复制到 老年代

  3. 清空源区域

    • 复制完成后,整个 Eden 和 From Survivor 被清空,所有对象要么去了 To Survivor,要么晋升到老年代。

  4. 角色互换

    • 此时,之前的 To Survivor (S1) 变成了新的 From Survivor,里面装着存活下来的对象。

    • 之前的 From Survivor (S0) 变成了新的 To Survivor,完全为空。

    • Eden 区重新变成一整块空闲空间。

关键点
Young GC 只处理年轻代,老年代不参与(但需要通过卡表记录老年代对年轻代的引用)。这个过程是 Stop-The-World 的,所有应用线程暂停,由多条 GC 线程并行进行复制和清理。

(2)Old GC / Full GC —— 老年代 + 年轻代一起回收

触发条件

  • 老年代空间不足以容纳晋升的对象(Young GC 时发现)。

  • 或者通过 System.gc() 等显式触发。

  • 元空间不足也可能触发。

过程(标记-整理算法,Parallel Old):

  1. 标记存活对象:从 GC Roots 出发,标记整个堆(年轻代 + 老年代)所有存活对象。

  2. 计算整理位置:将存活对象向老年代的一端滑动,消除内存碎片。

  3. 更新引用:所有指向移动后对象的指针都需要更新。

  4. 回收空间:整理完成后,老年代剩余空间是连续的一大块,年轻代被完全清空(因为 Full GC 会处理年轻代)。

Full GC 也是 Stop-The-World 的,由多线程并行执行,但时间通常比 Young GC 长得多,会严重影响响应时间。

1.5 关键参数:

 2.G1 GC(垃圾优先收集器)

2.1 参数:

-XX:+UseG1GC 启用(java9开始才是默认gc收集器),大内存(通常大于 4GB)且要求低延迟(可预测的停顿时间)的场景非常适用

-XX:MaxGCPauseMillis 控制每次停顿时间在设置的这个时间之内,JVM 会尽全力去达成这个目标,但不提供任何保证

2.2 模式:传统的垃圾收集器(如 Serial, Parallel, CMS)将堆内存划分为连续的三大块:年轻代(Eden + 2个 Survivor)和老年代(Old),G1将堆划分为多个大小相等的 Region,每个 Region 可以在逻辑上扮演 Eden、Survivor、Old 或 Humongous(大对象)区域,不严格要求连续分代,优先回收价值最大的 Region(垃圾最多),以可预测的停顿时间为目标。

2.3 收集过程 (GC Phases)

G1 的垃圾收集主要分为三种模式:

  • Young GC (年轻代收集):

    • 触发条件: Eden 区域被占满时。

    • 过程: 这是一个完全 Stop-The-World(STW,暂停所有用户线程)的过程。G1 会将 Eden 区存活的对象复制到 Survivor 区;如果有些对象年龄足够大,或者 Survivor 区空间不足,则直接晋升到 Old 区。

    • 结果: 清空 Eden 区,重新计算下次 Young GC 的 Eden Region 数量。

  • Concurrent Marking (并发标记):

    • 触发条件: 当堆内存占用率达到阈值(-XX:InitiatingHeapOccupancyPercent,默认 45%)时触发。

    • 过程:

      1. 初始标记 (Initial Mark, STW): 伴随一次 Young GC,标记从 GC Roots 直接可达的对象。

      2. 并发标记 (Concurrent Mark): 和用户线程并发执行,遍历对象图找出所有存活对象。

      3. 最终标记 (Remark, STW): 处理并发阶段用户线程修改导致的遗漏。

      4. 清理 (Cleanup, STW/并发): 统计各 Region 的存活对象比例,完全为空的 Region 直接被回收。

  • Mixed GC (混合收集):

    • 触发条件: 并发标记结束后,G1 知道哪些 Old Region 里的垃圾最多。

    • 过程: 同样是 STW 的复制算法。它不仅会收集所有的年轻代 Region,还会根据设定的停顿时间目标,挑选一部分垃圾最多的老年代 Region 进行回收。这就是 G1 能够控制停顿时间并有效处理老年代碎片的原因。

2.4 为什么 Java 8 推荐在大堆使用 G1

G1 有几个显著优势:

  1. 无内存碎片: CMS 基于“标记-清除”算法,容易产生内存碎片,最终导致 Full GC。G1 在 Region 之间使用的是“复制”算法,局部看也是“标记-整理”,有效避免了碎片问题。

  2. 停顿时间可控: CMS 只能控制并发线程数,无法精准控制 STW 时间。G1 允许用户指定目标停顿时间。

  3. 内存占用更均匀: 化整为零的 Region 设计让内存在动态分配时更加灵活。

3. Serial GC(串行收集器)

单线程,所有 GC 工作都暂停应用线程(STW);单核 CPU、内存几百 MB 的小应用适用,简单高效;暂停时间长,多核环境浪费资源。

4. CMS GC(并发标记清除,低延迟)

致力于低延迟,在 Java 9 已被标记废弃,Java 14 移除(Java 8 仍可稳定使用)。

四、如何选择与调优

4.1 选择

目标 推荐收集器 常用参数
高吞吐(后台批处理 / 数据计算) Parallel -XX:MaxGCPauseMillis
低延迟(Web 接口 / 交易系统) CMS -XX:CMSInitiatingOccupancyFraction=70 等
大堆 + 可预测停顿 G1 -XX:MaxGCPauseMillis=200-XX:ParallelGCThreads

通用建议

  • 尽量让朝生夕死的对象在年轻代被回收,避免提前晋升导致老年代膨胀。

  • 合理设置堆大小、元空间上限,避免频繁 Full GC。

  • 警惕大对象

  • 防范内存泄漏 及时清理长生命周期容器(如静态 MapThreadLocal、自定义缓存)中的无用引用。如果老年代的使用率呈阶梯状持续上升,通常就是内存泄漏的信号。

  • 开启并分析 GC 日志: 这是排查问题的唯一真理。通过日志观察 GC 发生的频率、每次回收前后的内存大小以及具体的停顿时间。

  • 对症下药: 调优前先定位核心矛盾。是 Young GC 过于频繁导致 CPU 飙高?还是 Full GC 停顿太长导致接口超时?针对具体症状调整。

  • 控制变量法: 一次只修改一个参数,调整后必须通过压测或灰度环境的监控数据来验证效果。如果改了参数没效果,甚至变差了,立刻回滚。

4.2. 常用 JVM 参数(能配、能调)
1. 堆内存与基本分配 (核心必配)

堆内存的合理分配是 JVM 稳定的基础。通常建议将 -Xms-Xmx 设置为相同值。

  • -Xms<size>

    • 作用: 设置 JVM 启动时的初始堆内存大小。

    • 示例: -Xms4g (初始堆为 4GB)

  • -Xmx<size>

    • 作用: 设置 JVM 可使用的最大堆内存大小。

    • 示例: -Xmx4g (最大堆为 4GB)

    • 最佳实践: -Xms-Xmx 配置相同,避免运行时动态扩容/缩容带来的性能开销。

  • -XX:MetaspaceSize=<size> / -XX:MaxMetaspaceSize=<size>

    • 作用: 设置元空间 (Metaspace,存放类元数据) 的初始和最大大小。(注意:Java 8 移除了 PermGen 永久代,改为 Metaspace)

    • 示例: -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m

    • 建议: 默认情况下 Metaspace 会使用本地内存并动态扩容,为防止其无限扩张导致系统物理内存耗尽,强烈建议明确指定上限,并保持两者一致。

2. Java 8 垃圾收集器选择 (根据场景二选一)

Java 8 默认的垃圾收集器是 Parallel GC,但在大内存下 G1 表现更好。

选项 A:高吞吐量 (批处理/后台计算 - Java 8 默认)
  • -XX:+UseParallelGC

    • 作用: 启用 Parallel Scavenge (年轻代) + Parallel Old (老年代) 收集器。追求最大 CPU 利用率,不在乎偶尔较长的卡顿。

选项 B:低延迟/可控停顿 (Web 服务/微服务 - Java 8 强烈推荐)
  • -XX:+UseG1GC

    • 作用: 启用 G1 (Garbage-First) 收集器。适合大内存 (一般大于 4GB) 且要求较短 STW 停顿时间的应用。

  • -XX:MaxGCPauseMillis=<N>

    • 作用: (仅 G1) 设置期望的最大 GC 停顿时间(毫秒)。

    • 示例: -XX:MaxGCPauseMillis=200

    • 注意: 这是一个“软目标”,G1 会尽力满足,但不能保证绝对不超。设置过小会导致 GC 极其频繁,降低吞吐量。

3. OOM 防御与 Dump 获取 (极其重要)

当系统发生 OutOfMemoryError 时,必须保留案发现场,这些参数是救命稻草。

  • -XX:+HeapDumpOnOutOfMemoryError

    • 作用: 当 JVM 发生 OOM 时,自动生成 Heap Dump (.hprof) 文件。

  • -XX:HeapDumpPath=<path>

    • 作用: 指定 OOM 时生成的 Dump 文件的保存路径。

    • 示例: -XX:HeapDumpPath=/data/logs/jvm/heapdump.hprof

    • 注意: 确保运行 JVM 的用户对该目录有写入权限,并且所在磁盘有足够空间容纳整个堆。

  • -XX:+ExitOnOutOfMemoryError

    • 作用: 一旦发生 OOM,JVM 进程立刻退出。

    • 场景: 在容器化 (Kubernetes/Docker) 环境中非常有用。OOM 后立刻退出,由容器编排工具快速拉起新实例,避免服务处于假死状态。

4. GC 日志参数 (Java 8 必备)

如果你不知道系统卡在哪,第一步就是看 GC 日志。以下是 Java 8 专属的日志配置方式:

  • -XX:+PrintGCDetails (打印详细 GC 发生时的内存变化信息)

  • -XX:+PrintGCDateStamps (打印 GC 发生的绝对时间戳,方便与业务日志对齐)

  • -Xloggc:/path/to/gc.log (指定 GC 日志输出的物理路径)

  • -XX:+UseGCLogFileRotation (开启日志滚动,防止单个日志文件把磁盘撑爆)

  • -XX:NumberOfGCLogFiles=10 (保留的滚动文件个数)

  • -XX:GCLogFileSize=50M (单个日志文件大小,达到 50M 后自动滚动生成新文件)

4. GC 日志分析(能看懂,能定位)
  • 读懂每一行 Minor GC / Full GC 日志,知道各数字(回收前、回收后、总大小)的含义

  • 能从日志判断是否正常:年轻代回收是否过于频繁、对象晋升速度是否异常、Full GC 为何触发

  • 利用在线工具(如 gceasy)快速可视化分析

5. 常见问题排查能力(能解决实际问题)

必须能独立处理以下典型场景:

  • 频繁 Full GC:是内存泄漏导致老年代持续增长?还是大对象直接进入老年代?还是元空间不足?

  • CPU 飙高:可能是并发 GC 线程过多、或频繁 Young GC

  • OOM 异常

    • Java heap space:堆内存不足,dump 分析大对象

    • GC overhead limit exceeded:GC 占用了 98% 以上 CPU 却只回收不到 2% 堆,几乎内存耗尽

    • Metaspace:动态类加载或类加载器泄漏

  • 内存泄漏排查:通过 jmap -histojmap -dump + MAT / JProfiler 分析 GC Root 引用链

  • CMS Concurrent Mode Failure:并发收集时老年代不足,退化为 Serial Old,参数如何调整

6. 工具链(会用、能组合)
  • 命令行:jpsjstat -gcjmapjstackjcmd

  • 可视化:VisualVM、JConsole、MAT(内存分析)、GCViewer

  • 在线生产慎用 jmap -dump 时的 Full GC 影响,掌握安全 dump 方式

更多推荐