Google perf 工具Java堆外内存分析

本文档记录了某次线上Java堆外内存问题分析排查的经过,分享了一些使用Google perf工具使用的经验,文章有不足之处欢迎大家指正。

官方文档:https://gperftools.github.io/gperftools/heapprofile.html

1、问题现象

VM是4C8G规格,Java堆内内存使用较小,内存dump出来没有大对象,但Java进程整体使用内存一直往上涨,可能原因是堆外内存分配太多,导致物理内存耗尽,被系统oom killer干掉。

oom killer查看命令:

sudo dmesg | grep -i kill

2、Google perf tools

2.1、工具原理

在Unix操作系统的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,用以指定预先装载的一些共享库或目标文件,且无论程序是否依赖这些共享库或者文件,LD_PRELOAD指定的这些文件都会被装载其优先级比LD_LIBRARY_PATH自定义的进程的共享库查找路径的执行还要早。Google perftools就是利用了LD_PRELOAD,将malloc方法替换成了tcmalloc的实现,这样就可以跟踪内存分配路径。

2.2、源码安装

git clone https://github.com/gperftools/gperftools.git
cd gperftools
./autogen.sh
./configure
make
sudo make install

2.3、使用方式

在java进程的启动脚本添加如下变量:

export LD_PRELOAD="/usr/local/lib/libtcmalloc.so"
export HEAPPROFILE="${HOME}/perf/test"
export HEAP_PROFILE_ALLOCATION_INTERVAL=2147483648

HEAP_PROFILE_ALLOCATION_INTERVAL:指当一块大小为HEAP_PROFILE_ALLOCATION_INTERVAL的内存被申请出来,生成一个新的heap文件。

HEAPPROFILE:存放结果的地址。

2.4、分析结果

程序运行一段时间后,会在 ${HOME}/perf/ 目录生成若干test.xxxx.heap文件,以test.0001.heap为例:

文本分析

cd ${HOME}/perf/test
pprof --text /opt/java/bin/java test.0001.heap
文本分析结果

第一列为以MB为单位的内存分配情况
第二列和第五列是第一列和第四列的百分比表示
第三列是第二列累加之和,如:第二行的第三列就是第一行的第二列+第二行的第二列
第四列代表所有的进程和它调用函数的内存之和

图像分析

sudo apt install ghostscript graphviz -y
pprof --pdf /opt/java/bin/java test.0001.heap > test.0001.pdf
调用链路分析结果

分析结果

allocator_alloc 分配的对外内存较多,根据调用链路来看是netty SSLContext.make native方法分配了较多的对外内存,需要排查对应释放堆外内存的地方。

3、解决方案

经排查代码发现OpenSslContext引用的OpenSSL堆外内存需要在gc的时候触发finalize方法释放,具体释放方法是SSLContext.free,详见代码如下:

public abstract class OpenSslContext extends ReferenceCountedOpenSslContext {
    OpenSslContext(Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apnCfg,
                   long sessionCacheSize, long sessionTimeout, int mode, Certificate[] keyCertChain,
                   ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp)
            throws SSLException {
        super(ciphers, cipherFilter, apnCfg, sessionCacheSize, sessionTimeout, mode, keyCertChain,
                clientAuth, protocols, startTls, enableOcsp, false);
    }

    OpenSslContext(Iterable<String> ciphers, CipherSuiteFilter cipherFilter,
                   OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize,
                   long sessionTimeout, int mode, Certificate[] keyCertChain,
                   ClientAuth clientAuth, String[] protocols, boolean startTls,
                   boolean enableOcsp) throws SSLException {
        super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, mode, keyCertChain, clientAuth, protocols,
                startTls, enableOcsp, false);
    }

    @Override
    final SSLEngine newEngine0(ByteBufAllocator alloc, String peerHost, int peerPort, boolean jdkCompatibilityMode) {
        return new OpenSslEngine(this, alloc, peerHost, peerPort, jdkCompatibilityMode);
    }

    @Override
    @SuppressWarnings("FinalizeDeclaration")
    protected final void finalize() throws Throwable {
        super.finalize();
        OpenSsl.releaseIfNeeded(this);
    }

本案例解决方案有两个:

1、调低堆内内存分配的大小,提前触发gc条件,触发finalize()方法,达到回收堆外内存的目的;
2、手动触发堆外内存回收;

解决效果

优化后结果

堆外内存占比下降。

留下评论