Java 篇

Redis 篇

Java Full GC 触发条件、原因追踪排查及避免或注意事项详解


java 的 Full GC 贯穿了 java 垃圾回收的方方面面的知识,涉及 java 内存结构、内存分配原理、垃圾回收算法、内存调优策略等等,涵盖了 java 语言底层设计的核心内容。Full GC 会造成线程的阻塞,通俗讲就是 “stop the world”,频繁发生会导致系统不稳定,甚至不可用,作为 java 程序员不是降低其发生的频率而是要避免发生 Full GC。

GC 基本概念

常用的 HotSpot JVM 把内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

java gc 内存情况

其中 Eden + Survivor 称之为年轻代新生代),Survivor 又被等分为 2个 Survivor 区(分为 from 和 to 角色),Tenured/Old 则称为老年代

年轻代 GC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC

一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理,直接在老年代分配),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1岁,当它的年龄增加到一定程度(可以通过 -XX:MaxTenuringThreshold 来设置,默认为 15)时,就会被移动到年老代中。

年轻代中的对象基本(80%左右)在几轮 Minor GC 后都会被回收,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。基于上述的描述,可以深入理解为什么 Eden 区和 2个 Survivor 区的空间默认比例是 8:1:1。

GC 刚开始,对象只会存在于 Eden 区和名为 “From” 的 Survivor 区,另一个 Survivor 区的 “To” 是空的。接着进行 GC,Eden 区中所有存活的对象都会被复制到 “To”,而在 “From” 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到阈值的对象会被移动到年老代中,没有达到阈值的对象会被复制到 “To” 区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From” 和 “To” 会交换它们的角色,也就是新的 “To” 就是上次 GC 前的 “From”,新的 “From” 就是上次 GC 前的 “To”。无论如何,角色名为 “To” 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 “To” 区被填满,“To” 区被填满之后,会将所有对象移动到年老代中。

老年代 GC

接着上述的年轻代 GC,对老年代 GC 称为 Major GC

其实针对 Major GC 没有正式的定义,它有点复杂,一方面,很多 Major GC 都是由 Minor GC 触发的,所以很多情况下将这两个概念分开是不可能的,另一方面,很多现代的垃圾回收会部分的执行老年代(Tenured space)清理。

老年代空间的主要由新生代转入的对象、创建的大对象以及大数组对象。

在老年代发生垃圾回收时,由于老年代中的对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-整理算法(标记-整理算法)进行垃圾回收。

Major GC 的速度一般会比 Minor GC 慢 10倍以上。

Full GC

Full GC 触发条件

老年代中如果没有足够的内存空间去容纳新进的对象时,就会引发一次 Full GC;如果在执行完 Full GC 之后,还是没有办法给这些对象分配内存,那么就凉凉了,会抛出如下 OOM 错误:

java.lang.OutOfMemoryError: Java heap space

Full GC 原因

full gc 的原因其实非常的多,将近 30 多种,具体查看如下代码中 return 返回的字符串描述:

#include "precompiled.hpp"
#include "gc/shared/gcCause.hpp"

const char* GCCause::to_string(GCCause::Cause cause) {
  switch (cause) {
    case _java_lang_system_gc:
      return "System.gc()";

    case _full_gc_alot:
      return "FullGCAlot";

    case _scavenge_alot:
      return "ScavengeAlot";

    case _allocation_profiler:
      return "Allocation Profiler";

    case _jvmti_force_gc:
      return "JvmtiEnv ForceGarbageCollection";

    case _gc_locker:
      return "GCLocker Initiated GC";

    case _heap_inspection:
      return "Heap Inspection Initiated GC";

    case _heap_dump:
      return "Heap Dump Initiated GC";

    case _wb_young_gc:
      return "WhiteBox Initiated Young GC";

    case _wb_conc_mark:
      return "WhiteBox Initiated Concurrent Mark";

    case _wb_full_gc:
      return "WhiteBox Initiated Full GC";

    case _update_allocation_context_stats_inc:
    case _update_allocation_context_stats_full:
      return "Update Allocation Context Stats";

    case _no_gc:
      return "No GC";

    case _allocation_failure:
      return "Allocation Failure";

    case _tenured_generation_full:
      return "Tenured Generation Full";

    case _metadata_GC_threshold:
      return "Metadata GC Threshold";

    case _metadata_GC_clear_soft_refs:
      return "Metadata GC Clear Soft References";

    case _cms_generation_full:
      return "CMS Generation Full";

    case _cms_initial_mark:
      return "CMS Initial Mark";

    case _cms_final_remark:
      return "CMS Final Remark";

    case _cms_concurrent_mark:
      return "CMS Concurrent Mark";

    case _old_generation_expanded_on_last_scavenge:
      return "Old Generation Expanded On Last Scavenge";

    case _old_generation_too_full_to_scavenge:
      return "Old Generation Too Full To Scavenge";

    case _adaptive_size_policy:
      return "Ergonomics";

    case _g1_inc_collection_pause:
      return "G1 Evacuation Pause";

    case _g1_humongous_allocation:
      return "G1 Humongous Allocation";

    case _dcmd_gc_run:
      return "Diagnostic Command";

    case _last_gc_cause:
      return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";

    default:
      return "unknown GCCause";
  }
  ShouldNotReachHere();
}

这里主要介绍常见的原因,如下列表:

  • Full GC (Ergonomics):使用 Parallel Scavenge 垃圾回收器,若晋升到老年代的平均大小大于老年代剩余的空间大小,则会触发该类 Full GC,如下打印:

    2022-08-09T00:00:02.161+0800: 11046.680: [Full GC (Ergonomics) [PSYoungGen: 9518K->0K(1730048K)] [ParOldGen: 3652109K->1779475K(3495424K)] 3661627K->1779475K(5225472K), [Metaspace: 164114K->162637K(1204224K)], 3.1009201 secs] [Times: user=10.15 sys=0.28, real=3.10 secs]
  • Full GC (Metadata GC Threshold):它是指 Metaspace 扩容触发了 Full GC 的初始化阈值,如果未通过 -XX:MetaspaceSize 设置,则默认大约是 21 M;在 GC 后,Metaspace 会被动态调整,若本次 GC 释放了大量空间,那么就适当降低该值,如果释放的空间较小则适当提高该值,当然它的值不会大于 -XX:MaxMetaspaceSize;若超过最大阈值,则会出现如下 OOM 错误,程序崩溃:

    java.lang.OutOfMemoryError: Metaspace
  • Full GC (System.gc())System.gc() 方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也增加了间歇性停顿的次数。

    强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过 -XX:+DisableExplicitGC 来禁止 RMI 调用 System.gc()

Full GC 排查追踪

java 程序启动时,最好都配置 gc 日志的监控打印参数 -Xloggc,指定输出 gc 信息日志文件路径,具体示例如下:

-Xloggc:/xxx/xxxx/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause

上述示例是生产环境建议的实践配置,它会在指定目录输出类似如下信息:

2022-08-08T23:59:34.443+0800: 11018.963: [GC (Allocation Failure) [PSYoungGen: 1716775K->6161K(1730560K)] 5177705K->3469633K(5225984K), 0.0214261 secs] [Times: user=0.13 sys=0.00, real=0.02 secs] 
2022-08-09T00:00:01.990+0800: 11046.510: [GC (Allocation Failure) [PSYoungGen: 1674838K->9518K(1730048K)] 5138310K->3661627K(5411328K), 0.1692477 secs] [Times: user=0.71 sys=0.22, real=0.17 secs] 
2022-08-09T00:00:02.161+0800: 11046.680: [Full GC (Ergonomics) [PSYoungGen: 9518K->0K(1730048K)] [ParOldGen: 3652109K->1779475K(3495424K)] 3661627K->1779475K(5225472K), [Metaspace: 164114K->162637K(1204224K)], 3.1009201 secs] [Times: user=10.15 sys=0.28, real=3.10 secs] 
2022-08-09T00:00:13.126+0800: 11057.646: [GC (Allocation Failure) [PSYoungGen: 1713152K->16874K(1656320K)] 3492627K->1991145K(5151744K), 0.0492017 secs] [Times: user=0.36 sys=0.00, real=0.05 secs] 
2022-08-09T00:00:35.881+0800: 11080.401: [GC (Allocation Failure) [PSYoungGen: 1656298K->17351K(1611264K)] 3630569K->2006252K(5106688K), 0.0195847 secs] [Times: user=0.12 sys=0.00, real=0.02 secs]

排查 GC 的打印,在日常打印基础上添加下列参数:

-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+PrintHeapAtGC -XX:+PrintTLAB -XX:+PrintReferenceGC -XX:+PrintTenuringDistribution

Full GC 避免及注意事项

针对 full gc 的对应策略总结如下:

  • 合理的新生代、老年代的空间比例,默认即可,不要轻易修改,除非对自身程序内存情况有把握;
  • 尽量初始化堆最大值 -Xmx 设置时,同时设置 -Xms 初始化内存值,其值最好与最大值相同,以避免在每次 GC 后调整堆的大小,进而可能的 Full GC 发生。