JVM 之调优及常见场景分析
GC 调优是最后要做的工作,GC 调优的目的可以总结为下面两点:
减少对象晋升到老年代的数量
减少 FullGC 的执行时间
通过监控排查问题及验证优化结果,可以分为:
命令监控:jps、jinfo、jstack、jmap、jstat、jhat
图形化监控: JConsole 和 VisualVM
阿里巴巴开源的 Java 诊断工具: Arthas(阿尔萨斯) :
如果 GC 执行时间满足下列所有条件,就没有必要进行 GC 优化了:
Minor GC 执行非常迅速(50ms 以内)
Minor GC 没有频繁执行(大约 10s 执行一次)
Full GC 执行非常迅速(1s 以内)
Full GC 没有频繁执行(大约 10min 执行一次)
案例参考:
CMS 调优
OOM 问题调优
Java 中 9 种常见的 CMS GC 问题分析与解决
常见场景分析
==========
动态扩容引起的空间震荡
===============
现象
======
服务?刚刚启动时 GC 次数较多?,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示:
原因分析
========
在 JVM 的参数中?-Xms?和?-Xmx?设置的不一致,在初始化时只会初始?-Xms?大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。另外,如果空间剩余很多时也会进行缩容操作,JVM 通过?-XX:MinHeapFreeRatio?和?-XX:MaxHeapFreeRatio?来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。
解决方案
========
尽量?将成对出现的空间大小配置参数设置成固定的?,如?-Xms?和?-Xmx?,?-XX:MaxNewSize?和?-XX:NewSize?,?-XX:MetaSpaceSize?和?-XX:MaxMetaSpaceSize?等。不过在不追求停顿时间的情况下震荡的空间也是有利的,可以动态地伸缩以节省空间,例如作为富客户端的 Java 应用。
显式 GC 的去和留
============
现象
======
手动调用 System.gc 方法会引发一次 STW 的 Full GC,对整个堆做收集,可以在 GC 日志中的 GC Cause 中确认。同时 JVM 提供?-XX:+DisableExplicitGC?参数可以避免这种 GC。那么有没有必要启用该参数呢?
去留分析
========
首先需要了解下?DirectByteBuffer?,它有着零拷贝等特点,被 Netty 等各种 NIO 框架使用,会使用到堆外内存。它的 Native Memory 的清理工作是通过?sun.misc.Cleaner?自动完成的,是一种基于虚引用 PhantomReference 的清理工具,比普通的 Finalizer 轻量些。而为 DirectByteBuffer 分配空间过程中会显式调用 System.gc ,希望通过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory。
如果通过?-XX:+DisableExplicitGC?关闭显式 GC,DirectByteBuffer 分配空间中 System.gc 将失效,这时如果很长一段时间没有做过 GC 或者只做了 Young GC,则不会触发 Cleaner 的工作,Native Memory 得不到及时释放,有可能发生内存泄漏。
所以一般建议保留显式 GC,但需要规范使用,避免频繁 GC 带来的性能开销。可通过?-XX:+ExplicitGCInvokesConcurrent?和?-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses?参数来讲 System.gc 的触发类型从 Foreground 改为 Background,同时 Background 也会做 Reference Processing,这样的话就能大幅降低了 STW 开销,同时也不会发生 NIO Direct Memory OOM。
MetaSpace 区 OOM
===================
现象
======
JVM 在启动后或者某个时间点开始, MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决 。
原因分析
========
Java 7 之前字符串常量池被放到了 Perm 区,所有被 intern 的 String 都会被存在这里,由于 String.intern 是不受控的,所以?-XX:MaxPermSize?的值也不太好设置,经常会出现?java.lang.OutOfMemoryError: PermGen space?异常。但在 Java 7 之后常量池等字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等几项被移到 Heap 中,PermGen 也被移除,取而代之的是 MetaSpace。在最底层,JVM 通过 mmap 接口向操作系统申请内存映射,每次申请 2MB 空间,这里是虚拟内存映射,不是真的就消耗了内存的 2MB,只有之后在使用的时候才会真的消耗内存。申请的这些内存放到一个链表中 VirtualSpaceList,作为其中的一个 Node。
关键原因就是 ClassLoader 不停地在内存中 load 了新的 Class ,一般这种问题都发生在动态类加载等情况上。
解决方案
========
dump 快照之后通过 JProfiler 或 MAT 观察 Classes 的 Histogram(直方图)即可,或者直接通过命令即可定位, jcmd 打几次 Histogram 地图,看一下具体是哪个包下的 Class 增加较多就可以定位了。
jcmd <PID> GC.class_stats|awk '{print$13}'|sed 's/(.).(.)/\1/g'|sort |uniq -c|sort -nrk1
经常会出问题的几个点有 Orika 的 classMap、JSON 的 ASMSerializer、Groovy 动态加载类等,基本都集中在反射、Javasisit 字节码增强、CGLIB 动态代理、OSGi 自定义类加载器等的技术点上。
过早晋升
========
现象
======
分配速率接近于晋升速率?,对象晋升年龄较小
Full GC 比较频繁?,且经历过一次 GC 之后 Old 区的?变化比例非常大
原因分析及策略
===========
Young/Eden 区过小?:一般情况下 Old 的大小应当为活跃对象的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区
分配速率过大?:偶发较大?:通过内存分析工具找到问题代码,从业务逻辑上做一些优化一直较大?:当前的 Collector 已经不满足应用程序的期望了,这种情况要么增加应用程序的 机器,要么调整 GC 收集器类型或加大空间
CMS Old GC 频繁
================
现象
======
Old 区频繁地做 CMS GC,但是每次耗时不是特别长,整体最大 STW 也在可接受范围内,但由于 GC 太频繁导致吞吐下降比较多。
原因分析
========
基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用?shouldConcurrentCollect()?方法做一次检测,判断是否达到了回收条件。如果达到条件(参考上文中 CMS GC 触发条件),使用?collect_in_background()?启动一次 Background 模式 GC。轮询的判断是使用?sleepBeforeNextCycle()?方法,间隔周期为?-XX:CMSWaitDuration?决定,默认为 2s。
解决方案
========
Dump Diff:分别在 CMS GC 的发生前后分别 dump 一次,进行 dump 文件差异分析
Leak Suspects:内存泄露报告
Top Component 分析:按照对象、类、类加载器、包等多个维度观察 Histogram,同时使用 outgoing 和 incoming 分析关联的对象,另外就是 Soft Reference 和 Weak Reference、Finalizer 等也要看一下
Unreachable 分析:不可达对象分析
单次 CMS Old GC 耗时长
=====================
现象
======
CMS GC 单次 STW 最大超过 1000ms,不会频繁发生。但这种场景非常危险,某些场景下会引起“雪崩效应”,我们应该尽量避免出现。
原因分析
========
可能造成 STW 的情况如下:
Init Mark 整个过程比较简单,从 GC Root 出发标记 Old 中的对象,处理完成后借助 BitMap 处理下 Young 区对 Old 区的引用,整个过程基本都比较快,很少会有较大的停顿。
Final MarkFinal Remark 的开始阶段与 Init Mark 处理的流程相同,但是后续多了 Card Table 遍历、Reference 实例的清理,并将其加入到 Reference 维护的?pend_list?中,如果要收集元数据信息,还要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等组件中不再使用的资源。
STW 前等待应用线程到达安全点(较少发生)
由此可见,大部分问题都出在 Final Remark 过程,观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过?-XX:+PrintReferenceGC?参数开启。 基本在日志里面就能定位到大概是哪个方向出了问题,耗时超过 10% 的就需要关注 。
一般来说最容易出问题的地方就是 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 一起被处理。
评论