
课程咨询: 400-996-5531 / 投诉建议: 400-111-8989
认真做教育 专心促就业
对于每一个java进程来说都有自己的内存池和使用空间,而这也就意味着会出现内存使用错误等问题,而这时候我们就需要对java内存进行诊断分析,今天我们就一起来了就一下,在进行内存诊断上都有哪些软件可以使用。
Java堆:分析诊断数据
堆转储分析
堆转储可以使用如下的工具进行分析:
EclipseMAT(内存分析工具,MemoryAnalyzerTool)是一个社区开发的分析堆转储的工具。它提供了一些很棒的特性,包括:
可疑的泄漏点:它能探测堆转储中可疑的泄露点,报告持续占有大量内存的对象;
直方图:列出每个类的对象数量、浅大小(shallow)以及这些对象所持有的堆。直方图中的对象可以很容易地使用正则表达式进行排序和过滤。这样有助于放大并集中我们怀疑存在泄露的对象。它还能够对比两个堆转储的直方图,展示每个类在实例数量方面的差异。这样能够帮助我们查找Java堆中增长快的对象,并进一步探查确定在堆中持有这些对象的根;
不可达的对象:MAT有一个非常棒的功能,那就是它允许在它的工作集对象中包含或排除不可达/死对象。如果你不想查看不可达的对象,也就是那些会在下一次GC周期中收集掉的对象,只关心可达的对象,那么这个特性是非常便利的;
重复的类:展现由多个类加载器所加载的重复的类;
到GC根的路径:能够展示到GC根(JVM本身保持存活的对象)的引用链,这些GC根负责持有堆中的对象;
OQL:我们可以使用对象查询语言(ObjectQueryLanguage)来探查堆转储中的对象。它丰富了OQL的基础设施,能够编写复杂的查询,帮助我们深入了解转储的内部。
JavaVisualVM:监控、分析和排查Java语言的一站式工具。它可以作为JDK工具的一部分来使用,也可以从GitHub上下载。它所提供的特性之一就是堆转储分析。它能够为正在监控的应用创建堆转储,也可以加载和解析它们。从堆转储中,它可以展现类的直方图、类的实例,也能查找特定实例的GC根;
jhat命令工具(在/bin文件夹中)提供了堆转储分析的功能,它能够在任意的浏览器中展现堆转储中的对象。默认情况下,Web服务器会在7000端口启动。jhat支持范围广泛的预定义查询和对象查询语言,以便于探查堆转储中的对象;
Java任务控制(JavaMissionControl)的JOverflow插件:这是一个实验性的插件,能够让Java任务控制执行简单的堆转储分析并报告哪里可能存在内存浪费;
Yourkit是一个商业的Javaprofiler,它有一个堆转储分析器,具备其他工具所提供的几乎所有特性。除此之外,YourKit还提供了:
可达性的范围(reachabilityscope):它不仅能够列出可达和不可达的对象,还能按照它们的可达性范围显示它们的分布,也就是,强可达、弱/软可达或不可达;
内存探查:YourKit内置了一组全面的查询,而不是使用ad-hoc查询功能,YourKit的查询能够探查内存,查找反模式并为常见的内存问题分析产生原因和提供解决方案。
Java任务控制
Java任务控制可以在JDK的/bin文件夹中找到。启用HeapStatistics功能之后所收集到的飞行记录能够极大地帮助我们解决内存泄露问题。我们可以在Memory->ObjectStatistics中查看对象的分析信息。这个视图将会展现对象的直方图,包括每个对象类型所占据的堆的百分比。它能够展现堆中增长快的对象,在大多数情况下,也就直接对应了内存泄露的对象。
终结器所导致的OutOfMemoryError
滥用终结器(finalizer)可能也会造成OutOfMemoryError。带有终结器的对象(也就是含有finalize()方法)会延迟它们所占有空间的回收。在回收这些实例并释放其堆空间之前,终结器线程(finalizerthread)需要调用它们的finalize()方法。如果终结者线程的处理速度比不上要终结对象的增加速度(添加到终结者队列中以便于调用其finalize()方法)的话,那么即便终结器队列中的对象都有资格进行回收,JVM可能也会出现OutOfMemoryError。因此,非常重要的一点就是确保不要因为大量对象等待(pending)终结而造成内存耗尽。
喜欢|作者PoonamParhar
,译者张卫滨
发布于2018年3月14日.估计阅读时间:74分钟|QCon北京站9折倒计时,人工智能、区块链、大数据、架构等领域海内外先驱实践!讨论
分享到:微博微信FacebookTwitter有道云笔记邮件分享
稍后阅读我的阅读清单
Close亲爱的读者:我们近添加了一些个人消息定制功能,您只需选择感兴趣的技术主题,即可获取重要资讯的邮件和网页通知。
核心要点
排查Java的内存问题可能会非常困难,但是正确的方法和适当的工具能够极大地简化这一过程;
JavaHotSpotJVM会报告各种OutOfMemoryError信息,清晰地理解这些错误信息非常重要,在我们的工具箱中有各种诊断和排查问题的工具,它们能够帮助我们诊断并找到这些问题的根本原因;
在本文中,我们会介绍各种诊断工具,在解决内存问题的时候,它们是非常有用的,包括:
HeapDumpOnOutOfMemoryError和PrintClassHistogramJVM选项
EclipseMAT
JavaVisualVM
JConsole
jhat
YourKit
jmap
jcmd
JavaFlightRecorder和JavaMissionControl
GCLogs
NMT
原生内存泄露探测工具,比如dbx、libumem、valgrind和purify等。
对于一个Java进程来说,会有多个内存池或空间——Java堆、Metaspace、PermGen(在Java8之前的版本中)以及原生堆。
每个内存池都可能会遇到自己的内存问题,比如不正常的内存增加、应用变慢或者内存泄露,每种形式的问题终都会以各自空间OutOfMemoryError的形式体现出来。
在本文中,我们会尝试理解这些OutOfMemoryError错误信息的含义以及分析和解决这些问题要收集哪些诊断数据,另外还会研究一些用来收集和分析数据的工具,它们有助于解决这些内存问题。本文的关注点在于如何处理这些内存问题以及如何在生产环境中避免出现这些问题。
JavaHotSpotVM所报告的OutOfMemoryError信息能够清楚地表明哪块内存区域正在耗尽。接下来,让我们仔细看一下各种OutOfMemoryError信息,理解其含义并探索导致它们出现的原因,后介绍如何排查和解决这些问题。
OutOfMemoryError:JavaHeapSpace
Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspace
atjava.util.Arrays.copyOfRange(UnknownSource)
atjava.lang.String.(UnknownSource)
atjava.io.BufferedReader.readLine(UnknownSource)
atjava.io.BufferedReader.readLine(UnknownSource)
atcom.abc.ABCParser.dump(ABCParser.java:23)
atcom.abc.ABCParser.mainABCParser.java:59)
这个信息表示JVM在Java堆上已经没有空闲的空间,JVM无法继续执行程序了。这种错误常见的原因就是指定的大Java堆空间已经不足以容纳所有的存活对象了。要检查Java堆空间是否足以容纳JVM中所有存活的对象,一种简单的方式就是检查GC日志。
688995.775:[FullGC[PSYoungGen:46400K->0K(471552K)][ParOldGen:1002121K->304673K(1036288K)]1048
521K->304673K(1507840K)[PSPermGen:253230K->253230K(1048576K)],0.3402350secs][Times:user=1.48
sys=0.00,real=0.34secs]
从上面的日志条目我们可以看到在FullGC之后,堆的占用从1GB(1048521K)降低到了305MB(304673K),这意味着分配给堆的1.5GB(1507840K)足以容纳存活的数据集。
现在,我们看一下如下的GC活动:
20.343:[FullGC(Ergonomics)[PSYoungGen:12799K->12799K(14848K)][ParOldGen:33905K->33905K(34304K)]46705K->46705K(49152K),[Metaspace:2921K->2921K(1056768K)],0.4595734secs][Times:user=1.17sys=0.00,real=0.46secs]
......severalFullGCs......
22.640:[FullGC(Ergonomics)[PSYoungGen:12799K->12799K(14848K)][ParOldGen:33911K->33911K(34304K)]46711K->46711K(49152K),[Metaspace:2921K->2921K(1056768K)],0.4648764secs][Times:user=1.11sys=0.00,real=0.46secs]
23.108:[FullGC(Ergonomics)[PSYoungGen:12799K->12799K(14848K)][ParOldGen:33913K->33913K(34304K)]46713K->46713K(49152K),[Metaspace:2921K->2921K(1056768K)],0.4380009secs][Times:user=1.05sys=0.00,real=0.44secs]
23.550:[FullGC(Ergonomics)[PSYoungGen:12799K->12799K(14848K)][ParOldGen:33914K->33914K(34304K)]46714K->46714K(49152K),[Metaspace:2921K->2921K(1056768K)],0.4767477secs][Times:user=1.15sys=0.00,real=0.48secs]
24.029:[FullGC(Ergonomics)[PSYoungGen:12799K->12799K(14848K)][ParOldGen:33915K->33915K(34304K)]46715K->46715K(49152K),[Metaspace:2921K->2921K(1056768K)],0.4191135secs][Times:user=1.12sys=0.00,real=0.42secs]Exceptioninthread"main"java.lang.OutOfMemoryError:GCoverheadlimitexceededatoom.main(oom.java:15)
从转储的“FullGC”频率信息我们可以看到,这里存在多次连续的FullGC,它会试图回收Java堆中的空间,但是堆已经完全满了,GC并没有释放任何空间。这种频率的FullGC会对应用的性能带来负面的影响,会让应用变慢。这个样例表明应用所需的堆超出了指定的Java堆的大小。增加堆的大小会有助于避免fullGC并且能够规避OutOfMemoryError。Java堆的大小可以通过-XmxJVM选项来指定:
java–Xmx1024m–Xms1024mTest
OutOfMemoryError可能也是应用存在内存泄露的一个标志。内存泄露通常难以察觉,尤其是缓慢的内存泄露。如果应用无意间持有了堆中对象的引用,会造成内存的泄露,这会导致对象无法被垃圾回收。随着时间的推移,在堆中这些无意被持有的对象可能会随之增加,终填满整个Java堆空间,导致频繁的垃圾收集,终程序会因为OutOfMemoryError错误而终止。
请注意,好始终启用GC日志,即便在生产环境也如此,在出现内存问题时,这样有助于探测和排查。如下的选项能够用来开启GC日志:
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:
探测内存泄露的一步就是监控应用的存活集合(live-set)。存活集合指的是fullGC之后的Java堆。如果应用达到稳定状态和稳定负载之后,存活集合依然在不断增长,这表明可能会存在内存泄露。堆的使用情况可以通过JavaVisualVM、JavaMissionControl和JConsole这样的工具来进行监控,也可以从GC日志中进行抽取。
Java堆:诊断数据的收集
在这一部分中,我们将会讨论要收集哪些诊断数据以解决Java堆上的OutOfMemoryErrors问题,有些工具能够帮助我们收集所需的诊断数据。
堆转储
在解决内存泄露问题时,堆转储(dump)是为重要的数据。堆转储可以通过jcmd、jmap、JConsole和HeapDumpOnOutOfMemoryErrorJVM配置项来收集,如下所示:
jcmdGC.heap_dumpfilename=heapdump.dmp
jmap-dump:format=b,file=snapshot.jmappid
JConsole工具,使用MbeanHotSpotDiagnostic
-XX:+HeapDumpOnOutOfMemoryError
java-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-Xmx20m-XX:+HeapDumpOnOutOfMemoryErroroom
0.402:[GC(AllocationFailure)[PSYoungGen:5564K->489K(6144K)]5564K->3944K(19968K),0.0196154secs][Times:user=0.05sys=0.00,real=0.02secs]
0.435:[GC(AllocationFailure)[PSYoungGen:6000K->496K(6144K)]9456K->8729K(19968K),0.0257773secs][Times:user=0.05sys=0.00,real=0.03secs]
0.469:[GC(AllocationFailure)[PSYoungGen:5760K->512K(6144K)]13994K->13965K(19968K),0.0282133secs][Times:user=0.05sys=0.00,real=0.03secs]
0.499:[FullGC(Ergonomics)[PSYoungGen:512K->0K(6144K)][ParOldGen:13453K->12173K(13824K)]13965K-
>12173K(19968K),[Metaspace:2922K->2922K(1056768K)],0.6941054secs][Times:user=1.45sys=0.00,real=0.69secs]1.205:[FullGC(Ergonomics)[PSYoungGen:5632K->2559K(6144K)][ParOldGen:12173K->13369K(13824K)]17805K-
>15929K(19968K),[Metaspace:2922K->2922K(1056768K)],0.3933345secs][Times:user=0.69sys=0.00,real=0.39secs]
1.606:[FullGC(Ergonomics)[PSYoungGen:4773K->4743K(6144K)][ParOldGen:13369K->13369K(13824K)]18143K-
>18113K(19968K),[Metaspace:2922K->2922K(1056768K)],0.3009828secs][Times:user=0.72sys=0.00,real=0.30secs]
1.911:[FullGC(AllocationFailure)[PSYoungGen:4743K->4743K(6144K)][ParOldGen:13369K->13357K(13824K)]18113K-
>18101K(19968K),[Metaspace:2922K->2922K(1056768K)],0.6486744secs][Times:user=1.43sys=0.00,real=0.65secs]
java.lang.OutOfMemoryError:Javaheapspace
Dumpingheaptojava_pid26504.hprof...
Heapdumpfilecreated[30451751bytesin0.510secs]Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspace
atjava.util.Arrays.copyOf(Arrays.java:3210)
atjava.util.Arrays.copyOf(Arrays.java:3181)
atjava.util.ArrayList.grow(ArrayList.java:261)
atjava.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
atjava.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
atjava.util.ArrayList.add(ArrayList.java:458)
atoom.main(oom.java:14)
请注意,并行垃圾收集器可能会连续地调用FullGC以便于释放堆上的空间,即便这种尝试的收益很小、堆空间几乎已被充满时,它可能也会这样做。为了避免这种情况的发生,我们可以调节-XX:GCTimeLimit和-XX:GCHeapFreeLimit的值。
GCTimeLimit能够设置一个上限,指定GC时间所占总时间的百分比。它的默认值是98%。减少这个值会降低垃圾收集所允许花费的时间。GCHeapFreeLimit设置了一个下限,它指定了垃圾收集后应该有多大的空闲区域,这是一个相对于堆的总小大的百分比。它的默认值是2%。增加这个值意味着在GC后要回收更大的堆空间。如果五次连续的FullGC都不能保持GC的成本低于GCTimeLimit并且无法释放GCHeapFreeLimit所要求的空间的话,将会抛出OutOfMemoryError。
例如,将GCHeapFreeLimit设置为8%的话,如果连续五次垃圾收集无法回收至少8%的堆空间并且超出了GCTimeLimit设置的值,这样能够帮助垃圾收集器避免连续调用FullGC的情况出现。
堆直方图
有时,我们需要快速查看堆中不断增长的内容是什么,绕过使用内存分析工具收集和分析堆转储的漫长处理路径。堆直方图能够为我们快速展现堆中的对象,并对比这些直方图,帮助我们找到Java堆中增长快的是哪些对象。
-XX:+PrintClassHistogram以及Control+Break
jcmdGC.class_histogramfilename=Myheaphistogram
jmap-histopid
jmap-histocore_file
下面的示例输出显示String、Double、Integer和Object[]的实例占据了Java堆中大多数的空间,并且随着时间的流逝数量在不断增长,这意味着它们可能会导致内存泄露:
Java飞行记录
将飞行记录(FlightRecordings)启用堆分析功能能够帮助我们解决内存泄露的问题,它会展现堆中的对象以及随着时间推移,哪些对象增长快。要启用堆分析功能,你可以使用JavaMissionControl并选中“HeapStatistics”,这个选项可以通过“Window->FlightRecordingTemplateManager”找到,如下所示:
或者手动编辑.jfc文件,将heap-statistics-enabled设置为true。
true
everyChunk
飞行记录可以通过如下的方式来创建:
JVMFlightRecorder选项,比如:
-XX:+UnlockCommercialFeatures-XX:+FlightRecorder
-XX:StartFlightRecording=delay=20s,duration=60s,name=MyRecording,
filename=C:\TEMP\myrecording.jfr,settings=profile
Java诊断命令:jcmd
jcmd7060JFR.startname=MyRecordingsettings=profiledelay=20sduration=2mfilename=c:\TEMP\myrecording.jfr
Java任务控制(JavaMissionControl)
飞行记录器只能帮我们确定哪种类型的对象出现了泄露,但是想要找到是什么原因导致了这些对象泄露,我们还需要堆转储。
Java堆:分析诊断数据
堆转储分析
堆转储可以使用如下的工具进行分析:
EclipseMAT(内存分析工具,MemoryAnalyzerTool)是一个社区开发的分析堆转储的工具。它提供了一些很棒的特性,包括:
可疑的泄漏点:它能探测堆转储中可疑的泄露点,报告持续占有大量内存的对象;
直方图:列出每个类的对象数量、浅大小(shallow)以及这些对象所持有的堆。直方图中的对象可以很容易地使用正则表达式进行排序和过滤。这样有助于放大并集中我们怀疑存在泄露的对象。它还能够对比两个堆转储的直方图,展示每个类在实例数量方面的差异。这样能够帮助我们查找Java堆中增长快的对象,并进一步探查确定在堆中持有这些对象的根;
不可达的对象:MAT有一个非常棒的功能,那就是它允许在它的工作集对象中包含或排除不可达/死对象。如果你不想查看不可达的对象,也就是那些会在下一次GC周期中收集掉的对象,只关心可达的对象,那么这个特性是非常便利的;
重复的类:展现由多个类加载器所加载的重复的类;
到GC根的路径:能够展示到GC根(JVM本身保持存活的对象)的引用链,这些GC根负责持有堆中的对象;
OQL:我们可以使用对象查询语言(ObjectQueryLanguage)来探查堆转储中的对象。它丰富了OQL的基础设施,能够编写复杂的查询,帮助我们深入了解转储的内部。
JavaVisualVM:监控、分析和排查Java语言的一站式工具。它可以作为JDK工具的一部分来使用,也可以从GitHub上下载。它所提供的特性之一就是堆转储分析。它能够为正在监控的应用创建堆转储,也可以加载和解析它们。从堆转储中,它可以展现类的直方图、类的实例,也能查找特定实例的GC根;
jhat命令工具(在/bin文件夹中)提供了堆转储分析的功能,它能够在任意的浏览器中展现堆转储中的对象。默认情况下,Web服务器会在7000端口启动。jhat支持范围广泛的预定义查询和对象查询语言,以便于探查堆转储中的对象;
Java任务控制(JavaMissionControl)的JOverflow插件:这是一个实验性的插件,能够让Java任务控制执行简单的堆转储分析并报告哪里可能存在内存浪费;
Yourkit是一个商业的Javaprofiler,它有一个堆转储分析器,具备其他工具所提供的几乎所有特性。除此之外,YourKit还提供了:
可达性的范围(reachabilityscope):它不仅能够列出可达和不可达的对象,还能按照它们的可达性范围显示它们的分布,也就是,强可达、弱/软可达或不可达;
内存探查:YourKit内置了一组全面的查询,而不是使用ad-hoc查询功能,YourKit的查询能够探查内存,查找反模式并为常见的内存问题分析产生原因和提供解决方案。
我使用EclipseMAT较多,我发现在分析堆转储时,它是非常有用的。
MAT有一些高级的特性,包括直方图以及与其他的直方图进行对比的功能。这样的话,就能清晰地看出内存中哪些内容在增长并且能够看到Java堆中占据空间大的是什么内容。我非常喜欢的一个特性是“MergeShortestPathstoGCRoots(合并到GCRoot的短路径)”,它能够帮助我们查找无意中所持有的对象的跟踪痕迹。比如,在下面的引用链中,ThreadLocalDateFormat对象被ThreadLocalMap$Entry对象的“value”字段所持有。只有当ThreadLocalMap$Entry从ThreadLocalMap中移除之后,ThreadLocalDateFormat才能被回收。
weblogic.work.ExecuteThread@0x6996963a8[ACTIVE]ExecuteThread:'203'forqueue:'weblogic.kernel.Default(self-tuning)'BusyMonitor,Thread|1|176|40|10,536
'-threadLocalsjava.lang.ThreadLocal$ThreadLocalMap@0x69c2b5fe0|1|24|40|7,560
'-tablejava.lang.ThreadLocal$ThreadLocalMap$Entry[256]@0x6a0de2e40|1|1,040|40|7,536
'-[116]java.lang.ThreadLocal$ThreadLocalMap$Entry@0x69c2ba050|1|32|40|1,088
'-valueweblogic.utils.string.ThreadLocalDateFormat@0x69c23c418|1|40|40|1,056
通过这种方式,我们可以看到堆中增长快的罪魁祸,并且看到内存中哪里出现了泄露。
Java任务控制
Java任务控制可以在JDK的/bin文件夹中找到。启用HeapStatistics功能之后所收集到的飞行记录能够极大地帮助我们解决内存泄露问题。我们可以在Memory->ObjectStatistics中查看对象的分析信息。这个视图将会展现对象的直方图,包括每个对象类型所占据的堆的百分比。它能够展现堆中增长快的对象,在大多数情况下,也就直接对应了内存泄露的对象。
终结器所导致的OutOfMemoryError
滥用终结器(finalizer)可能也会造成OutOfMemoryError。带有终结器的对象(也就是含有finalize()方法)会延迟它们所占有空间的回收。在回收这些实例并释放其堆空间之前,终结器线程(finalizerthread)需要调用它们的finalize()方法。如果终结者线程的处理速度比不上要终结对象的增加速度(添加到终结者队列中以便于调用其finalize()方法)的话,那么即便终结器队列中的对象都有资格进行回收,JVM可能也会出现OutOfMemoryError。因此,非常重要的一点就是确保不要因为大量对象等待(pending)终结而造成内存耗尽。
我们可以使用如下的工具来监控等待终结的对象数量:
JConsole
我们可以连接JConsole到一个运行中的进程,然后在VMSummary页面查看等待终结的对象数量,如下图所示。
jmap–finalizerinfo
D:\tests\GC_WeakReferences>jmap-finalizerinfo29456
AttachingtoprocessID29456,pleasewait...
Debuggerattachedsuccessfully.Servercompilerdetected.
JVMversionis25.122-b08
Numberofobjectspendingforfinalization:10
堆转储
几乎所有的堆转储分析工具都能详细给出等待终结的对象信息。
JavaVisualVM的输出
Datetaken:FriJan0614:48:54PST2017
File:D:\tests\java_pid19908.hprof
Filesize:11.3MB
Totalbytes:10,359,516
Totalclasses:466
Totalinstances:105,182
Classloaders:2
GCroots:419
Numberofobjectspendingforfinalization:2
OutOfMemoryError:PermGenSpace
java.lang.OutOfMemoryError:PermGenspace
我们知道,从Java8之后,PermGen已经移除掉了。如果读者运行的是Java8以上的版本,那么这一小节可以直接略过。
在Java7及以前,PermGen(“永久代,permanentgeneration”的缩写)用来存储类定义以及它们的元数据。在这个内存区域中,PermGen意料之外的增长以及OutOfMemoryError意味着类没有按照预期卸载,或者所指定的PermGen空间太小,无法容纳所有要加载的类和它们的元数据。
要确保PermGen的大小能够满足应用的需求,我们需要监控它的使用情况并使用如下的JVM选项进行相应的配置:
–XX:PermSize=n–XX:MaxPermSize=m
OutOfMemoryError:Metaspace
MetaSpace的OutOfMemoryError输出样例如下所示:
java.lang.OutOfMemoryError:Metaspace
从Java8开始,类元数据存储到了Metaspace中。Metaspace并不是Java堆的一部分,它是分配在原生内存上的。所以,它仅仅受到机器可用原生内存数量的限制。但是,Metaspace也可以通过MaxMetaspaceSize参数来设置它的大小。
如果Metaspace的使用接近MaxMetaspaceSize的大限制,那么我们就会遇到OutOfMemoryError。与其他的区域类似,这种错误可能是因为没有足够的Metaspace,或者存在类加载器/类泄露。如果出现了后者的情况,我们需要借助诊断工具,解决Metaspace中的内存泄露。
OutOfMemoryError:Compressedclassspace
java.lang.OutOfMemoryError:Compressedclassspace
如果启用了UseCompressedClassesPointers的话(打开UseCompressedOops的话之后,会默认启用),那么原生内存上会有两个独立的区域用来存储类和它们的元数据。启用UseCompressedClassesPointers之后,64位的类指针会使用32位的值来表示,压缩的类指针会存储在压缩类空间(compressedclassspace)中。默认情况下,压缩类空间的大小是1GB并且可以通过CompressedClassSpaceSize进行配置。
MaxMetaspaceSize能够为这两个区域设置一个总的提交(committed)空间大小,即压缩类空间和类元数据的提交空间。
启用UseCompressedClassesPointers之后,在GC日志中会进行采样输出。在Metaspace所报告的提交和保留(reserved)空间中包含了压缩类空间的提交和预留空间。
Metaspaceused2921K,capacity4486K,committed4864K,reserved1056768K
classspaceused288K,capacity386K,committed512K,reserved1048576K
PermGen和Metaspace:数据收集和分析工具
PermGen和Metaspace所占据的空间可以使用Java任务控制、JavaVisualVM和JConsole进行监控。GC能够帮助我们理解FullGC前后PermGen/Metaspace的使用情况,也能看到是否存在因为PermGen/Metaspace充满而导致的FullGC。
另外非常重要的一点在于确保类按照预期进行了卸载。类的加载和卸载可以通过启用下面的参数来进行跟踪:
-XX:+TraceClassUnloading–XX:+TraceClassLoading
在将应用从开发环境提升到生产环境时,需要注意应用程序有可能会被无意地改变一些JVM可选参数,从而带来不良的后果。其中有个选项就是-Xnoclassgc,它会让JVM在垃圾收集的时候不去卸载类。现在,如果应用需要加载大量的类,或者在运行期有些类变得不可达了,需要加载另外一组新类,应用恰好是在–Xnoclassgc模式下运行的,那么它有可能达到PermGen/Metaspace的大容量,就会出现OutOfMemoryError。因此,如果你不确定这个选项为何要设置的话,那么好将其移除,让垃圾收集器在这些类能够回收的时候将其卸载掉。
加载的类和它们所占用的内存可以通过NativeMemoryTracker(NMT)来进行跟踪。我们将会在下面的“OutOfMemoryError:NativeMemory”小节详细讨论这个工具。
需要注意,在使用并发标记清除收集器(ConcurrentMarkSweepCollector,CMS)时,需要启用如下的选项,从而确保CMS并发收集周期能够将类卸载掉:–XX:+CMSClassUnloadingEnabled
在Java7中,这个标记默认是关闭的,而在Java8中它默认就是启用的。
jmap
“jmap–permstat”会展现类加载器的统计数据,比如类加载器、类加载器所加载的类的数量以及这些类加载已死亡还是尚在存活。它还会告诉我们PermGen中interned字符串的总数,以及所加载的类及其元数据所占用的字节数。如果我们要确定是什么内容占满了PermGen,那么这些信息是非常有用的。如下是一个示例的输出,展现了所有的统计信息。在列表的后一行我们能够看到有一个总数的概述。
$jmap-permstat29620
AttachingtoprocessID29620,pleasewait...
Debuggerattachedsuccessfully.Clientcompilerdetected.
JVMversionis24.85-b06
12674internStringsoccupying1082616bytes.findingclassloaderinstances..
done.computingperloaderstat..done.pleasewait..computingliveness.........................................done.
class_loader classesbytesparent_loaderalive?type
18465321080nulllive
0xd0bf382800 nulllivesun/misc/Launcher$ExtClassLoader@0xd8c98c78
0xd0d2f3701904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0c9928011440 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0b71d90000xd0b5b9c0 live java/util/ResourceBundle$RBClassLoader@0xd8d042e8
0xd0d2f4c01904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0b5bf9819200xd0b5bf38deadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0c992481904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0d2f4881904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0b5bf386118320xd0b5b9c0deadsun/reflect/misc/MethodUtil@0xd8e8e560
0xd0d2f3381904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0d2f4181904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0d2f3a81904 nulldead sun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0b5b9c031713974480xd0bf3828livesun/misc/Launcher$AppClassLoader@0xd8cb83d8
0xd0d2f3001904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0d2f3e01904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0ec396811440 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0e0a2481904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0c992101904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0d2f4501904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0d2f4f81904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
0xd0e0a2801904 nulldeadsun/reflect/DelegatingClassLoader@0xd8c22f50
total=22 21866746816N/Aalive=4,dead=18 N/A
从Java8开始,jmap–clstats命令能够打印类加载器及其存活性的类似信息,不过它所展现的是Metaspace中已加载的类的数量和大小,而不再是PermGen。
jmap-clstats26240
AttachingtoprocessID26240,pleasewait...
Debuggerattachedsuccessfully.Servercompilerdetected.JVMversionis25.66-b00findingclassloaderinstances..done.computingperloaderstat..done.pleasewait..computingliveness.livenessanalysismaybeinaccurate...
class_loader classesbytesparent_loaderalive?type
513950353nulllive
0x0000000084e066d08244160x0000000084e06740livesun/misc/Launcher$AppClassLoader@0x0000000016bef6a0
0x0000000084e0674000nulllivesun/misc/Launcher$ExtClassLoader@0x0000000016befa48
0x0000000084ea18f0000x0000000084e066d0deadjava/util/ResourceBundle$RBClassLoader@0x0000000016c33930
total=4 521 974769N/A alive=3,dead=1 N/A
堆转储
正如我们在前面的章节所提到的,EclipseMAT、jhat、JavaVisualVM、JOverflowJMC插件和Yourkit这些工具都能分析堆转储文件,从而分析排查OutOfMemoryError。在解决PermGen和Metaspace的内存问题时,堆转储同样是有用的。EclipseMAT提供了一个非常好的特性叫做“DuplicateClasses”,它能够列出被不同的类加载实例多次加载的类。由不同的类加载器加载数量有限的重复类可能是应用设计的一部分,但是,如果它们的数量随着时间推移不断增长的话,那么这就是一个危险的信号,需要进行调查。应用服务器托管多个应用时,它们运行在同一个JVM中,如果多次卸载和重新部署应用的话,经常会遇到这种状况。如果被卸载的应用没有释放所有它创建的类加载器的引用,JVM就不能卸载这些类加载器所加载的类,而新部署的应用会使用新的类加载器实例重新加载这些类。
原生堆:诊断工具
让我们看一下内存泄露的探查工具,它们能够帮助我们找到原生内存泄露的原因。
原生内存跟踪
JVM有一个强大的特性叫做原生内存跟踪(NativeMemoryTracking,NMT),它在JVM内部用来跟踪原生内存。需要注意的是,它无法跟踪JVM之外或原生库分配的内存。通过下面两个简单的步骤,我们就可以监控JVM的原生内存使用情况:
以启用NMT的方式启动进程。输出级别可以设置为“summary”或“detail”级别:
-XX:NativeMemoryTracking=summary
-XX:NativeMemoryTracking=detail
使用jcmd来获取原生内存使用的细节:
jcmdVM.native_memory
排查内存问题可能会非常困难和棘手,但是正确的方法和适当的工具能够极大地简化这一过程。我们看到,JavaHotSpotJVM会报告各种OutOfMemoryError信息,清晰地理解这些错误信息非常重要,在工具集中有各种诊断和排查工具,帮助我们诊断和根治这些问题。
作者:Poonam Parhar
译者:张卫滨
来源:infoq
【免责声明】:本内容转载于网络,转载目的在于传递信息。文章内容为作者个人意见,本平台对文中陈述、观点保持中立,不对所包含内容的准确性、可靠性与完整性提供形式地保证。请读者仅作参考。