【Java核心-进阶】JVM 内存监控与诊断

1. 获取 JVM 内存信息的方法

1.1 综合性图形化工具

如:JConsole、VisualVM(GitHub)、 VisualVM(JDK tool)、Java Mission Control(JMC)等

 

其中,JFR(Java Flight Recorder)+ JMC 算是针对生产环境中查看 JVM 信息的典型方法之一。

具体操作就是:

  1. 先让Java进程开启JFR特性运行一段时间,从而将收集到的JVM信息导出到指定的 jfr 文件中

    在JVM启动参数中添加“-XX:+UnlockCommercialFeatures -XX:+FlightRecorder”即可开启JFR特性。

    可以在启动参数中指定数据收集时长与导出文件

     

    -XX:StartFlightRecording=duration=60s,filename=my-app.jfr

     

    也可以在Java进程运行一段时间后,再通过 jcmd 开启数据收集并指定导出文件

     

    jcmd $PID JFR.start duration=60s filename=my-app.jfr

     

  2. 再通过 JMC 导入上述 jfr 文件,在JMC中查看分析相关数据

    包括CPU、内存、线程、IO、各种事件及其它操作系统信息等

1.2 命令行工具

如:jstat、jmap、jstack 等

 

1.2.1 jstat

如:通过命令 “jstat -gc 49379 3000 5” 查看进程号为49379的Java进程GC相关的堆统计信息,每个3000毫秒统计一次,共统计5次

 

[root@localhost bin]# jstat -gc 49379 3000 5
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
17472.0 17472.0  0.0   1576.0 139968.0 10169.5   349568.0   40525.3   72408.0 70025.4 8704.0 8172.9     19    2.869   5      0.468    3.337
17472.0 17472.0  0.0   1576.0 139968.0 10417.3   349568.0   40525.3   72408.0 70025.4 8704.0 8172.9     19    2.869   5      0.468    3.337
17472.0 17472.0  0.0   1576.0 139968.0 10956.6   349568.0   40525.3   72408.0 70025.4 8704.0 8172.9     19    2.869   5      0.468    3.337
17472.0 17472.0  0.0   1576.0 139968.0 11661.5   349568.0   40525.3   72408.0 70025.4 8704.0 8172.9     19    2.869   5      0.468    3.337
17472.0 17472.0  0.0   1576.0 139968.0 12173.4   349568.0   40525.3   72408.0 70025.4 8704.0 8172.9     19    2.869   5      0.468    3.337

【Java核心-进阶】JVM 内存监控与诊断_第1张图片
 

1.2.2 jmap

如:

  • 通过命令 “jmap -heap 49379” 查看进程号为49379的Java进程堆信息

 

[root@localhost bin]# jmap -heap 49379
Attaching to process ID 49379, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.152-b16

using thread-local object allocation.
Mark Sweep Compact GC

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 178913280 (170.625MB)
   MaxNewSize               = 357892096 (341.3125MB)
   OldSize                  = 357957632 (341.375MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB) # 元数据区
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 161218560 (153.75MB)
   used     = 130447904 (124.40481567382812MB)
   free     = 30770656 (29.345184326171875MB)
   80.91370125127033% used
Eden Space:
   capacity = 143327232 (136.6875MB)
   used     = 123462536 (117.74304962158203MB)
   free     = 19864696 (18.94445037841797MB)
   86.14031979631058% used
From Space:
   capacity = 17891328 (17.0625MB)
   used     = 6985368 (6.661766052246094MB)
   free     = 10905960 (10.400733947753906MB)
   39.04331752232143% used
To Space:
   capacity = 17891328 (17.0625MB)
   used     = 0 (0.0MB)
   free     = 17891328 (17.0625MB)
   0.0% used
tenured generation: # 老年代
   capacity = 357957632 (341.375MB)
   used     = 57555232 (54.888946533203125MB)
   free     = 300402400 (286.4860534667969MB)
   16.07878331254577% used

32415 interned Strings occupying 3713496 bytes.

 

  • 通过命令 “jmap -dump:format=b,file=dump.bin 49379” 将进程号为49379的Java进程的堆信息导出到文件 dump.bin 中;
    • 再将导出的文件导入到VirtualVM之类的图形化工具中查看详细信息;
    • 或通过 jhat 加载导出的文件进行查看(不过远没有图形化工具友好实用)

 

[root@localhost bin]# jmap -dump:format=b,file=dump.bin 49379
Dumping heap to /opt/dump.bin ...
Heap dump file created
 

1.2.3 jstack

如:通过命令 “jstack -l 49379” 查看进程号为49379的Java进程的栈信息,包括与锁相关的额外信息

[root@localhost bin]# jstack -l 49379
...
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f0a0c07a800 nid=0xc0e7 in Object.wait() [0x00007f0a11632000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x00000000d578d6d0> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

   Locked ownable synchronizers:
	- None

"VM Thread" os_prio=0 tid=0x00007f0a0c073000 nid=0xc0e6 runnable 

"VM Periodic Task Thread" os_prio=0 tid=0x00007f0a0c0be800 nid=0xc0ed waiting on condition 

JNI global references: 557

 

1.3 其它工具

NMT(Native Memory Tracking)

注:JVM开启对NMT的支持会导致性能有所下降。

Native Memory Tracking 写道
Enable NMT using the following command line. Note that enabling this will cause 5-10% performance overhead.

 

java.lang.instrument

如果配合 Javaassist 可以更方便地从字节码层面操控Java程序,获取更多样化的信息

 

2. OOM诊断

除了 程序计数器(PC),其它 JVM内存区 都有可能发生 OutOfMemeryError

 

2.1 OOM种类含义

出现OOM意味着:

  • GC已经无法提供足够的内存存放新对象,堆的容量也无法扩展
  • 或本地内存无法提供足够的空间载入Java类
  • 极少数情况下,可能是因为GC持续了很长时间但几乎没有内存被释放

诊断OOM的第一步是定位其来源。然后再根据具体的错误信息结合程序代码得出真正的解决方案。

因为只知哪片内存区出现OOM而直接增大该区域空间往往抓不住问题根源,得不出真正的解决方案。

 

2.1.1 Java heap space

java.lang.OutOfMemoryError:Java heap space

具体可能原因:

  • 内存泄漏
  • 堆大小设置不合理
  • JVM处理引用不及时,导致内存无法释放

2.1.2 GC Overhead limit exceeded

java.lang.OutOfMemoryError: GC Overhead limit exceeded

Oracle的官方解释是当JVM花了绝大部分时间去GC但回收的内存太少时会抛出该类型的OOM;并给出了一个宽泛的解决方法——增加堆内存。

实际使用场景中,还是需要根据详细的错误信息结合程序的代码进一步分析,再得出真正的解决方案。因为可能是程序逻辑的问题导致创建太多对象(如,在死循环内创建对象)。

《Understand the OutOfMemoryError Exception》 写道
After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown.

 

2.1.3 Requested array size exceeds VM limit

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

程序试图创建一个数组时,该数组的大小超过了剩余可用(连续空间)的堆大小。

出现这种情况可能是堆设置得太小了。也有可能是程序在选择数组容量大小的逻辑有问题,特别是当该容量是通过某种算法动态决定的情况。

 

2.1.4 Metaspace

java.lang.OutOfMemoryError: Metaspace

可通过JVM参数 MaxMetaSpaceSize 设置元数据区的最大容量

永久代 被 元数据区 替代后,“方法区”出现OOM的情况会少一些。因为元数据区空间默认会自增

 

2.1.5 request size bytes for reason. Out of swap space

java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space

Oracle的官方解释是,本地内存(native heap)不够用导致的。

为了找到深层原因,可能得分析相关错误日志;同时可以结合其它操作系统运维工具进行排查。

错误日志的具体路径可以通过JVM启动参数配置。如:-XX:ErrorFile=/var/log/java/java_error.log

 

2.1.6 Compressed class space

java.lang.OutOfMemoryError: Compressed class space

可通过JVM启动参数配置增大相应的内存区域。如:-XX:CompressedClassSpaceSize=2g

 

2.1.7 reason stack_trace_with_native_method

java.lang.OutOfMemoryError: reason stack_trace_with_native_method

一般出现该错误时,线程正在执行一个本地方法。也就是说执行本地方法时内存不足。

可以通过操作系统运维工具进一步排查。

 

 

2.1.8 其它

Java虚拟机栈 和 本地方法栈

  • 过深的递归调用导致栈空间不足时,一般抛出 StackOverFlowError,栈溢出
  • 当 JVM试图扩展栈空间失败时,抛出 OOM

老版本的永久代内存区

  • 错误信息:java.lang.OutOfMemoryError:PermGen space
  • 永久代大小有限,且GC不积极,在某些场景下容易抛 OOM。如:
    • 运行时大量生成动态类型
    • Intern 字符串缓存过多

3. 内存泄漏诊断

诊断内存泄漏是非常繁琐的事情,需要长时间尝试Java程序的运行场景并监视内存对象状态。

可以利用前述的各种工具协助排查。

如,查看待回收的对象数:

  • 可以通过 JConsole等图形化工具
  • 也可以通过 “jmap -finalizerinfo [pid]”

如果条件允许,可供选择的方式/工具非常多,甚至可以自己开发小工具。

java.lang.management、java.lang.instrument、java.lang.Thread、JVM Tool Interface、JPDA(Java Platform Debugger Architecture)等都可以尝试。

 

思考

当试图分配一个 100MB 的数组时发生 OOM,但GC日志显示可用堆空间大于 100MB,可能是什么原因?

 

总体思路

  • 分配一个数组需要一块连续的内存空间
  • GC日志显示的可用空间大小是总大小,而这些可用空间可能是由不连续的多个空间合成的;如果这多个空间中没有一个是大于100MB的,就会抛出OOM

具体可能情况

  • 如果如果新生代的GC机制是 serial,会默认使用 copying 算法;当遇到大对象时会直接将大对象放到老年代中
    • 此时,如果老年代的GC机制是 serial old,则会使用 mark compact 方式,试图在老年代中整理出一块足够大的连续空间。如果获取空间失败,则OOM
    • 如果,此时老年代的GC机制是 CMS (Concurrent Mark Sweep),老年代的内存区只会被标记清理,不会被“压缩”整理,也就会出现碎片化。这样即使老年代的可用空间就已大于100MB,也可能出现OOM
  • 如果采用了G1收集垃圾,那就可能是没有多个连续的 region 内存之和超过100MB

你可能感兴趣的:(Java)