通过JMX控制在full GC前后做heap dump

后一篇:[url=http://rednaxelafx.iteye.com/blog/1049240]通过jinfo工具在full GC前后做heap dump[/url]

有时候我们想知道一个Java程序在一次full GC的时候到底回收了哪些对象。特别是当full GC看起来很频密但系统看起来却又没有内存泄漏的时候,了解究竟是哪些对象引致了这些GC会对调优有帮助。

做了个简单的例子,讲解一种简单的办法在full GC的前后得到heap dump。本文说的办法只能在HotSpot VM上使用;其它JVM要达到同样的目的或许有其它做法,回头有机会再说。
(同样的工作在JRockit或者J9上做似乎都更容易些… :oops: )

======================================================================

[size=small][b]一般获取heap dump的办法[/b][/size]

[b]1、[url=http://download.oracle.com/javase/6/docs/technotes/tools/share/jmap.html]jmap[/url][/b]
大家最熟悉的办法或许就是JDK自带的命令行工具jmap了。jmap可以在任何时候连接到一个跑在HotSpot VM的Java进程上,根据需要制作[url=http://hg.openjdk.java.net/jdk6/jdk6/jdk/file/tip/src/share/demo/jvmti/hprof/manual.html]HPROF格式[/url]的heap dump。
jmap -dump:format=b,file=<filename> <pid>

这是最常用的用法。

在Sun的JDK 5和JDK 1.4.2的后期版本中(JDK 5 update 17和JDK 1.4.2 update 16或更高版本),还可以在启动参数里加上[url=http://blogs.oracle.com/poonam/entry/heap_dump_using_xx_heapdumponctrlbreak][b]-XX:+HeapDumpOnCtrlBreak[/b][/url],然后通过[b]ctrl + break[/b]或者发生SIGQUIT来让VM生成heap dump。
不过这个参数在Sun JDK 6里不存在;JDK6上直接用jmap更方便些,倒也没关系。
[url=http://download.oracle.com/docs/cd/E15289_01/doc.40/e15062/optionxx.htm]JRockit R28[/url]倒是支持使用这个参数。

[b]2、[url=http://download.oracle.com/javase/6/docs/technotes/tools/share/jconsole.html]JConsole[/url]、[url=http://visualvm.java.net/]VisualVM[/url]、[url=www.eclipse.org/mat/]MAT[/url][/b]
这几个工具都封装了heap dump的功能,用起来很方便——只要知道如何让这些工具连接到目标进程上。所以它们通常在本地使用很方便,而要连接远程进程就麻烦一些。
还有别的一些工具也有提供生成heap dump功能的,不过我一下想不起来了就只写了上面仨。
[url=http://www.ibm.com/developerworks/java/jdk/tools/gcmv/]GCMV[/url]或许也可以吧…呃,刚看了下,不行。还是得在VM里配置参数来生成heap dump。

JConsole:
[img]http://dl.iteye.com/upload/attachment/483670/70f7efd1-2cc7-3d12-8396-64bcfae7b5fe.png[/img]

VisualVM:
[img]http://dl.iteye.com/upload/attachment/483666/ca7a42b9-a69f-34ae-b7de-a00d5d0e0829.png[/img]
[img]http://dl.iteye.com/upload/attachment/483668/9ee1e212-93c2-3cb4-98f8-e27af4d2904f.png[/img]

Eclipse Memory Analyzer (MAT):
[img]http://dl.iteye.com/upload/attachment/483673/f1140895-7062-312c-a5cd-55dd39a48446.png[/img]

[b]3、JMX的API[/b]
Sun JDK通过JMX暴露出HotSpotDiagnosticMXBean,可以用于获取VM信息。它支持dumpHeap(String outputFile, boolean live)操作,让Java程序能直接指定路径和是否只要活对象进行heap dump。使用方法可以参考下面的链接:[url=http://blogs.oracle.com/sundararajan/entry/programmatically_dumping_heap_from_java]A. Sundararajan's Weblog: Programmatically dumping heap from Java applications[/url]

通过Serviceability Agent API也可以做heap dump。事实上jmap的其中一个模式就是包装了SA API的sun.jvm.hotspot.tools.HeapDumper来完成功能的。

[b]4、JVMTI[/b]
很老的版本的JVMTI API里曾经有过heap dump函数,[url=http://java.sun.com/developer/technicalArticles/Programming/jvmti/]不过后来被去掉了[/url]。

[b]5、让JVM在一些特定事件发生的时候自动做heap dump[/b]
(这就是HotSpot操作起来没有JRockit和J9方便的地方了…)
有时候我们只想在发生OutOfMemoryError的时候让JVM自动生成一个heap dump出来,以便做事后分析。这种时候设置启动参数[b]-XX:+HeapDumpOnOutOfMemoryError[/b]即可。参考下面的文章来了解该参数的一些历史:
[url=http://blogs.oracle.com/alanb/entry/heap_dumps_are_back_with]Alan Bateman: Heap dumps are back with a vengeance![/url]

HotSpot VM支持其它事件触发heap dump么?参考[url=http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html]官方文档[/url]:
[quote]Flags marked as manageable are dynamically writeable through the JDK management interface (com.sun.management.HotSpotDiagnosticMXBean API) and also through JConsole. In Monitoring and Managing Java SE 6 Platform Applications, Figure 3 shows an example. The manageable flags can also be set through jinfo -flag.[/quote]
声明为manageable的参数可以在运行时通过JMX修改。与heap dump相关的有以下4个参数:
[url=http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/tip/src/share/vm/runtime/globals.hpp]hotspot/src/share/vm/runtime/globals.[/url]hpp
manageable(bool, HeapDumpBeforeFullGC, false,                             \
"Dump heap to file before any major stop-world GC") \
\
manageable(bool, HeapDumpAfterFullGC, false, \
"Dump heap to file after any major stop-world GC") \
\
manageable(bool, HeapDumpOnOutOfMemoryError, false, \
"Dump heap to file when java.lang.OutOfMemoryError is thrown") \
\
manageable(ccstr, HeapDumpPath, NULL, \
"When HeapDumpOnOutOfMemoryError is on, the path (filename or" \
"directory) of the dump file (defaults to java_pid<pid>.hprof" \
"in the working directory)") \

可以看到,除了HeapDumpOnOutOfMemoryError之外,还有[b]HeapDumpBeforeFullGC[/b]与[b]HeapDumpAfterFullGC[/b]参数,分别用于指定在full GC之前与之后生成heap dump。

顺带一提,前面VisualVM的截图里“Disable Heap Dump on OOME”的功能,就是通过HotSpotDiagnosticMXBean将HeapDumpOnOutOfMemoryError参数设置为false来实现的。

======================================================================

[size=small][b]通过JMX API在full GC前后生成heap dump的例子[/b][/size]

原始代码放在这里了:[url]https://gist.github.com/978336[/url]

很简单,就是演示了:
·获取HotSpotDiagnosticMXBean;
·通过它上面的setVMOption(String name, String value)方法修改[b]HeapDumpBeforeFullGC[/b]与[b]HeapDumpAfterFullGC[/b]参数为true;
·触发一次full GC;
·将VM参数恢复为false。

为了方便,例子用Groovy来写。要在Groovy Shell中看到GC的日志,可以设置环境变量JAVA_OPTIONS=-XX:+PrintGCDetails,或者是在当前目录放一个.hotspotrc来配置这个参数;我是用的后者。

具体代码:
$ groovysh
[GC [PSYoungGen: 14016K->1312K(16320K)] 14016K->1312K(53696K), 0.0111510 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 15328K->2272K(30336K)] 15328K->4832K(67712K), 0.0286280 secs] [Times: user=0.02 sys=0.03, real=0.03 secs]
Groovy Shell (1.7.7, JVM: 1.6.0_25)
Type 'help' or '\h' for help.
----------------------------------------------------------------------------------------------------------------------------
groovy:000> import java.lang.management.ManagementFactory
===> [import java.lang.management.ManagementFactory]
groovy:000> import com.sun.management.HotSpotDiagnosticMXBean
===> [import java.lang.management.ManagementFactory, import com.sun.management.HotSpotDiagnosticMXBean]
groovy:000>
groovy:000> HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic"
===> com.sun.management:type=HotSpotDiagnostic
groovy:000> server = ManagementFactory.platformMBeanServer
[GC [PSYoungGen: 30304K->2288K(30336K)] 32864K->8856K(67712K), 0.0643130 secs] [Times: user=0.16 sys=0.01, real=0.07 secs]
===> com.sun.jmx.mbeanserver.JmxMBeanServer@7297e3a5
groovy:000> bean = ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_BEAN_NAME, HotSpotDiagnosticMXBean)
===> MXBeanProxy(com.sun.jmx.mbeanserver.JmxMBeanServer@7297e3a5[com.sun.management:type=HotSpotDiagnostic])
groovy:000> bean.setVMOption('HeapDumpBeforeFullGC', 'true')
===> null
groovy:000> bean.setVMOption('HeapDumpAfterFullGC', 'true')
===> null
groovy:000> System.gc()
[GC [PSYoungGen: 10460K->2288K(58368K)] 17028K->9639K(95744K), 0.0166920 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
[Heap Dump: Dumping heap to java_pid16836.hprof ...
Heap dump file created [20066598 bytes in 0.347 secs]
, 0.3514030 secs][Full GC (System) [PSYoungGen: 2288K->0K(58368K)] [PSOldGen: 7351K->9621K(37376K)] 9639K->9621K(95744K) [PSPermGen: 18626K->18626K(37824K)], 0.1324840 secs] [Times: user=0.12 sys=0.02, real=0.14 secs]
[Heap DumpDumping heap to java_pid16836.hprof.1 ...
Heap dump file created [20013677 bytes in 0.340 secs]
, 0.3398950 secs]===> null
groovy:000> bean.setVMOption('HeapDumpBeforeFullGC', 'false')
===> null
groovy:000> bean.setVMOption('HeapDumpAfterFullGC', 'false')
===> null
groovy:000> quit
Heap
PSYoungGen total 58368K, used 9250K [0x00000000edc00000, 0x00000000f1740000, 0x0000000100000000)
eden space 56064K, 16% used [0x00000000edc00000,0x00000000ee5089b0,0x00000000f12c0000)
from space 2304K, 0% used [0x00000000f1500000,0x00000000f1500000,0x00000000f1740000)
to space 2304K, 0% used [0x00000000f12c0000,0x00000000f12c0000,0x00000000f1500000)
PSOldGen total 37376K, used 9621K [0x00000000c9400000, 0x00000000cb880000, 0x00000000edc00000)
object space 37376K, 25% used [0x00000000c9400000,0x00000000c9d65410,0x00000000cb880000)
PSPermGen total 37824K, used 18758K [0x00000000c4200000, 0x00000000c66f0000, 0x00000000c9400000)
object space 37824K, 49% used [0x00000000c4200000,0x00000000c5451ba8,0x00000000c66f0000)

这样就得到了 java_pid16836.hprof 与 java_pid16836.hprof.1 两个heap dump文件。

把第二个heap dump文件改名为 java_pid16836.1.hprof 之后,用MAT打开这两个heap dump,在第一个文件的histogram试图下可以看到
[img]http://dl.iteye.com/upload/attachment/483662/b0321b6a-4ded-30c8-8591-cf2a8e9b70da.png[/img]

目前MAT只支持histogram试图中比较两个heap dump。
点击上方工具条最右边的“<->”按钮,并选上第二个heap dump文件之后,可以看到:
[img]http://dl.iteye.com/upload/attachment/483664/91d433e8-b4e9-3939-9fb8-2c2f85be7e19.png[/img]

这样就能很方便的得知这次full GC当中到底收集了多少个什么类型的对象。
实际效果跟手动用jmap -histo比较差不多,不过要精确的在full GC前后手动做些操作不是件简单的事情。

或许会有人想说,为啥MAT不能直接把具体是哪些对象被收集了显示出来呢?
这功能不好做。GC的时候对象可能会被移动,也就是说不能通过地址来将full GC前后的两个heap dump里的记录关联到一起;而HPROF格式也没有记录足够信息让多个heap dump之间能建立起联系。
结果能很方便做比较的就只有按类型做的统计。通常这也能提供有用的头绪去进一步做分析了。

P.S. 如果一个HPROF的heap dump是在开了压缩指针的64位JVM上生成的,那么用MAT查看的时候,里面显示的Shallow Heap和Retained Heap数据都会是错误的。因为HPROF格式只能分辨是32位还是64位的,却没有记录有没有开压缩指针、每个对象实际的大小是多少。这种条件下请不要相信MAT(或其它分析HPROF格式的heap dump的工具)显示的对象大小。
<think>我们讨论的是瞬时Full GC计数,即短时间内发生的Full GC次数。在JVM中,Full GC是清理整个堆内存(包括年轻代、老年代和元空间)的垃圾回收过程,通常会导致较长的停顿时间。 关于“正常范围”,实际上并没有一个固定的标准值,因为正常的Full GC频率取决于具体的应用、负载、内存配置以及垃圾回收器的选择。但是,我们可以从以下几个角度来评估: 1. **频率**:在理想情况下,一个健康的应用应该很少发生Full GC。例如,一天几次或者更少。如果每小时发生多次Full GC,则通常表明存在问题。 2. **时间间隔**:瞬时Full GC指的是在短时间内(例如几分钟甚至几秒钟内)连续发生多次Full GC。这种情况通常是不正常的,可能表明内存泄漏、内存配置不当或者存在大对象分配问题。 然而,我们可以给出一些经验性的参考: - **正常情况**:在应用启动后,随着类加载可能会触发几次Full GC(尤其是元空间的调整),之后在稳定运行期,Full GC应该很少。例如,在低峰期,可能几小时甚至几天才发生一次Full GC。 - **警戒线**:如果在一分钟内发生多次Full GC(例如2次以上),或者连续多个监控周期(如每5秒监控一次)都观察到Full GC,那么就需要立即调查。 为了监控瞬时Full GC,我们可以使用以下方法: **1. 使用JVM内置的MXBean监控Full GC次数(Java代码示例)** 以下代码演示了如何通过JMX获取Full GC的次数,并计算在固定时间间隔内的增量(即瞬时Full GC次数)。 ```java import javax.management.*; import java.lang.management.*; import java.util.*; public class FullGCMonitor { private static long lastFullGCCount = 0; public static void main(String[] args) throws InterruptedException { // 初始化 List<GarbageCollectorMXBean> gcMxBeans = ManagementFactory.getGarbageCollectorMXBeans(); // 记录初始的Full GC次数 lastFullGCCount = getTotalFullGCCount(gcMxBeans); // 模拟持续监控,每5秒检查一次 while (true) { Thread.sleep(5000); // 5秒 long currentFullGCCount = getTotalFullGCCount(gcMxBeans); long delta = currentFullGCCount - lastFullGCCount; lastFullGCCount = currentFullGCCount; if (delta > 0) { System.out.println("Warning: Detected " + delta + " Full GC(s) in the last 5 seconds."); // 这里可以添加告警逻辑,如发送邮件、短信等 } } } private static long getTotalFullGCCount(List<GarbageCollectorMXBean> gcMxBeans) { long total = 0; // 通常,老年代GCFull GC的收集器名称包含"Old", "Full", "MarkSweep"等 for (GarbageCollectorMXBean gcMxBean : gcMxBeans) { String name = gcMxBean.getName(); if (name.contains("Old") || name.contains("Full") || name.contains("MarkSweep")) { total += gcMxBean.getCollectionCount(); } } return total; } } ``` **代码解释**: - 通过`ManagementFactory.getGarbageCollectorMXBeans()`获取所有垃圾收集器的MXBean。 - 在`getTotalFullGCCount`方法中,我们遍历这些MXBean,并只选择那些名称中包含“Old”、“Full”或“MarkSweep”的收集器(这些通常代表老年代或Full GC的收集器),然后累加它们的回收次数。 - 在main方法中,每5秒检查一次,计算距离上次检查的增量(即在这5秒内发生的Full GC次数)。如果增量大于0,则输出警告。 **2. 使用JVM参数记录GC日志** 在启动JVM时添加以下参数,可以记录详细的GC日志,便于后续分析: ``` -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:<path_to_gc_log_file> ``` 然后可以使用工具(如GCViewer)来分析GC日志,观察Full GC发生的频率和原因。 **3. 监控工具** 使用如jstat、VisualVM、JConsole等实时监控工具,观察GC活动。 **结论**: 瞬时Full GC计数的正常范围应该是0(在监控周期内没有发生Full GC)。如果在一个较短的监控周期(如5秒)内发生了1次或多次Full GC,则属于异常情况,需要立即关注。 异常情况可能的原因: - 内存泄漏导致老年代对象不断增加,最终触发Full GC。 - 大量大对象直接分配到老年代,导致老年代空间不足。 - 年轻代过小,导致对象过早晋升到老年代。 - 元空间不足(如果Full GC是由于元空间触发)。 因此,当监控到瞬时Full GC频繁发生时,应该结合GC日志和内存分析工具(如jmap、MAT)来进一步定位问题。 §§相关问题§§: 1. 如何通过分析GC日志来定位Full GC频繁发生的原因? 2. 在Java中,有哪些常见的内存泄漏场景,如何避免? 3. 如何调整JVM参数以减少Full GC的发生? 4. 不同的垃圾回收器(如Parallel、CMS、G1)在Full GC行为上有何不同?</think>### 瞬时 Full GC 计数的正常范围分析 在 JVM 性能监控中,瞬时 Full GC 计数(即短时间内发生的 Full GC 次数)的正常范围取决于应用类型、负载和 JVM 配置。以下是关键判断标准: #### 正常范围参考值 | 应用类型 | 正常范围 (5分钟窗口) | 警戒阈值 | 异常场景 | |------------------------|----------------------|---------------|----------------------| | 低负载 Web 服务 | 0-1 次 | ≥2 次 | 内存泄漏/配置不当 | | 高并发交易系统 | 0-3 次 | ≥4 次 | 大对象分配/代际失衡 | | 批处理作业 | 每批次 0-1 次 | ≥2 次/批次 | 数据处理逻辑缺陷 | | 实时流处理 | 0 次 | ≥1 次 | 背压/序列化问题 | #### 监控与判断代码实现 ```java import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; public class FullGCHealthMonitor { // 记录上次检查时的Full GC计数 private static final AtomicLong lastFullGCCount = new AtomicLong(0); // 异常计数器 private static final AtomicLong alertCount = new AtomicLong(0); // 监控间隔(秒) private static final long MONITOR_INTERVAL = 60; public static void main(String[] args) throws InterruptedException { // 初始化基准值 lastFullGCCount.set(getCurrentFullGCCount()); while (true) { TimeUnit.SECONDS.sleep(MONITOR_INTERVAL); checkFullGCHealth(); } } private static void checkFullGCHealth() { long currentCount = getCurrentFullGCCount(); long delta = currentCount - lastFullGCCount.getAndSet(currentCount); // 判断健康状态 if (delta == 0) { System.out.println("✅ 健康: 过去 " + MONITOR_INTERVAL + " 秒无 Full GC"); } else if (delta <= 2) { System.out.println("⚠️ 注意: 过去 " + MONITOR_INTERVAL + " 秒发生 " + delta + " 次 Full GC"); } else { long totalAlerts = alertCount.incrementAndGet(); System.err.println("❌ 严重: 过去 " + MONITOR_INTERVAL + " 秒发生 " + delta + " 次 Full GC!"); System.err.println("累计异常次数: " + totalAlerts); // 触发诊断操作 if (totalAlerts % 3 == 0) { triggerDiagnostics(); } } } private static long getCurrentFullGCCount() { return ManagementFactory.getGarbageCollectorMXBeans().stream() .filter(bean -> bean.getName().matches(".*(MarkSweep|Old|Full).*")) .mapToLong(GarbageCollectorMXBean::getCollectionCount) .sum(); } private static void triggerDiagnostics() { System.out.println("🚀 执行诊断操作..."); // 实际场景中可执行: // 1. 生成堆转储: jcmd <pid> GC.heap_dump // 2. 捕获线程快照 // 3. 记录JVM统计信息 } } ``` #### 代码关键逻辑解释: 1. **基准值设置**:启动时获取初始 Full GC 计数 2. **增量计算**:通过比较当和上次计数的差值确定瞬时 Full GC 次数 3. **三级健康判断**: - 绿色(0次):理想状态 - 黄色(1-2次):需要关注但非紧急 - 红色(3+次):立即需要干预 4. **智能诊断触发**:累计3次异常后自动触发诊断流程 #### 不同 GC 算法的正常表现 | GC 算法 | 正常 Full GC 特征 | 异常信号 | |------------|-------------------------------------------|-----------------------------| | Parallel | 批次作业后偶发,持续时间稳定 | 频率增加或时间延长 >30% | | CMS | 应极少发生,由并发模式失败触发 | 任何常规 Full GC 都是异常 | | G1 | 应避免 Full GC,由疏散失败触发 | 发生即表示严重问题 | | ZGC/Shenandoah | 理论上永不发生 Full GC | 发生即表示 JVM 或硬件故障 | #### 健康配置建议 1. **Full GC 持续时间**:单次应 < 1秒(大堆 < 3秒) 2. **发生间隔**:生产环境应 > 30分钟 3. **内存回收效率**:每次 Full GC 应回收 >70% 老年代空间 4. **系统影响**:Full GC 导致的应用暂停时间应 < 总运行时间的 0.1% > **黄金法则**:对于现代 JVMJDK11+),理想状态是 **零 Full GC**。任何瞬时 Full GC 都应视为需要调查的事件,尤其在 G1 或 ZGC 等现代收集器中。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值