1 内存溢出区域
注意,JDK1.7及之前的内存区域有调整,现在通常指的都是1.8及之后的版本
1.1 栈溢出
java栈空间是线程私有的,一个线程java栈的大小由-Xss
参数确定。每个方法执行时都会在java栈空间产生一个栈帧,存放方法的变量表,返回值等信息,方法的执行到结束就是一个栈帧入栈到出栈的过程。
在Java虚拟机规范中,对这个区域规定了两种异常状况:StackOverflowError
和OutOfMemoryError
异常。
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