JAVA线上问题排查|内存篇

766 阅读9分钟

1 内存溢出区域

image.png

注意,JDK1.7及之前的内存区域有调整,现在通常指的都是1.8及之后的版本

image.png

1.1 栈溢出

java栈空间是线程私有的,一个线程java栈的大小由-Xss参数确定。每个方法执行时都会在java栈空间产生一个栈帧,存放方法的变量表,返回值等信息,方法的执行到结束就是一个栈帧入栈到出栈的过程。 在Java虚拟机规范中,对这个区域规定了两种异常状况:StackOverflowErrorOutOfMemoryError异常。

StackOverflowError异常

线程的方法嵌套调用层次太多(如递归调用),随着java栈中帧的逐渐增多,最终会由于该线程java栈中所有栈帧大小总和大于-Xss设置的值,而产生StackOverflowError内存溢出异常

Exception in thread "main" java.lang.StackOverflowError

OutOfMemoryError异常

java程序代码启动一个新线程时,没有足够的内存空间为该线程分配java栈,jvm则抛出OutOfMemoryError异常

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

相关参数

  • -Xss

1.2 元空间溢出

方法区用于存放java类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。

OutOfMemoryError异常

当需要存储类信息而方法区的内存占用又已经达到MaxMetaspaceSize设置的最大值时会抛出

Caused by: java.lang.OutOfMemoryError: Meta space

相关参数

  • -XX:MetaspaceSize=128m
  • -XX:MaxMetaspaceSize=256m

1.3 堆溢出

Java堆用于储存对象实例。

OutOfMemoryError异常

当需要为对象实例分配内存,而堆的内存占用又已经达到-Xmx设置的最大值

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

相关参数

  • -Xms20m 最小堆内存
  • -Xmx20m 最大堆内存

默认情况下,并不是堆内存耗尽,才会报 OutOfMemoryError,而是如果 JVM 觉得 GC 效率不高,也会报这个错误。

java.lang.OutOfMemoryError:GC overhead limit exceeded

相关参数

  • XX:+UseGCOverheadLimit 开启
  • GCTimeLimit能够设置一个上限,指定 GC 时间所占总时间的百分比。默认值 98%。
  • GCHeapFreeLimit设置了一个下限,它指定了垃圾收集后应该有多大的空闲区域,这是一个相对于堆的总小大的百分比。默认值是2%。

默认情况下,启用了 UseGCOverheadLimit。连续 5 次,碰到 GC 时间占比超过 98%,GC 回收的内存不足 2% 时,会抛出这个异常。

1.3 堆外内存

堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定 如果由于使用 Netty 导致的,可能会报

OutOfDirectMemoryError

如果直接是 DirectByteBuffer

OutOfMemoryError:Direct buffer memory

相关参数

  • -XX:MaxDirectMemorySize=1024m (最大堆外内存)

2 内存泄漏


栈内存由系统自动分配和管理。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题;
堆内存由应用程序自己来分配和管理。 需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存, 就会造成内存泄漏

JVM内存的释放,是由GC决定和执行的。java中的内存泄露,通俗的说,就是不再会被使用的对象的内存不能被回收。在解决内存泄露问题时,堆转储(dump)是最为重要的数据。

2.1 排查步骤

2.1.1 free查看系统内存使用情况

  free -m        

2.1.2 top查看进程CPU、内存占用情况

  top pid        

表现为 CPU占用率高(做大量GC)

2.1.3 jstat分析gc是否正常执行

//每1秒 获取一次 gc 信息
jstat -gcutil pid 1000

确定频繁Full GC现象

2.1.4 jmap查看存活的对象情况

命令

//查看存活对象情况
jmap -histo:live pid

//查看年轻代, 老年代 内存配置 和 使用情况 8.0以前
jmap -heap pid

//8.0以后
jhsdb jmap --heap --pid pid 

输出示例

Attaching to process ID 1474, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.66-b17

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 2147483648 (2048.0MB)
   MaxNewSize               = 2147483648 (2048.0MB)
   OldSize                  = 2147483648 (2048.0MB)
   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 = 1932787712 (1843.25MB)
   used     = 1698208480 (1619.5378112792969MB)
   free     = 234579232 (223.71218872070312MB)
   87.86316621615607% used
Eden Space:
   capacity = 1718091776 (1638.5MB)
   used     = 1690833680 (1612.504653930664MB)
   free     = 27258096 (25.995346069335938MB)
   98.41346682518548% used
From Space:
   capacity = 214695936 (204.75MB)
   used     = 7374800 (7.0331573486328125MB)
   free     = 207321136 (197.7168426513672MB)
   3.4349974840697497% used
To Space:
   capacity = 214695936 (204.75MB)
   used     = 0 (0.0MB)
   free     = 214695936 (204.75MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 2147483648 (2048.0MB)
   used     = 322602776 (307.6579818725586MB)
   free     = 1824880872 (1740.3420181274414MB)
   15.022362396121025% used

29425 interned Strings occupying 3202824 bytes

2.1.5 堆转储分析,定位到代码

获取dump文件

  • 命令获取
jmap dump:live,format=b,file=heap.hprof  pid
  • 自动获取
-XX: +HeapDumpOnOutOfMemoryError 
-XX: HeapDumpPath

当异常抛出时Dump出当前的内存到指定路径

3 分析堆外内存

3.1 堆外内存跟踪 NativeMemoryTracking

Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用情况的一个功能。可以用jcmd这个工具来访问NMT的数据。

NMT必须先通过JVM启动参数中打开,打开NMT会带来5%-10%的性能损耗。

-XX:NativeMemoryTracking=[off | summary | detail]
# off: 默认关闭
# summary: 只统计各个分类的内存使用情况.
# detail: Collect memory usage by individual call sites.

然后运行进程,使用jcmd的命令查看直接内存:

jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
 
# summary: 分类内存使用情况.
# detail: 详细内存使用情况,除了summary信息之外还包含了虚拟内存使用情况。
# baseline: 创建内存使用快照,方便和后面做对比
# summary.diff: 和上一次baseline的summary对比
# detail.diff: 和上一次baseline的detail对比
# shutdown: 关闭NMT

3.2 步骤

3.2.1 添加JVM参数

-XX:NativeMemoryTracking=detail

3.2.2 jcmd显示的内存情况

使用jcmd 命令查看内存

jcmd pid VM.native_memory summary scale=MB

输出

Native Memory Tracking:
Total: reserved=6988749KB, committed=3692013KB
 #堆内存
- Java Heap (reserved=5242880KB, committed=3205008KB)
 (mmap: reserved=5242880KB, committed=3205008KB)
 类加载信息
- Class (reserved=1114618KB, committed=74642KB)
 (classes #10657)
 (malloc=4602KB #32974)
 (mmap: reserved=1110016KB, committed=70040KB)
 线程栈
- Thread (reserved=255213KB, committed=255213KB)
 (thread #248)
 (stack: reserved=253916KB, committed=253916KB)
 (malloc=816KB #1242)
 (arena=481KB #494)
 代码缓存
- Code (reserved=257475KB, committed=46551KB)
 (malloc=7875KB #10417)
 (mmap: reserved=249600KB, committed=38676KB)
 垃圾回收
- GC (reserved=31524KB, committed=23560KB)
 (malloc=17180KB #2113)
 (mmap: reserved=14344KB, committed=6380KB)
 编译器
- Compiler (reserved=598KB, committed=598KB)
 (malloc=467KB #1305)
 (arena=131KB #3)
 内部
- Internal (reserved=6142KB, committed=6142KB)
 (malloc=6110KB #23691)
 (mmap: reserved=32KB, committed=32KB)
 符号
- Symbol (reserved=11269KB, committed=11269KB)
 (malloc=8544KB #89873)
 (arena=2725KB #1)
 nmt
- Native Memory Tracking (reserved=2781KB, committed=2781KB)
 (malloc=199KB #3036)
 (tracking overhead=2582KB)
- Arena Chunk (reserved=194KB, committed=194KB)
 (malloc=194KB)
- Unknown (reserved=66056KB, committed=66056KB)
 (mmap: reserved=66056KB, committed=66056KB)

reserved

reserved memory 是指JVM 通过 mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries),保证了其他进程不会被占用。 在堆内存下,就是xmx值,jvm申请的最大保留内存。

committed

committed memory 是JVM向操坐系统实际分配的内存(malloc/mmap), mmaped PROT_READ | PROT_WRITE,相当于程序实际申请的可用内存。

在堆内存下,就是xms值,最小堆内存,heap committed memory。

注意

当Java程序启动后,会根据Xmx为堆预申请一块保留内存,并不会直接使用,也不会占用物理内存。由于操作系统的内存管理是惰性的,对于已申请(malloc之类的方法)的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存(top命令中的RES)

(1)committed申请的内存并不是说直接占用了物理内存,committed > res也是很可能的。

(2)Java程序GC的回收只是针对Jvm申请的这块内存区域,并不会调用操作系统释放内存。所以该进程的内存并不会释放,这时就会出现进程内存>堆+非堆的情况。

(3)jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存,所以可能发现committed < res

3.2.3 pmap 查看进程内存地址空间

命令

pmap -x pid  

输出

Address           Kbytes     RSS   Dirty Mode   Mapping  
0000000040000000      36       4       0 r-x--  java  
0000000040108000       8       8       8 rwx--  java  
00000000418c9000   13676   13676   13676 rwx--    [ anon ]  
00000006fae00000   83968   83968   83968 rwx--    [ anon ]  
0000000700000000  527168  451636  451636 rwx--    [ anon ]  
00000007202d0000  127040       0       0 -----    [ anon ]  
...  
...  
00007f55ee124000       4       4       0 r-xs-  az.png  
00007fff017ff000       4       4       0 r-x--    [ anon ]  
ffffffffff600000       4       0       0 r-x--    [ anon ]  
----------------  ------  ------  ------  
total kB         7796020 3037264 3023928
  • Address: 内存分配地址

  • Kbytes:   实际分配的内存大小

  • RSS:  程序实际占用的内存大小

  • Mapping: 分配该内存的模块的名称

可以看到很多anon,这些表示这块内存是由mmap分配的。

RSS是Resident Set Size,常驻内存大小,即进程实际占用的物理内存大小。 包含:

  • JVM本身需要的内存,包括其加载的第三方库以及这些库分配的内存

  • NIO的DirectBuffer是分配的native memory

  • 内存映射文件,包括JVM加载的一些JAR和第三方库,以及程序内部用到的。

  • JIT, JVM会将Class编译成native代码,这些内存也不会少,如果使用了Spring的AOP,CGLIB会生成更多的类,JIT的内存开销也会随之变大

  • JNI,一些JNI接口调用的native库也会分配一些内存

  • 线程栈,每个线程都会有自己的栈空间

4 分析工具

  • Eclipse MAT (内存分析工具,Memory Analyzer Tool)是一个社区开发的分析堆转储的工具。它提供了一些很棒的特性,包括:

可疑的泄漏点:它能探测堆转储中可疑的泄露点,报告持续占有大量内存的对象; 直方图:列出每个类的对象数量、浅大小(shallow)以及这些对象所持有的堆。直方图中的对象可以很容易地使用正则表达式进行排序和过滤。这样有助于放大并集中我们怀疑存在泄露的对象。它还能够对比两个堆转储的直方图,展示每个类在实例数量方面的差异。这样能够帮助我们查找 Java 堆中增长最快的对象,并进一步探查确定在堆中持有这些对象的根; 不可达的对象:MAT 有一个非常棒的功能,那就是它允许在它的工作集对象中包含或排除不可达 / 死对象。如果你不想查看不可达的对象,也就是那些会在下一次 GC 周期中收集掉的对象,只关心可达的对象,那么这个特性是非常便利的; 重复的类:展现由多个类加载器所加载的重复的类; 到 GC 根的路径:能够展示到 GC 根(JVM 本身保持存活的对象)的引用链,这些 GC 根负责持有堆中的对象;

  • JProfile