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 区空间不足,部分存活对象需要晋升到老年代。分配担保机制的目的是在以下情况下确保程序正常运行:
- Survivor 空间不足:部分存活对象需要直接晋升到老年代。
- 老年代空间不足:JVM 需要保证老年代有足够空间容纳这些晋升的对象,否则可能导致垃圾回收失败或程序崩溃。
分配担保机制的工作原理
-
Minor GC 前的空间检查:
- 在执行 Minor GC 前,JVM 会检查老年代的剩余空间是否足够存放所有可能需要晋升的对象。
- 这一估算基于之前 Minor GC 的晋升数据,例如最近一次 Minor GC 晋升到老年代的最大对象大小。
-
分配担保是否可用:
- 如果老年代剩余空间 足够大,则 Minor GC 正常进行。
- 如果老年代剩余空间不足以保证晋升,JVM 会尝试触发 Full GC 来腾出空间。
-
晋升过程:
- 当 Survivor 空间不足时,部分对象直接从年轻代晋升到老年代。
- 分配担保确保老年代可以接收这些对象,避免因内存不足导致程序崩溃。
-
失败情况:
- 如果老年代空间不足以接收晋升的对象,并且 Full GC 后空间仍不足,JVM 将抛出
OutOfMemoryError。
- 如果老年代空间不足以接收晋升的对象,并且 Full GC 后空间仍不足,JVM 将抛出
分配担保的参数控制
分配担保机制受以下参数控制:
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
-
触发 Minor GC:
- 当 Eden 区分配满(10 MB 对象)时,触发 Minor GC。
- Eden 区的存活对象需要转移到 Survivor 区,假设存活对象总大小为 4 MB。
- Survivor 区仅有 2 MB 空间,因此无法存放所有对象。
-
分配担保机制:
- 多出的 2 MB 对象直接晋升到老年代。
- 老年代有足够的剩余空间(50 MB - 已用空间),分配成功。
-
老年代空间不足的情况:
- 如果老年代的剩余空间小于 2 MB(晋升对象的大小),JVM 可能触发 Full GC。
- 如果 Full GC 后空间仍不足,JVM 抛出
OutOfMemoryError。
分配担保机制的优化建议
-
优化 Survivor 区大小:
- 使用
-XX:SurvivorRatio调整 Eden 和 Survivor 区的比例,增大 Survivor 区空间,减少对象直接晋升老年代的概率。
- 使用
-
调整晋升年龄阈值:
- 使用
-XX:MaxTenuringThreshold控制对象晋升老年代的年龄,延迟对象晋升,增加 Survivor 区的利用率。
- 使用
-
预防大对象直接晋升:
- 使用
-XX:PretenureSizeThreshold设置大对象直接分配到老年代的阈值,避免频繁晋升大对象。
- 使用
-
选择合适的 GC 策略:
- 对于需要频繁分配短生命周期对象的应用,可以选择 G1 GC 或其他更适合的垃圾回收器。
总结
- 分配担保机制的核心是确保在 Minor GC 时,老年代能够接收晋升的对象。
- 如果老年代空间不足,JVM 会尝试通过 Full GC 腾出空间。
- 分配担保机制可以有效避免年轻代回收失败导致的程序崩溃,但老年代空间不足仍可能导致
OutOfMemoryError。 - 通过调整 Survivor 区大小、晋升阈值等参数,可以减少分配担保机制的触发频率,从而提高应用的性能和稳定性。
分配担保机制的引入并不会影响 Minor GC 是否能进行。Minor GC 是否触发的条件依旧是 Eden 区空间不足。分配担保机制的作用是在 Minor GC 过程中 为处理 Survivor 空间不足的情况提供保障。因此,在分配担保机制的参与下,Minor GC 可以正常进行,但对象晋升到老年代的策略可能会改变。
完整流程:分配担保机制如何作用于 Minor GC
-
Eden 区空间不足触发 Minor GC
- 当 Eden 区没有足够空间分配新对象时,会直接触发 Minor GC。这是 Minor GC 唯一的触发条件,与 Survivor 空间或老年代空间是否充足无关。
-
Minor GC 尝试将对象复制到 Survivor 区
- Minor GC 开始时,Eden 区的存活对象(通过 GC Roots 可达的对象)会尝试复制到 Survivor 区(
S0或S1)。 - 如果 Survivor 区有足够空间,存活对象将被复制到 Survivor 区。
- 如果 Survivor 空间不足,部分对象将被直接晋升到老年代。
- Minor GC 开始时,Eden 区的存活对象(通过 GC Roots 可达的对象)会尝试复制到 Survivor 区(
-
分配担保机制检查老年代空间
- JVM 会检查老年代是否有足够的剩余空间来接收可能晋升的对象。
- 如果老年代空间足够,Minor GC 正常进行,晋升的对象直接进入老年代。
- 如果老年代空间不足,JVM 会触发 Full GC 尝试清理老年代以腾出空间。
-
Full GC 后的情况
- 如果 Full GC 后老年代仍然没有足够的空间,JVM 会抛出
OutOfMemoryError。
- 如果 Full GC 后老年代仍然没有足够的空间,JVM 会抛出
分配担保机制的关键点
-
分配担保机制不是触发 Minor GC 的条件
- 分配担保机制只在 Minor GC 的过程中起作用,确保存活对象能找到去处。
- Minor GC 的触发仅由 Eden 区空间不足决定。
-
分配担保机制不会阻止 Minor GC
- 即使老年代空间不足,Minor GC 依然会触发,但存活对象可能无法全部晋升到老年代,从而导致后续触发 Full GC。
-
Minor GC 是否执行与分配担保机制无关
- 分配担保机制只是为 Minor GC 过程中提供保障机制,使得即使 Survivor 空间不足时,也能有策略处理存活对象(如晋升到老年代)。
总结
-
分配担保机制后是否能进行 Minor GC?
- 是的,分配担保机制的存在并不会影响 Minor GC 的触发。
- 只要 Eden 区空间不足,就会触发 Minor GC,无论分配担保机制是否被触发。
-
分配担保机制的作用是什么?
- 在 Minor GC 过程中,为无法存放到 Survivor 区的对象提供一个晋升到老年代的保障,避免垃圾回收过程中因 Survivor 空间不足导致程序崩溃。
-
什么时候会导致 Minor GC 后无法正常处理?
- 如果老年代空间不足,并且 Full GC 无法回收足够空间,则可能导致
OutOfMemoryError。此时,分配担保机制的保障也失效了。
- 如果老年代空间不足,并且 Full GC 无法回收足够空间,则可能导致
什么情况下会发生Full GC?
- 老年代空间不足: 当老年代无法容纳新生代晋升过来的对象时,可能触发Major GC。这通常发生在年轻代的Minor GC后,存活的对象被移动到老年代,导致老年代的空间不足。
- 永久代空间不足: 在Java 7及之前的版本中,常量池等信息存放在永久代中。如果常量池或类的元数据占用的空间过大,可能导致永久代空间不足,触发Full GC。在Java 8及之后的版本中,永久代被元空间(Metaspace)取代。
- 使用CMS(Concurrent Mark-Sweep)垃圾回收器时的并发失败: CMS是一种以减少应用程序停顿时间为目标的垃圾回收器,但它可能会因为一些原因(比如老年代空间不足)而导致并发失败,从而触发Full GC。
- System.gc()的显式调用: 调用
System.gc()或Runtime.getRuntime().gc()并不能确保会立即进行垃圾回收,但它可能会触发Full GC。 - 永久代/Metaspace溢出: 如果Metaspace(Java 8及以后的版本)或永久代(Java 7及之前的版本)中的元数据信息溢出,可能触发Full GC。
- 分配担保失败: 在进行Minor GC时,虚拟机会检查老年代的剩余空间是否大于新生代的对象总大小。如果不大于,会尝试进行一次Full GC。这是为了确保在新生代GC后,存活的对象能够顺利晋升到老年代。
- 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 Failure、Promotion Failure等。 -
内存使用情况:查看 Full GC 前后的堆内存使用量,是否存在频繁的堆内存抛弃或溢出的情况。
-
堆内存设置:检查堆内存设置(
-Xms和-Xmx)。如果堆内存太小,可能导致频繁的 GC。 -
内存泄漏检测:使用工具(如 VisualVM、MAT、JProfiler 等)分析是否有内存泄漏。
使用 Eclipse MAT:
- 对应用进行堆转储。
- 生成堆分析报告,查看是否有不释放的对象。
- 检查是否有大量无用对象被引用,导致内存无法释放。
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 堆的所有对象数据,可以通过分析该文件来查看内存分配情况,识别可能的内存泄漏。堆转储文件通常很大,包含应用中的所有对象及其引用关系。
常见内存泄漏迹象:
-
静态集合或缓存:程序中使用了静态集合或缓存,但未按时清理,导致对象无法被垃圾回收。
-
ThreadLocal:
ThreadLocal使用不当,导致线程内存泄漏。 -
事件监听器:没有正确移除的事件监听器。
-
GC策略调整:根据负载选择适合的垃圾回收器,例如 G1 或 CMS。
-
对象创建和回收:分析是否有大量短生命周期的对象频繁创建,导致频繁回收。
在 Java 的 GC 日志中,Full GC 的原因 通常会以简短的描述形式显示,例如 Allocation Failure 或 Promotion Failure。这些原因帮助开发者理解是什么情况导致了 Full GC 的触发,从而可以针对性地优化内存分配或垃圾回收配置。以下是常见原因的详细解释:
1. Allocation Failure
含义:
- Allocation Failure 表示 JVM 在尝试为新对象分配内存时失败,通常因为堆空间(Eden 区或年轻代)不足导致。
- Allocation Failure 是触发 Minor GC 的主要原因;如果 Minor GC 后仍然无法腾出足够空间(例如老年代空间不足导致晋升失败),会进一步触发 Full GC。
触发路径:
- 新对象分配到 Eden 区,但 Eden 区空间不足,触发 Minor GC。
- Minor GC 后,部分存活对象需要晋升到老年代。
- 如果老年代空间不足以存放晋升的对象,就会触发 Full GC。
优化建议:
- 增加堆内存大小:通过调整 JVM 参数
-Xmx增大堆空间。 - 优化对象分配:减少大对象和短生命周期对象的分配。
- 调整垃圾回收器:如使用 G1 GC,它更适合高分配速率的应用。
2. Promotion Failure
含义:
- Promotion Failure(或
Tenuring Failure)表示 在 Minor GC 时,存活对象需要从年轻代晋升到老年代,但老年代空间不足。 - 这是由于年轻代(Eden 和 Survivor 区)空间不足,且老年代无法接收晋升的对象。
触发路径:
- Eden 区满,触发 Minor GC。
- 存活对象尝试进入 Survivor 区,但 Survivor 区空间不足。
- 对象需要晋升到老年代。
- 老年代空间不足时,会触发 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。
触发路径:
- 使用 CMS GC 时,老年代的并发回收正在进行。
- 在并发回收尚未完成时,老年代空间不足以满足分配需求。
- 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。
- 增加 PermGen 大小:
-
如果使用 JDK 8 或更高版本:
- 增加 Metaspace 大小:
-XX:MetaspaceSize和-XX:MaxMetaspaceSize。 - 分析类加载情况,避免频繁的动态类加载或内存泄漏。
- 增加 Metaspace 大小:
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 原因包括:
- Allocation Failure:分配新对象时内存不足。
- Promotion Failure:对象晋升到老年代失败。
- Concurrent Mode Failure:CMS 回收过程中老年代空间不足。
- GCLocker Initiated GC:JNI 本地代码导致的垃圾回收。
- System.gc() 调用:显式调用
System.gc()。 - Metaspace/PermGen Space Full:类加载器未释放导致的空间不足。
- GC Ergonomics 调整:JVM 自适应垃圾回收策略触发的 Full GC。
通过分析日志中的原因和触发路径,可以针对性优化内存配置和垃圾回收策略,以减少 Full GC 的频率,提高系统性能。