日志格式
ParallelGC YoungGC日志
ParallelGC FullGC日志
最佳实践
在不同的 JVM 的不垃圾回收器上,看参数默认是什么,不要轻信别人的建议,命令行示例如下:
java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy
PrintCommandLineFlags:通过它,你能够查看当前所使用的垃圾回收器和一些默认的值。
# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=127905216 -XX:MaxHeapSize=2046483456 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_41"
OpenJDK Runtime Environment (build 1.8.0_41-b04)
OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)
G1垃圾收集器JVM参数最佳实践:
# 1.基本参数
-server # 服务器模式
-Xmx12g # 初始堆大小
-Xms12g # 最大堆大小
-Xss256k # 每个线程的栈内存大小
-XX:+UseG1GC # 使用 G1 (Garbage First) 垃圾收集器
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=1g # 元空间最大大小
-XX:MaxGCPauseMillis=200 # 每次YGC / MixedGC 的最多停顿时间 (期望最长停顿时间)
# 2.必备参数
-XX:+PrintGCDetails # 输出详细GC日志
-XX:+PrintGCDateStamps # 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintTenuringDistribution # 打印对象分布:为了分析GC时的晋升情况和晋升导致的高暂停,看对象年龄分布日志
-XX:+PrintHeapAtGC # 在进行GC的前后打印出堆的信息
-XX:+PrintReferenceGC # 打印Reference处理信息:强引用/弱引用/软引用/虚引用/finalize方法万一有问题
-XX:+PrintGCApplicationStoppedTime # 打印STW时间
-XX:+PrintGCApplicationConCurrentTime # 打印GC间隔的服务运行时长
# 3.日志分割参数
-XX:+UseGCLogFileRotation # 开启日志文件分割
-XX:NumberOfGCLogFiles=14 # 最多分割几个文件,超过之后从头文件开始写
-XX:GCLogFileSize=32M # 每个文件上限大小,超过就触发分割
-Xloggc:/path/to/gc-%t.log # GC日志输出的文件路径,使用%t作为日志文件名,即gc-2021-03-29_20-41-47.log
CMS垃圾收集器JVM参数最佳实践:
# 1.基本参数
-server # 服务器模式
-Xmx4g # JVM最大允许分配的堆内存,按需分配
-Xms4g # JVM初始分配的堆内存,一般和Xmx配置成一样以避免每次gc后JVM重新分配内存
-Xmn256m # 年轻代内存大小,整个JVM内存=年轻代 + 年老代 + 持久代
-Xss512k # 设置每个线程的堆栈大小
-XX:+DisableExplicitGC # 忽略手动调用GC, System.gc()的调用就会变成一个空调用,完全不触发GC
-XX:+UseConcMarkSweepGC # 使用 CMS 垃圾收集器
-XX:+CMSParallelRemarkEnabled # 降低标记停顿
-XX:+UseCMSCompactAtFullCollection # 在FULL GC的时候对年老代的压缩
-XX:+UseFastAccessorMethods # 原始类型的快速优化
-XX:+UseCMSInitiatingOccupancyOnly # 使用手动定义初始化定义开始CMS收集
-XX:LargePageSizeInBytes=128m # 内存页的大小
-XX:CMSInitiatingOccupancyFraction=70 # 使用cms作为垃圾回收使用70%后开始CMS收集
# 2.必备参数
-XX:+PrintGCDetails # 输出详细GC日志
-XX:+PrintGCDateStamps # 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintTenuringDistribution # 打印对象分布:为分析GC时的晋升情况和晋升导致的高暂停,看对象年龄分布
-XX:+PrintHeapAtGC # 在进行GC的前后打印出堆的信息
-XX:+PrintReferenceGC # 打印Reference处理信息:强引用/弱引用/软引用/虚引用/finalize方法万一有问题
-XX:+PrintGCApplicationStoppedTime # 打印STW时间
-XX:+PrintGCApplicationConCurrentTime # 打印GC间隔的服务运行时长
# 3.日志分割参数
-XX:+UseGCLogFileRotation # 开启日志文件分割
-XX:NumberOfGCLogFiles=14 # 最多分割几个文件,超过之后从头文件开始写
-XX:GCLogFileSize=32M # 每个文件上限大小,超过就触发分割
-Xloggc:/path/to/gc-%t.log # GC日志输出的文件路径,使用%t作为日志文件名,即gc-2021-03-29_20-41-47.log
test、stage 环境jvm使用CMS 参数配置(jdk8)
-server -Xms256M -Xmx256M -Xss512k -Xmn96M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=256M -XX:MaxHeapSize=256M -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=2 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UnlockDiagnosticVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxTenuringThreshold=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8 -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../dump
online 环境jvm使用CMS参数配置(jdk8)
-server -Xms4G -Xmx4G -Xss512k -Xmn1536M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=4G -XX:MaxHeapSize=4G -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=2 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UnlockDiagnosticVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxTenuringThreshold=10 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8 -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../dump
Full GC场景
场景一:System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过 -XX:+ DisableExplicitGC
来禁止RMI调用System.gc()。
场景二:老年代空间不足
- 原因分析:新生代对象转入老年代、创建大对象或数组时,执行FullGC后仍空间不足
- 抛出错误:
Java.lang.OutOfMemoryError: Java heap space
- 解决办法:
- 尽量让对象在YoungGC时被回收
- 让对象在新生代多存活一段时间
- 不要创建过大的对象或数组
场景三:永生区空间不足
- 原因分析:JVM方法区因系统中要加载的类、反射的类和调用的方法较多而可能会被占满
- 抛出错误:
java.lang.OutOfMemoryError: PermGen space
- 解决办法:
- 增大老年代空间大小
- 使用CMS GC
场景四:CMS GC时出现promotion failed和concurrent mode failure
- 原因分析:
promotion failed
:是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成concurrent mode failure
:是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的
- 抛出错误:GC日志中存在
promotion failed
和concurrent mode
- 解决办法:增大幸存区或老年代
场景五:堆中分配很大的对象
- 原因分析:创建大对象或长数据时,此对象直接进入老年代,而老年代虽有很大剩余空间,但没有足够的连续空间来存储
- 抛出错误:触发FullGC
- 解决办法:配置-XX:+UseCMSCompactAtFullCollection开关参数,用于享受用完FullGC后额外免费赠送的碎片整理过程,但同时停顿时间不得不变长。可以使用-XX:CMSFullGCsBeforeCompaction参数来指定执行多少次不压缩的FullGC后才执行一次压缩
CMS GC场景
场景一:动态扩容引起的空间震荡
-
现象
服务刚刚启动时 GC 次数较多,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示:
-
原因
在 JVM 的参数中
-Xms
和-Xmx
设置的不一致,在初始化时只会初始-Xms
大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。另外,如果空间剩余很多时也会进行缩容操作,JVM 通过-XX:MinHeapFreeRatio
和-XX:MaxHeapFreeRatio
来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。整个伸缩的模型理解可以看这个图,当 committed 的空间大小超过了低水位/高水位的大小,capacity 也会随之调整: -
策略
观察 CMS GC 触发时间点 Old/MetaSpace 区的 committed 占比是不是一个固定的值,或者像上文提到的观察总的内存使用率也可以。尽量 将成对出现的空间大小配置参数设置成固定的 ,如
-Xms
和-Xmx
,-XX:MaxNewSize
和-XX:NewSize
,-XX:MetaSpaceSize
和-XX:MaxMetaSpaceSize
等。
场景二:显式GC的去与留
-
现象
除了扩容缩容会触发 CMS GC 之外,还有 Old 区达到回收阈值、MetaSpace 空间不足、Young 区晋升失败、大对象担保失败等几种触发条件,如果这些情况都没有发生却触发了 GC ?这种情况有可能是代码中手动调用了 System.gc 方法,此时可以找到 GC 日志中的 GC Cause 确认下。
-
原因
保留 System.gc:CMS中使用 Foreground Collector 时将会带来非常长的 STW,在应用程序中 System.gc 被频繁调用,那就非常危险。增加
-XX:+DisableExplicitGC
参数则可以禁用。去掉 System.gc:禁用掉后会带来另一个内存泄漏的问题,为 DirectByteBuffer 分配空间过程中会显式调用 System.gc ,希望通过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory,如Netty等。 -
策略
无论是保留还是去掉都会有一定的风险点,不过目前互联网中的 RPC 通信会大量使用 NIO,所以建议保留。此外 JVM 还提供了
-XX:+ExplicitGCInvokesConcurrent
和-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
参数来将 System.gc 的触发类型从 Foreground 改为 Background,同时 Background 也会做 Reference Processing,这样的话就能大幅降低了 STW 开销,同时也不会发生 NIO Direct Memory OOM。
场景三:MetaSpace区OOM
-
现象
JVM 在启动后或者某个时间点开始, MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决 。
-
原因
在讨论为什么会 OOM 之前,我们先来看一下这个区里面会存什么数据,Java 7 之前字符串常量池被放到了 Perm 区,所有被 intern 的 String 都会被存在这里,由于 String.intern 是不受控的,所以
-XX:MaxPermSize
的值也不太好设置,经常会出现java.lang.OutOfMemoryError: PermGen space
异常,所以在 Java 7 之后常量池等字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等几项被移到 Heap 中。而 Java 8 之后 PermGen 也被移除,取而代之的是 MetaSpace。由场景一可知,为了避免弹性伸缩带来的额外 GC 消耗,我们会将-XX:MetaSpaceSize
和-XX:MaxMetaSpaceSize
两个值设置为固定的,但这样也会导致在空间不够的时候无法扩容,然后频繁地触发 GC,最终 OOM。所以关键原因是 ClassLoader不停地在内存中load了新的Class ,一般这种问题都发生在动态类加载等情况上。 -
策略
可以 dump 快照之后通过 JProfiler 或 MAT 观察 Classes 的 Histogram(直方图) 即可,或者直接通过命令即可定位, jcmd 打几次 Histogram 的图,看一下具体是哪个包下的 Class 增加较多就可以定位。如果无法从整体的角度定位,可以添加
-XX:+TraceClassLoading
和-XX:+TraceClassUnLoading
参数观察详细的类加载和卸载信息。
场景四:过早晋升
-
现象
这种场景主要发生在分代的收集器上面,专业的术语称为“Premature Promotion”。90% 的对象朝生夕死,只有在 Young 区经历过几次 GC 的洗礼后才会晋升到 Old 区,每经历一次 GC 对象的 GC Age 就会增长 1,最大通过
-XX:MaxTenuringThreshold
来控制。过早晋升一般不会直接影响 GC,总会伴随着浮动垃圾、大对象担保失败等问题,但这些问题不是立刻发生的,我们可以观察以下几种现象来判断是否发生了过早晋升:-
分配速率接近于晋升速率 ,对象晋升年龄较小。
GC 日志中出现“Desired survivor size 107347968 bytes, new threshold 1(max 6) ”等信息,说明此时经历过一次 GC 就会放到 Old 区。
-
Full GC 比较频繁 ,且经历过一次 GC 之后 Old 区的 变化比例非常大 。
如Old区触发回收阈值是80%,经历一次GC之后下降到了10%,这说明Old区70%的对象存活时间其实很短。
过早晋升的危害:
- Young GC 频繁,总的吞吐量下降
- Full GC 频繁,可能会有较大停顿
-
-
原因
主要的原因有以下两点:
- Young/Eden 区过小: 过小的直接后果就是 Eden 被装满的时间变短,本应该回收的对象参与了 GC 并晋升,Young GC 采用的是复制算法,由基础篇我们知道 copying 耗时远大于 mark,也就是 Young GC 耗时本质上就是 copy 的时间(CMS 扫描 Card Table 或 G1 扫描 Remember Set 出问题的情况另说),没来及回收的对象增大了回收的代价,所以 Young GC 时间增加,同时又无法快速释放空间,Young GC 次数也跟着增加
- 分配速率过大: 可以观察出问题前后 Mutator 的分配速率,如果有明显波动可以尝试观察网卡流量、存储类中间件慢查询日志等信息,看是否有大量数据被加载到内存中
-
策略
-
如果是 Young/Eden 区过小 ,可以在总的 Heap 内存不变的情况下适当增大Young区。一般情况下Old的大小应当为活跃对象的2~3倍左右,考虑到浮动垃圾问题最好在3倍左右,剩下的都可以分给Young区
-
过早晋升优化来看,原配置为Young 1.2G+Old 2.8G,通过观察CMS GC的情况找到存活对象大概为 300~400M,于是调整Old 1.5G左右,剩下2.5G分给Young 区。仅仅调了一个Young区大小参数(
-Xmn
),整个 JVM 一分钟Young GC从26次降低到了11次,单次时间也没有增加,总的GC时间从1100ms降低到了500ms,CMS GC次数也从40分钟左右一次降低到了7小时30分钟一次:如果是分配速率过大:
- 偶发较大 :通过内存分析工具找到问题代码,从业务逻辑上做一些优化
- 一直较大 :当前的 Collector 已经不满足 Mutator 的期望了,这种情况要么扩容 Mutator 的 VM,要么调整 GC 收集器类型或加大空间
-
-
小结
过早晋升问题一般不会特别明显,但日积月累之后可能会爆发一波收集器退化之类的问题,所以我们还是要提前避免掉的,可以看看自己系统里面是否有这些现象,如果比较匹配的话,可以尝试优化一下。一行代码优化的 ROI 还是很高的。如果在观察 Old 区前后比例变化的过程中,发现可以回收的比例非常小,如从 80% 只回收到了 60%,说明我们大部分对象都是存活的,Old 区的空间可以适当调大些。
场景五:CMS Old GC频繁
-
现象
Old 区频繁的做 CMS GC,但是每次耗时不是特别长,整体最大 STW 也在可接受范围内,但由于 GC 太频繁导致吞吐下降比较多。
-
原因
这种情况比较常见,基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用
shouldConcurrentCollect()
方法做一次检测,判断是否达到了回收条件。如果达到条件,使用collect_in_background()
启动一次 Background 模式 GC。轮询的判断是使用sleepBeforeNextCycle()
方法,间隔周期为-XX:CMSWaitDuration
决定,默认为 2s。 -
策略
处理这种常规内存泄漏问题基本是一个思路,主要步骤如下:
Dump Diff 和 Leak Suspects 比较直观,这里说下其它几个关键点:
- 内存 Dump: 使用 jmap、arthas 等 dump 堆进行快照时记得摘掉流量,同时 分别在 CMS GC 的发生前后分别 dump 一次
- 分析 Top Component: 要记得按照对象、类、类加载器、包等多个维度观察Histogram,同时使用 outgoing和incoming分析关联的对象,其次Soft Reference和Weak Reference、Finalizer 等也要看一下
- 分析 Unreachable: 重点看一下这个,关注下 Shallow 和 Retained 的大小。如下图所示的一次 GC 优化,就根据 Unreachable Objects 发现了 Hystrix 的滑动窗口问题。
场景六:单次CMS Old GC耗时长
-
现象
CMS GC 单次 STW 最大超过 1000ms,不会频繁发生,如下图所示最长达到了 8000ms。某些场景下会引起“雪崩效应”,这种场景非常危险,我们应该尽量避免出现。
-
原因
CMS在回收的过程中,STW的阶段主要是 Init Mark 和 Final Remark 这两个阶段,也是导致CMS Old GC 最多的原因,另外有些情况就是在STW前等待Mutator的线程到达SafePoint也会导致时间过长,但这种情况较少。
-
策略
知道了两个 STW 过程执行流程,我们分析解决就比较简单了,由于大部分问题都出在 Final Remark 过程,这里我们也拿这个场景来举例,主要步骤:
- 【方向】 观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过
-XX:+PrintReferenceGC
参数开启。 基本在日志里面就能定位到大概是哪个方向出了问题,耗时超过 10% 的就需要关注 。
2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs] [class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs]
-
【根因】 有了具体的方向我们就可以进行深入的分析,一般来说最容易出问题的地方就是 Reference 中的 FinalReference 和元数据信息处理中的 scrub symbol table 两个阶段,想要找到具体问题代码就需要内存分析工具 MAT 或 JProfiler 了,注意要 dump 即将开始 CMS GC 的堆。在用 MAT 等工具前也可以先用命令行看下对象 Histogram,有可能直接就能定位问题。
- 对 FinalReference 的分析主要观察
java.lang.ref.Finalizer
对象的 dominator tree,找到泄漏的来源。经常会出现问题的几个点有 Socket 的SocksSocketImpl
、Jersey 的ClientRuntime
、MySQL 的ConnectionImpl
等等 - scrub symbol table 表示清理元数据符号引用耗时,符号引用是 Java 代码被编译成字节码时,方法在 JVM 中的表现形式,生命周期一般与 Class 一致,当
_should_unload_classes
被设置为 true 时在CMSCollector::refProcessingWork()
中与 Class Unload、String Table 一起被处理
- 对 FinalReference 的分析主要观察
-
【策略】 知道 GC 耗时的根因就比较好处理了,这种问题不会大面积同时爆发,不过有很多时候单台 STW 的时间会比较长,如果业务影响比较大,及时摘掉流量,具体后续优化策略如下:
- FinalReference:找到内存来源后通过优化代码的方式来解决,如果短时间无法定位可以增加
-XX:+ParallelRefProcEnabled
对 Reference 进行并行处理 - symbol table:观察 MetaSpace 区的历史使用峰值,以及每次 GC 前后的回收情况,一般没有使用动态类加载或者 DSL 处理等,MetaSpace 的使用率上不会有什么变化,这种情况可以通过
-XX:-CMSClassUnloadingEnabled
来避免 MetaSpace 的处理,JDK8 会默认开启 CMSClassUnloadingEnabled,这会使得 CMS 在 CMS-Remark 阶段尝试进行类的卸载
- FinalReference:找到内存来源后通过优化代码的方式来解决,如果短时间无法定位可以增加
- 【方向】 观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过
-
小结
正常情况进行的 Background CMS GC,出现问题基本都集中在 Reference 和 Class 等元数据处理上,在 Reference 类的问题处理方面,不管是 FinalReference,还是 SoftReference、WeakReference 核心的手段就是找准时机 dump快照,然后用内存分析工具来分析。Class处理方面目前除了关闭类卸载开关,没有太好的方法。在 G1 中同样有 Reference 的问题,可以观察日志中的 Ref Proc,处理方法与 CMS 类似。
场景七:内存碎片&收集器退化
-
现象
并发的 CMS GC 算法,退化为 Foreground 单线程串行 GC 模式,STW 时间超长,有时会长达十几秒。其中 CMS 收集器退化后单线程串行 GC 算法有两种:
- 带压缩动作的算法,称为 MSC,上面我们介绍过,使用标记-清理-压缩,单线程全暂停的方式,对整个堆进行垃圾收集,也就是真正意义上的 Full GC,暂停时间要长于普通 CMS
- 不带压缩动作的算法,收集 Old 区,和普通的 CMS 算法比较相似,暂停时间相对 MSC 算法短一些
-
原因
CMS 发生收集器退化主要有以下几种情况:
-
晋升失败(Promotion Failed)
-
增量收集担保失败
-
显式 GC
-
并发模式失败(Concurrent Mode Failure)
-
-
策略
分析到具体原因后,我们就可以针对性解决了,具体思路还是从根因出发,具体解决策略:
- 内存碎片: 通过配置
-XX:UseCMSCompactAtFullCollection=true
来控制 Full GC 的过程中是否进行空间的整理(默认开启,注意是 Full GC,不是普通 CMS GC),以及-XX: CMSFullGCsBeforeCompaction=n
来控制多少次 Full GC 后进行一次压缩 - 增量收集: 降低触发 CMS GC 的阈值,即参数
-XX:CMSInitiatingOccupancyFraction
的值,让 CMS GC 尽早执行,以保证有足够的连续空间,也减少 Old 区空间的使用大小,另外需要使用-XX:+UseCMSInitiatingOccupancyOnly
来配合使用,不然 JVM 仅在第一次使用设定值,后续则自动调整 - 浮动垃圾: 视情况控制每次晋升对象的大小,或者缩短每次 CMS GC 的时间,必要时可调节 NewRatio 的值。另外使用
-XX:+CMSScavengeBeforeRemark
在过程中提前触发一次Young GC,防止后续晋升过多对象
- 内存碎片: 通过配置
-
小结
正常情况下触发并发模式的 CMS GC,停顿非常短,对业务影响很小,但 CMS GC 退化后,影响会非常大,建议发现一次后就彻底根治。只要能定位到内存碎片、浮动垃圾、增量收集相关等具体产生原因,还是比较好解决的,关于内存碎片这块,如果
-XX:CMSFullGCsBeforeCompaction
的值不好选取的话,可以使用-XX:PrintFLSStatistics
来观察内存碎片率情况,然后再设置具体的值。最后就是在编码的时候也要避免需要连续地址空间的大对象的产生,如过长的字符串,用于存放附件、序列化或反序列化的 byte 数组等,还有就是过早晋升问题尽量在爆发问题前就避免掉。
场景八:堆外内存OOM
-
现象
内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象, 通过 top 命令发现 Java 进程的 RES 甚至超过了
-Xmx
的大小 。出现这些现象时,基本可确定是出现堆外内存泄漏。 -
原因
JVM 的堆外内存泄漏,主要有两种的原因:
- 通过
UnSafe#allocateMemory
,ByteBuffer#allocateDirect
主动申请了堆外内存而没有释放,常见于 NIO、Netty 等相关组件 - 代码中有通过 JNI 调用 Native Code 申请的内存没有释放
- 通过
-
策略
原因一:主动申请未释放
原因二:通过 JNI 调用的 Native Code 申请的内存未释放
场景九:JNI引发的GC问题
-
现象
在 GC 日志中,出现 GC Cause 为 GCLocker Initiated GC。
2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs]2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs]
-
原因
JNI(Java Native Interface)意为 Java 本地调用,它允许 Java 代码和其他语言写的 Native 代码进行交互。JNI 如果需要获取 JVM 中的 String 或者数组,有两种方式:
- 拷贝传递
- 共享引用(指针),性能更高
由于 Native 代码直接使用了 JVM 堆区的指针,如果这时发生 GC,就会导致数据错误。因此,在发生此类 JNI 调用时,禁止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。
-
策略
- 添加
-XX+PrintJNIGCStalls
参数,可以打印出发生 JNI 调用时的线程,进一步分析,找到引发问题的 JNI 调用 - JNI 调用需要谨慎,不一定可以提升性能,反而可能造成 GC 问题
- 升级 JDK 版本到 14,避免 JDK-8048556 导致的重复 GC
- 添加