自学内容网 自学内容网

堆外内存分析方法

优质博文:IT-BLOG-CN

一、背景

陶特在运行时偶尔会报docker OOM,运行一段时间通过free -g查看时,发现java进程占用的内存超过了指定的JVM堆内存。

利用JVM Dump Heap观察堆内情况也是正常的,因此怀疑是堆外内存导致的。

二、pmap

首先使用pmap观察内存情况:

sudo -u deploy pmap 39 | sort -nk2 | less 

可以看到有大量约64M的内存分配:
在这里插入图片描述

这个是glibcfeature 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}(内存地址可以使用pmapless /proc/{pid}/maps查看)
【4】查看内存中的字面量: strings {path}
通过上述步骤,可以看到对应内存地址中的字符串,若有堆外内存泄露,一般和DirectBuffer或者Deflater有关,如果打印的字符串中没有嫌疑对象,可能意味着这块内存没有问题。打印内容示例:
在这里插入图片描述

四、perf

目前用到perf主要是看线程栈调用的情况(perf record && perf report),在内存分析上用的较少。

五、Jemalloc

由于glibcfeature,以及想看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_intervalHeap profiling的间隔,以2为底数的指数,单位为bytes30意味着每分配2^30 bytes = 1 GB就生成一dump文件。
     ☑️lg_prof_sampleHeap allocation采样的精度,以2为底的指数,单位bytes17意味着每分配2^17 bytes = 128 KB就做一次采样,值越高保真度越低,但程序性能越好。
     ☑️prof_prefixHeap 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产生的。

由此得出解决方案:短期内关闭rocksdbstatistics,长期看替换rocksdb
在这里插入图片描述

关闭statistics后的dump analysis
在这里插入图片描述

六、Native Memory Tracking (NMT)

NMTJVM自带的本地内存追踪工具,能够分析JVM自身分配的一些内存,但是无法追踪非JVM分配的内存,例如JNInative code,因此上面由RocksDB导致的内存开销,NMT无法分析。

参数配置
JVM默认不启用NMT,可以在JVM参数中增加-XX:NativeMemoryTracking={mode}参数,mode可以为summarydetaildetail模式会打印出各个类别的内存地址,可以结合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.811项输出结果,含义说明如下:

名称说明相关JVM参数
Unknown其他无法确定的内存
Thread线程占用的内存,与线程数相关-Xss
Symbol常量池、符号表等,与String.intern也有关系-XX:StringTableSize
Native Memory TrackingNMT使用的内存
Java HeapJava堆占用的内存-Xmx;-Xms
Internal除了上述外的其他内存占用,一般和DirectBuffer、JVMTI有关,Unsafe_AllocateMemory
GC垃圾收集器使用的内存,例如G1的CardTable/RememberedSet等
CompilerC1/C2编译器优化代码时使用的内存
CodeJIT生成的代码占用的内存-XX:InitialCodeCacheSize;-XX:ReservedCodeCacheSize
Class元空间占用的内存-XX:MetaspaceSize;-XX:MaxMetaspaceSize
Arena Chunk

Reserved vs Committed vs Resident
NMT输出的内存分为ReservedCommitted两类,使用pmap/top输出的内存有VirtualRssShare几类,说明如下:
1、Resident:驻留集,实际占用物理内存的大小,包括Share的内存
2、Committed:占用的物理内存 + mmap标记为Read/Write的内存,近似于Resident
3、ReservedCommitted + mmap标记为NONE的内存,大于Committed
4、Virtual:虚拟内存空间,JDK8Reserved近似

上述几个内存,一般看ResidentCommitted,其余的没有必要关注。

结果分析
截取同一个进程(32G docker,28G heap)两次NMT输出的结果,第一次为程序点火成功输出后,第二次是19小时后输出的。从图中可以得出几个结论:
【1】启动后GCInternal占用的内存大约2GB
【2】随着时间推进,GCInternal还在膨胀,大约增长了600MB

以上信息和top的结果吻合,程序启动后,RSS差不多在29.6G左右,运行一段时间后RSS达到31G
在这里插入图片描述

NMTdetail输出中,可以看到GCInternal的内存分配堆栈,信息截取如下:

GC:
在这里插入图片描述

主要的内容为保留Object引用关系的CardTable等;

Internal:
在这里插入图片描述

这块的内存占用主要是DirectByteBuffer(Unsafe_AllocateMemory)BitMap。其中DirectByteBuffer主要用在网络传输中,和dubbo调用等有关;BitMap用在GC操作时的关系保留,还不太明白这块的作用。

综上所述,这次发现的oom和代码层面无关,看起来是JVM本身在GC时会有一些内存的增长。此时若其他进程有些内存开销或较大报文的网络传输,可能会触发linuxoom 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
看到JemallocIssue上说Jemalloc从Release 5.x之后就不再内置Valgrind的支持了,Valgrind主要用于分析Memory Leak,没有实际使用过,先Mark下,看看以后是否会用到。


原文地址:https://blog.csdn.net/zhengzhaoyang122/article/details/143839935

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!