堆外内存分析方法
优质博文:IT-BLOG-CN
一、背景
陶特在运行时偶尔会报docker OOM
,运行一段时间通过free -g
查看时,发现java
进程占用的内存超过了指定的JVM
堆内存。
利用JVM Dump Heap
观察堆内情况也是正常的,因此怀疑是堆外内存导致的。
二、pmap
首先使用pmap
观察内存情况:
sudo -u deploy pmap 39 | sort -nk2 | less
可以看到有大量约64M的内存分配:
这个是glibc
的feature
arena-leak-in-glibc,可以通过调整MALLOC_ARENA_MAX
解决tuning-glibc-memory-behavior。
三、gdb
gdb
可以dump
进程的内存信息查看,知道内存中是什么内容后,可以进一步排查是否存在泄露的情况。排查另一个oom
的问题时,在生产上的机器试验了下,并未查看到有问题的信息,记录下gdb
查看内存的使用方法。
【1】安装gdb: sudo yum install -y gdb
;
【2】attach: sudo -u deploy gdb --pid={pid}
;
【3】dump
内存: dump memory {path} {start_address} {end_address}
(内存地址可以使用pmap
或less /proc/{pid}/maps
查看)
【4】查看内存中的字面量: strings {path}
通过上述步骤,可以看到对应内存地址中的字符串,若有堆外内存泄露,一般和DirectBuffer
或者Deflater
有关,如果打印的字符串中没有嫌疑对象,可能意味着这块内存没有问题。打印内容示例:
四、perf
目前用到perf
主要是看线程栈调用的情况(perf record && perf report)
,在内存分析上用的较少。
五、Jemalloc
由于glibc
的feature
,以及想看Memory Heap
分配情况,搜了一下其他的内存分配器,其中Jemalloc
既能提升分配效率,又能dump heap
,因此首选了Jemalloc
试验。
【1】准备工作
下载Jemalloc
安装相关依赖:sudo yum install -y bzip2 gcc make graphviz
【2】解压编译
在存放Jemalloc
压缩包的目录下执行如下操作,假设jemalloc
的版本为{version}
1、解压:tar -xvf jemalloc-{version}.tar.bz2
2、编译:cd jemalloc-{version} && ./configure --enable-prof && make && sudo make install
3、验证:jeprof --version
【3】JVM
使用Jemalloc
安装好Jemalloc
后,需要在JVM
启动时指定使用Jemalloc
进行内存分配。
指定方式为在JVM
参数中设置如下环境变量(物理机:tomcat/bin/baseenv.sh;docker:tomcat/bin/setenv.sh
):
export LD_PRELOAD=/usr/local/lib/libjemalloc.so
export MALLOC_CONF=prof:true,lg_prof_interval:30,lg_prof_sample:17,prof_prefix:/mnt/mesos/sandbox/{APPID}/jeprof
参数说明
1、LD_PRELOAD
: 指定了在JVM
启动时需要链接的文件。
2、MALLOC_CONF
: 指定了Heap profiling
的参数。
☑️ prof
:是否需要做Heap profiling
。
☑️lg_prof_interval
:Heap profiling
的间隔,以2
为底数的指数,单位为bytes
,30
意味着每分配2^30 bytes = 1 GB
就生成一dump
文件。
☑️lg_prof_sample
:Heap allocation
采样的精度,以2
为底的指数,单位bytes
,17
意味着每分配2^17 bytes = 128 KB
就做一次采样,值越高保真度越低,但程序性能越好。
☑️prof_prefix
:Heap dump
的路径及前缀,需要确保程序对该路径有写入权限,生成的文件名格式为<prefix>.<pid>.<seq>.i<iseq>.heap
。
生成Dump
报告
如果上述步骤进行无误,程序运行后一段时间可以在{prof_prefix}
指定的目录下看到一些名为jeprof.*.heap
的文件。执行如下命令:
sudo -u deploy /usr/local/bin/jeprof --show_bytes --pdf /usr/java/jdk1.8/bin/java /mnt/mesos/sandbox/{APPID}/jeprof.* > ~/output.pdf
执行完成后就可以看到Heap dump
的内容了,通过该内容可以判断是哪里产生了Native Memory
分配。
以陶特的Dump
结果为例,其中rocksdb
分配了超过10%
的内存,主要是由rocksdb.Statistics
产生的。
由此得出解决方案:短期内关闭rocksdb
的statistics
,长期看替换rocksdb
。
关闭statistics
后的dump analysis
:
六、Native Memory Tracking (NMT)
NMT
是JVM
自带的本地内存追踪工具,能够分析JVM
自身分配的一些内存,但是无法追踪非JVM
分配的内存,例如JNI
等native code
,因此上面由RocksDB
导致的内存开销,NMT
无法分析。
参数配置
JVM
默认不启用NMT
,可以在JVM
参数中增加-XX:NativeMemoryTracking={mode}
参数,mode
可以为summary
或detail
,detail
模式会打印出各个类别的内存地址,可以结合pmap
的结果一并分析。本次分析选择detail
参数。
程序启动后,可以通过sudo -u deploy /{JAVA_HOME}/jcmd {pid} VM.native_memory {option}
打印NMT
分析的结果,option
的选项有:
1、summary
:配合mode=summary
使用,输出内存分配的概要信息
2、detail
:配合mode=detail
使用,输出内存分配的概要信息+虚拟内存地址
3、baseline
:记录一次baseline
,用于后续diff
4、summary.diff
:输出与baseline
的概要diff
,必须先执行baseline
5、detail.diff
:输出与baseline
的详情diff
,必须先执行baseline
类别说明
JDK1.8
有11
项输出结果,含义说明如下:
名称 | 说明 | 相关JVM参数 |
---|---|---|
Unknown | 其他无法确定的内存 | |
Thread | 线程占用的内存,与线程数相关 | -Xss |
Symbol | 常量池、符号表等,与String.intern也有关系 | -XX:StringTableSize |
Native Memory Tracking | NMT使用的内存 | |
Java Heap | Java堆占用的内存 | -Xmx;-Xms |
Internal | 除了上述外的其他内存占用,一般和DirectBuffer、JVMTI有关,Unsafe_AllocateMemory | |
GC | 垃圾收集器使用的内存,例如G1的CardTable/RememberedSet等 | |
Compiler | C1/C2编译器优化代码时使用的内存 | |
Code | JIT生成的代码占用的内存 | -XX:InitialCodeCacheSize;-XX:ReservedCodeCacheSize |
Class | 元空间占用的内存 | -XX:MetaspaceSize;-XX:MaxMetaspaceSize |
Arena Chunk |
Reserved vs Committed vs Resident
NMT
输出的内存分为Reserved
和Committed
两类,使用pmap/top
输出的内存有Virtual
、Rss
和Share
几类,说明如下:
1、Resident
:驻留集,实际占用物理内存的大小,包括Share
的内存
2、Committed
:占用的物理内存 + mmap
标记为Read/Write
的内存,近似于Resident
3、Reserved
:Committed + mmap
标记为NONE
的内存,大于Committed
4、Virtual
:虚拟内存空间,JDK8
与Reserved
近似
上述几个内存,一般看Resident
和Committed
,其余的没有必要关注。
结果分析
截取同一个进程(32G docker,28G heap)
两次NMT
输出的结果,第一次为程序点火成功输出后,第二次是19
小时后输出的。从图中可以得出几个结论:
【1】启动后GC
和Internal
占用的内存大约2GB
【2】随着时间推进,GC
和Internal
还在膨胀,大约增长了600MB
以上信息和top
的结果吻合,程序启动后,RSS
差不多在29.6G
左右,运行一段时间后RSS
达到31G
。
从NMT
的detail
输出中,可以看到GC
和Internal
的内存分配堆栈,信息截取如下:
GC:
主要的内容为保留Object
引用关系的CardTable
等;
Internal:
这块的内存占用主要是DirectByteBuffer(Unsafe_AllocateMemory)
和BitMap
。其中DirectByteBuffer主要用在网络传输中,和dubbo
调用等有关;BitMap
用在GC
操作时的关系保留,还不太明白这块的作用。
综上所述,这次发现的oom
和代码层面无关,看起来是JVM
本身在GC
时会有一些内存的增长。此时若其他进程有些内存开销或较大报文的网络传输,可能会触发linux
的oom killer
。解决办法是调小Java
的堆大小?
参考链接
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/nmt-8.html
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr022.html
https://stackoverflow.com/questions/31173374/why-does-a-jvm-report-more-committed-memory-than-the-linux-process-resident-set
Valgrind
Valgrind
看到Jemalloc
的Issue
上说Jemalloc从Release 5.x
之后就不再内置Valgrind
的支持了,Valgrind
主要用于分析Memory Leak
,没有实际使用过,先Mark
下,看看以后是否会用到。
原文地址:https://blog.csdn.net/zhengzhaoyang122/article/details/143839935
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!