GC 排查

309 阅读18分钟

fullGC 排查

首先对三种GC进行介绍,

  • Minor GC(新生代GC):

    • Minor GC主要关注清理年轻代(Young Generation)的内存区域。
    • 年轻代通常分为三个部分:Eden区和两个Survivor区(通常是S0和S1)。
    • 在Minor GC过程中,首先会进行Eden区的垃圾回收,存活的对象将会被移动到其中一个Survivor区。之后,再清理Eden区和另一个Survivor区。这个过程会使得年轻代中的对象晋升到老年代(Old Generation)。
    • Minor GC通常发生频繁,但它的停顿时间相对较短。
  • Major GC(老年代GC):

    • Major GC主要关注清理老年代的内存区域。
    • 触发Major GC的条件包括老年代空间不足,永久代(在Java 8及之前的版本)或Metaspace空间不足等。
    • Major GC的执行可能伴随较长的停顿时间,因为它需要整理老年代的内存,移动对象以减少碎片化。
  • Full GC(完全GC):

    • Full GC是对整个堆内存(包括年轻代、老年代、永久代或Metaspace等)进行清理的一种垃圾回收操作,它是Major GC的一种特殊情况。
    • 触发Full GC的条件可能包括老年代空间不足、永久代/Metaspace空间不足、显式调用System.gc()等。
    • Full GC的执行会导致相对较长的停顿时间,因为它需要对整个堆内存进行回收。

什么情况下会发生Minor GC?

Minor GC(或称为Young GC)通常在年轻代(Young Generation)垃圾回收时发生。年轻代是堆内存的一部分,用于存放新创建的对象。Minor GC发生的情况包括:

  • Minor GC 的触发条件Eden 区空间不足

    • 当一个新对象需要分配内存时,如果 Eden 区没有足够的可用空间,JVM 会触发 Minor GC。
    • Eden 区的对象经过 Minor GC 清理后,存活的对象会尝试被复制到 Survivor 区。
  • Survivor 空间不足与 Minor GC 触发的关系

    • Survivor 空间不足 仅会在 Minor GC 的过程中影响存活对象的处理方式,但不会直接触发 Minor GC。
    • 如果在 Minor GC 中,存活的对象无法完全复制到 Survivor 区,JVM 会采用 分配担保机制,将多余的存活对象直接晋升到老年代。
什么是 分配担保机制

分配担保机制(Allocation Guarantee/Promotion Guarantee)

分配担保机制是 JVM 内存管理中的一项策略,用于确保 Minor GC 在特定情况下能够正常执行,避免因内存不足导致程序崩溃。它的主要功能是确保在 年轻代(Eden + Survivor 区)垃圾回收时,老年代可以接收从年轻代晋升的对象。


背景与目的

在 JVM 的垃圾回收过程中,当 Eden 区空间不足时,会触发 Minor GC,将存活的对象移动到 Survivor 区或者老年代。但有时 Survivor 区空间不足,部分存活对象需要晋升到老年代。分配担保机制的目的是在以下情况下确保程序正常运行:

  1. Survivor 空间不足:部分存活对象需要直接晋升到老年代。
  2. 老年代空间不足:JVM 需要保证老年代有足够空间容纳这些晋升的对象,否则可能导致垃圾回收失败或程序崩溃。

分配担保机制的工作原理

  1. Minor GC 前的空间检查

    • 在执行 Minor GC 前,JVM 会检查老年代的剩余空间是否足够存放所有可能需要晋升的对象。
    • 这一估算基于之前 Minor GC 的晋升数据,例如最近一次 Minor GC 晋升到老年代的最大对象大小。
  2. 分配担保是否可用

    • 如果老年代剩余空间 足够大,则 Minor GC 正常进行。
    • 如果老年代剩余空间不足以保证晋升,JVM 会尝试触发 Full GC 来腾出空间。
  3. 晋升过程

    • 当 Survivor 空间不足时,部分对象直接从年轻代晋升到老年代。
    • 分配担保确保老年代可以接收这些对象,避免因内存不足导致程序崩溃。
  4. 失败情况

    • 如果老年代空间不足以接收晋升的对象,并且 Full GC 后空间仍不足,JVM 将抛出 OutOfMemoryError

分配担保的参数控制

分配担保机制受以下参数控制:

1. -XX:HandlePromotionFailure
  • 作用:控制是否启用分配担保机制。

    • true(JDK 6u24 及之前的版本):JVM 在 Minor GC 前会检查老年代空间是否足够,不足时尝试触发 Full GC。
    • false(JDK 6u24 及之后的版本,默认值):不依赖分配担保,JVM 始终执行 Minor GC,但可能会导致 OutOfMemoryError
2. -XX:PretenureSizeThreshold
  • 作用:设置对象的大小阈值。超过该阈值的对象会直接分配到老年代,而不会在年轻代分配。
  • 适用场景:减少 Survivor 空间和老年代晋升压力。
3. -XX:MaxTenuringThreshold
  • 作用:设置对象在 Survivor 区的最大年龄。超过该年龄的对象会晋升到老年代。
  • 优化方式:降低晋升阈值可以减少 Survivor 区的压力,但可能增加老年代的压力。

分配担保机制的执行示例

示例场景

假设一个 JVM 堆内存如下:

  • Eden 区:10 MB
  • 每个 Survivor 区:2 MB
  • 老年代:50 MB
  1. 触发 Minor GC

    • 当 Eden 区分配满(10 MB 对象)时,触发 Minor GC。
    • Eden 区的存活对象需要转移到 Survivor 区,假设存活对象总大小为 4 MB。
    • Survivor 区仅有 2 MB 空间,因此无法存放所有对象。
  2. 分配担保机制

    • 多出的 2 MB 对象直接晋升到老年代。
    • 老年代有足够的剩余空间(50 MB - 已用空间),分配成功。
  3. 老年代空间不足的情况

    • 如果老年代的剩余空间小于 2 MB(晋升对象的大小),JVM 可能触发 Full GC。
    • 如果 Full GC 后空间仍不足,JVM 抛出 OutOfMemoryError

分配担保机制的优化建议

  1. 优化 Survivor 区大小

    • 使用 -XX:SurvivorRatio 调整 Eden 和 Survivor 区的比例,增大 Survivor 区空间,减少对象直接晋升老年代的概率。
  2. 调整晋升年龄阈值

    • 使用 -XX:MaxTenuringThreshold 控制对象晋升老年代的年龄,延迟对象晋升,增加 Survivor 区的利用率。
  3. 预防大对象直接晋升

    • 使用 -XX:PretenureSizeThreshold 设置大对象直接分配到老年代的阈值,避免频繁晋升大对象。
  4. 选择合适的 GC 策略

    • 对于需要频繁分配短生命周期对象的应用,可以选择 G1 GC 或其他更适合的垃圾回收器。

总结

  • 分配担保机制的核心是确保在 Minor GC 时,老年代能够接收晋升的对象。
  • 如果老年代空间不足,JVM 会尝试通过 Full GC 腾出空间。
  • 分配担保机制可以有效避免年轻代回收失败导致的程序崩溃,但老年代空间不足仍可能导致 OutOfMemoryError
  • 通过调整 Survivor 区大小、晋升阈值等参数,可以减少分配担保机制的触发频率,从而提高应用的性能和稳定性。

分配担保机制的引入并不会影响 Minor GC 是否能进行Minor GC 是否触发的条件依旧是 Eden 区空间不足。分配担保机制的作用是在 Minor GC 过程中 为处理 Survivor 空间不足的情况提供保障。因此,在分配担保机制的参与下,Minor GC 可以正常进行,但对象晋升到老年代的策略可能会改变。


完整流程:分配担保机制如何作用于 Minor GC

  1. Eden 区空间不足触发 Minor GC

    • 当 Eden 区没有足够空间分配新对象时,会直接触发 Minor GC。这是 Minor GC 唯一的触发条件,与 Survivor 空间或老年代空间是否充足无关。
  2. Minor GC 尝试将对象复制到 Survivor 区

    • Minor GC 开始时,Eden 区的存活对象(通过 GC Roots 可达的对象)会尝试复制到 Survivor 区(S0S1)。
    • 如果 Survivor 区有足够空间,存活对象将被复制到 Survivor 区。
    • 如果 Survivor 空间不足,部分对象将被直接晋升到老年代。
  3. 分配担保机制检查老年代空间

    • JVM 会检查老年代是否有足够的剩余空间来接收可能晋升的对象。
    • 如果老年代空间足够,Minor GC 正常进行,晋升的对象直接进入老年代。
    • 如果老年代空间不足,JVM 会触发 Full GC 尝试清理老年代以腾出空间。
  4. Full GC 后的情况

    • 如果 Full GC 后老年代仍然没有足够的空间,JVM 会抛出 OutOfMemoryError

分配担保机制的关键点

  1. 分配担保机制不是触发 Minor GC 的条件

    • 分配担保机制只在 Minor GC 的过程中起作用,确保存活对象能找到去处。
    • Minor GC 的触发仅由 Eden 区空间不足决定。
  2. 分配担保机制不会阻止 Minor GC

    • 即使老年代空间不足,Minor GC 依然会触发,但存活对象可能无法全部晋升到老年代,从而导致后续触发 Full GC。
  3. Minor GC 是否执行与分配担保机制无关

    • 分配担保机制只是为 Minor GC 过程中提供保障机制,使得即使 Survivor 空间不足时,也能有策略处理存活对象(如晋升到老年代)。

总结

  • 分配担保机制后是否能进行 Minor GC?

    • 是的,分配担保机制的存在并不会影响 Minor GC 的触发。
    • 只要 Eden 区空间不足,就会触发 Minor GC,无论分配担保机制是否被触发。
  • 分配担保机制的作用是什么?

    • 在 Minor GC 过程中,为无法存放到 Survivor 区的对象提供一个晋升到老年代的保障,避免垃圾回收过程中因 Survivor 空间不足导致程序崩溃。
  • 什么时候会导致 Minor GC 后无法正常处理?

    • 如果老年代空间不足,并且 Full GC 无法回收足够空间,则可能导致 OutOfMemoryError。此时,分配担保机制的保障也失效了。

什么情况下会发生Full GC?

  1. 老年代空间不足: 当老年代无法容纳新生代晋升过来的对象时,可能触发Major GC。这通常发生在年轻代的Minor GC后,存活的对象被移动到老年代,导致老年代的空间不足。
  2. 永久代空间不足: 在Java 7及之前的版本中,常量池等信息存放在永久代中。如果常量池或类的元数据占用的空间过大,可能导致永久代空间不足,触发Full GC。在Java 8及之后的版本中,永久代被元空间(Metaspace)取代。
  3. 使用CMS(Concurrent Mark-Sweep)垃圾回收器时的并发失败: CMS是一种以减少应用程序停顿时间为目标的垃圾回收器,但它可能会因为一些原因(比如老年代空间不足)而导致并发失败,从而触发Full GC。
  4. System.gc()的显式调用: 调用System.gc()Runtime.getRuntime().gc()并不能确保会立即进行垃圾回收,但它可能会触发Full GC。
  5. 永久代/Metaspace溢出: 如果Metaspace(Java 8及以后的版本)或永久代(Java 7及之前的版本)中的元数据信息溢出,可能触发Full GC。
  6. 分配担保失败: 在进行Minor GC时,虚拟机会检查老年代的剩余空间是否大于新生代的对象总大小。如果不大于,会尝试进行一次Full GC。这是为了确保在新生代GC后,存活的对象能够顺利晋升到老年代。
  7. G1垃圾回收器的一些特殊情况: G1垃圾回收器在一些特殊情况下可能触发Full GC,例如在进行Mixed GC(混合收集)时,或者由于空间不足而放弃Mixed GC,转而执行Full GC。

如何避免频繁Full GC?

调整堆内存大小:
合理设置新生代和老年代的比例: 选择合适的垃圾回收器
检查内存泄漏

发生fullGC怎么排查?Java线上频繁Full GC 怎么排查

查看GC日志:启用 GC 日志(-Xloggc:<file>-XX:+PrintGCDetails)查看 Full GC 发生的时间、原因和内存使用情况。

首先,启用 GC 日志,以便深入了解垃圾回收的行为和原因。可以通过在 JVM 启动参数中添加以下选项来开启 GC 日志:

bash
复制代码
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<log_file>

解释:

  • -XX:+PrintGCDetails:打印垃圾回收的详细信息。
  • -XX:+PrintGCDateStamps:在 GC 日志中添加时间戳。
  • -Xloggc:<log_file>:指定日志文件路径,记录 GC 信息。

分析 GC 日志时,关注以下内容:

  • Full GC 时间和频率:查看 Full GC 发生的时间间隔,是否频繁。

  • 原因:GC 日志中会显示 Full GC 的原因,如 Allocation FailurePromotion Failure 等。

  • 内存使用情况:查看 Full GC 前后的堆内存使用量,是否存在频繁的堆内存抛弃或溢出的情况。

  • 堆内存设置:检查堆内存设置(-Xms-Xmx)。如果堆内存太小,可能导致频繁的 GC。

  • 内存泄漏检测:使用工具(如 VisualVM、MAT、JProfiler 等)分析是否有内存泄漏。

使用 Eclipse MAT:
  1. 对应用进行堆转储。
  2. 生成堆分析报告,查看是否有不释放的对象。
  3. 检查是否有大量无用对象被引用,导致内存无法释放。

1. 生成堆转储(Heap Dump)

堆转储是一个包含 Java 堆内存中所有对象及其状态的文件,可以用于后期分析。生成堆转储的方式有很多种,通常可以通过以下几种方式触发堆转储:

1.1 通过 JVM 参数触发堆转储

你可以在启动 Java 应用时,通过 JVM 参数指定堆转储文件的路径和文件名。常用的参数有:

bash
复制代码
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<path_to_file>
  • -XX:+HeapDumpOnOutOfMemoryError:如果发生 OutOfMemoryError,JVM 会自动生成堆转储文件。
  • -XX:HeapDumpPath=<path_to_file>:指定堆转储文件保存的位置。

例如,启动 JVM 时使用以下命令:

bash
复制代码
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof -jar yourapp.jar

这个命令会在应用抛出 OutOfMemoryError 时,生成一个名为 heapdump.hprof 的堆转储文件。

1.2 手动生成堆转储

如果应用没有发生 OutOfMemoryError,你也可以通过以下方式手动触发堆转储:

  • JVM 远程调试:可以使用 jmap 工具连接到运行中的 JVM 实例,并生成堆转储。

    例如,使用 jmap 命令生成堆转储:

    bash
    复制代码
    jmap -dump:format=b,file=/path/to/heapdump.hprof <pid_of_jvm>
    

    这里,<pid_of_jvm> 是你的应用进程的 PID。

  • JVisualVM:你可以通过 JVisualVM(一个图形化的监控工具)连接到正在运行的应用,并使用其 "Heap Dump" 功能生成堆转储。

1.3 Heap Dump 文件的内容

堆转储文件(通常是 .hprof 后缀)包含了 Java 堆的所有对象数据,可以通过分析该文件来查看内存分配情况,识别可能的内存泄漏。堆转储文件通常很大,包含应用中的所有对象及其引用关系。

常见内存泄漏迹象:
  • 静态集合或缓存:程序中使用了静态集合或缓存,但未按时清理,导致对象无法被垃圾回收。

  • ThreadLocalThreadLocal 使用不当,导致线程内存泄漏。

  • 事件监听器:没有正确移除的事件监听器。

  • GC策略调整:根据负载选择适合的垃圾回收器,例如 G1 或 CMS。

  • 对象创建和回收:分析是否有大量短生命周期的对象频繁创建,导致频繁回收。

在 Java 的 GC 日志中,Full GC 的原因 通常会以简短的描述形式显示,例如 Allocation FailurePromotion Failure。这些原因帮助开发者理解是什么情况导致了 Full GC 的触发,从而可以针对性地优化内存分配或垃圾回收配置。以下是常见原因的详细解释:


1. Allocation Failure

含义:
  • Allocation Failure 表示 JVM 在尝试为新对象分配内存时失败,通常因为堆空间(Eden 区或年轻代)不足导致。
  • Allocation Failure 是触发 Minor GC 的主要原因;如果 Minor GC 后仍然无法腾出足够空间(例如老年代空间不足导致晋升失败),会进一步触发 Full GC
触发路径:
  1. 新对象分配到 Eden 区,但 Eden 区空间不足,触发 Minor GC。
  2. Minor GC 后,部分存活对象需要晋升到老年代。
  3. 如果老年代空间不足以存放晋升的对象,就会触发 Full GC。
优化建议:
  • 增加堆内存大小:通过调整 JVM 参数 -Xmx 增大堆空间。
  • 优化对象分配:减少大对象和短生命周期对象的分配。
  • 调整垃圾回收器:如使用 G1 GC,它更适合高分配速率的应用。

2. Promotion Failure

含义:
  • Promotion Failure(或 Tenuring Failure)表示 在 Minor GC 时,存活对象需要从年轻代晋升到老年代,但老年代空间不足
  • 这是由于年轻代(Eden 和 Survivor 区)空间不足,且老年代无法接收晋升的对象。
触发路径:
  1. Eden 区满,触发 Minor GC。
  2. 存活对象尝试进入 Survivor 区,但 Survivor 区空间不足。
  3. 对象需要晋升到老年代。
  4. 老年代空间不足时,会触发 Full GC,尝试释放老年代的空间。
优化建议:
  • 增大老年代大小:使用 -Xmx-Xms,适当增大老年代占比。
  • 调整晋升阈值:通过 -XX:MaxTenuringThreshold 增加对象在 Survivor 区的停留时间,延迟晋升。
  • 优化 Survivor 比例:调整 -XX:SurvivorRatio 增大 Survivor 区,减少晋升压力。
  • 分析内存分配:检查是否有不必要的长生命周期对象或内存泄漏。

3. Concurrent Mode Failure

含义:
  • Concurrent Mode Failure 是 CMS 垃圾回收器特有的 Full GC 触发原因。
  • 当 CMS(Concurrent Mark-Sweep)在回收老年代时,应用程序线程仍在分配内存,导致老年代空间耗尽而无法继续运行,此时 JVM 被迫触发 Full GC。
触发路径:
  1. 使用 CMS GC 时,老年代的并发回收正在进行。
  2. 在并发回收尚未完成时,老年代空间不足以满足分配需求。
  3. JVM 触发 Full GC(通常是 Stop-The-World 的标记-清理算法)。
优化建议:
  • 增大老年代大小:通过 -Xmx-Xms 增加堆内存。
  • 优化 CMS 启动阈值:调整 -XX:CMSInitiatingOccupancyFraction 提前触发 CMS。
  • 使用 G1 GC:如果应用对低延迟要求更高,可以尝试用 G1 GC 替代 CMS。

4. GCLocker Initiated GC

含义:
  • GCLocker Initiated GC 是由 JVM 的 JNI 本地代码(如通过 DirectByteBuffer 分配的本地内存)触发。
  • 在某些情况下,本地代码可能通过 JNI 保持 GC 锁,阻止垃圾回收器回收堆内存。GC 锁释放后,JVM 会被迫触发一次 Full GC。
优化建议:
  • 尽量减少对 JNI 本地代码的使用,或者确保本地代码不会长时间持有 GC 锁。
  • 检查是否有大量 DirectByteBuffer 分配未及时释放。

5. System.gc() 调用

含义:
  • 调用 System.gc()(或通过 JMX 远程调用 gc 方法)会显式触发 Full GC。
  • 这通常是人为的,可能是应用程序或框架的代码调用了 System.gc()
优化建议:
  • 避免显式调用 System.gc(),除非有明确的业务需求。
  • 如果无法修改代码,可通过 JVM 参数 -XX:+DisableExplicitGC 禁用显式的 GC 调用。

6. Metaspace/PermGen Space Full

含义:
  • 在 JDK 8 之前,PermGen 空间不足(如类加载器未被正确释放)会触发 Full GC。
  • 从 JDK 8 开始,PermGen 被移除,替换为 Metaspace,当 Metaspace 空间不足时也可能触发 Full GC。
优化建议:
  • 如果使用 JDK 8 之前的版本:

    • 增加 PermGen 大小:-XX:PermSize-XX:MaxPermSize
  • 如果使用 JDK 8 或更高版本:

    • 增加 Metaspace 大小:-XX:MetaspaceSize-XX:MaxMetaspaceSize
    • 分析类加载情况,避免频繁的动态类加载或内存泄漏。

7. GC Ergonomics 调整

含义:
  • GC Ergonomics 是 JVM 的自适应垃圾回收策略,当 JVM 检测到内存分配速率过高,或垃圾回收无法满足设定的暂停时间目标时,可能会主动触发 Full GC。
优化建议:
  • 检查 JVM 参数,如 -XX:MaxGCPauseMillis,调整目标暂停时间。
  • 如果暂停时间目标过于激进,可以适当放宽限制。

GC 日志中的示例

假设你启用了 GC 日志:

-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps

你可能会看到如下日志:

2024-11-27T12:34:56.789: [Full GC (Allocation Failure) 1234K->567K(2048K), 0.0501234 secs]

解析:

  • Full GC:类型为 Full GC。
  • (Allocation Failure) :触发原因是分配失败。
  • 1234K->567K(2048K) :GC 前使用的堆内存是 1234K,GC 后是 567K,总堆空间是 2048K。
  • 0.0501234 secs:Full GC 的持续时间是 50 毫秒。

总结

常见的 Full GC 原因包括:

  1. Allocation Failure:分配新对象时内存不足。
  2. Promotion Failure:对象晋升到老年代失败。
  3. Concurrent Mode Failure:CMS 回收过程中老年代空间不足。
  4. GCLocker Initiated GC:JNI 本地代码导致的垃圾回收。
  5. System.gc() 调用:显式调用 System.gc()
  6. Metaspace/PermGen Space Full:类加载器未释放导致的空间不足。
  7. GC Ergonomics 调整:JVM 自适应垃圾回收策略触发的 Full GC。

通过分析日志中的原因和触发路径,可以针对性优化内存配置和垃圾回收策略,以减少 Full GC 的频率,提高系统性能。