记一次java进程占用内存高,Netty中的buffer一直没被gc的排查

本文详细分析了Elasticsearch在项目部署中导致高内存占用的原因,通过排查进程内存、使用MAT分析、审查ES源码,最终定位问题为未释放的TransportClient连接,提供了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近发现一个项目在部署到机器上的时候,机器内存一直占用很高。

 接着开始排查机器内存高的原因

1、查找机器上占用内存高的进程

ps aux |sort -k4nr|head -20查找占用内存高的前20个进程

发现前20个进程平均每个进程占用内存3g,总共就占用了60g。每个进程占用内存都很高,是造成机器总体内存高的原因。

2、查看进程的内存占用情况

以进程号13588为例,使用jmap -histo:live 13588|head -30查看该java进程中占用内存最高的前30个对象

发现netty内存池中的ByteBuffer一直没有被gc。

 3、使用mat进行内存分析

通过jmap -dump:format=b,file=heapdump.hprof <pid> 把进程的内存占用情况导出来,导入mat做内存分析用。

查看对象的引用情况发现:es的连接没有释放。es底层使用netty通信,netty又使用ByteBuffer池缓存了ByteBuffer

4、查看es的源码

NettyTransport启动通信线程

private ClientBootstrap createClientBootstrap() {

        if (blockingClient) {
            clientBootstrap = new ClientBootstrap(new OioClientSocketChannelFactory(Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_WORKER_THREAD_NAME_PREFIX))));
        } else {
            int bossCount = settings.getAsInt("transport.netty.boss_count", 1);
            clientBootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(
                    Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_BOSS_THREAD_NAME_PREFIX)),
                    bossCount,
                    new NioWorkerPool(Executors.newCachedThreadPool(daemonThreadFactory(settings, TRANSPORT_CLIENT_WORKER_THREAD_NAME_PREFIX)), workerCount),
                    new HashedWheelTimer(daemonThreadFactory(settings, "transport_client_timer"))));
        }
        clientBootstrap.setPipelineFactory(configureClientChannelPipelineFactory());
        clientBootstrap.setOption("connectTimeoutMillis", connectTimeout.millis());

        String tcpNoDelay = settings.get("transport.netty.tcp_no_delay", settings.get(TCP_NO_DELAY, "true"));
        if (!"default".equals(tcpNoDelay)) {
            clientBootstrap.setOption("tcpNoDelay", Booleans.parseBoolean(tcpNoDelay, null));
        }

        String tcpKeepAlive = settings.get("transport.netty.tcp_keep_alive", settings.get(TCP_KEEP_ALIVE, "true"));
        if (!"default".equals(tcpKeepAlive)) {
            clientBootstrap.setOption("keepAlive", Booleans.parseBoolean(tcpKeepAlive, null));
        }

        ByteSizeValue tcpSendBufferSize = settings.getAsBytesSize("transport.netty.tcp_send_buffer_size", settings.getAsBytesSize(TCP_SEND_BUFFER_SIZE, TCP_DEFAULT_SEND_BUFFER_SIZE));
        if (tcpSendBufferSize != null && tcpSendBufferSize.bytes() > 0) {
            clientBootstrap.setOption("sendBufferSize", tcpSendBufferSize.bytes());
        }

        ByteSizeValue tcpReceiveBufferSize = settings.getAsBytesSize("transport.netty.tcp_receive_buffer_size", settings.getAsBytesSize(TCP_RECEIVE_BUFFER_SIZE, TCP_DEFAULT_RECEIVE_BUFFER_SIZE));
        if (tcpReceiveBufferSize != null && tcpReceiveBufferSize.bytes() > 0) {
            clientBootstrap.setOption("receiveBufferSize", tcpReceiveBufferSize.bytes());
        }

        clientBootstrap.setOption("receiveBufferSizePredictorFactory", receiveBufferSizePredictorFactory);

        boolean reuseAddress = settings.getAsBoolean("transport.netty.reuse_address", settings.getAsBoolean(TCP_REUSE_ADDRESS, NetworkUtils.defaultReuseAddress()));
        clientBootstrap.setOption("reuseAddress", reuseAddress);

        return clientBootstrap;
    }

 向上回溯调用createClientBootrap()方法的地方

最后发现是在es的TransportClient中调用。在创建TransportClient时,回启动底层的通信线程。

 由此发现肯定有TransportClient对象常驻在内存中,没有关闭连接。

5、在mat中模糊搜索TransportClient 

 6、在mat中查看TransportClient的对象引用情况

定位到问题代码在EsUtil.java中

 

 7、查找EsUtil的引用

要确定EsUtil是什么时候被引用。只有EsUtil被引用到时,jvm编译器才会寻找这个类,并将这个类加载到AppClassLoader中。只要确保EsUtil在java进程的整个执行过程中不被引用到,jvm编译器就不会初始化EsUtil的静态变量。

 

<think>嗯,我现在遇到了一个问题,就是Linux里运行的Java微服务占用内存RES很,但是生成的内存快照文件却大,分析起来什么价值。这该怎么办呢?让我先理清楚情况。 首先,RES(Resident Set Size)说明进程占用了很多物理内存,但生成的内存快照比如heap dump却大,可能说明堆内存是问题所在。那问题可能出在堆外内存或者JVM的其他区域。这时候需要考虑几个可能性:堆外内存泄漏,比如使用NIO的DirectBuffer或者MappedByteBuffer,或者是JNI调用导致的内存泄漏,或者是JVM自身的内存区域比如Metaspace、线程栈等。 接下来,我应该如何确认是堆外内存的问题呢?可能需要用一些工具来检查。例如,使用pmap命令查看进程内存映射,或者查看/proc/<pid>/smaps文件,分析内存区域的分布。另外,JVM的Native Memory Tracking(NMT)功能可以帮助跟踪JVM内部的内存使用情况,看看是否有异常。 另外,有有可能是一些第三方库或框架在使用堆外内存?比如Netty这样的网络库会使用Direct Buffer,如果配置当或者有正确释放,可能导致内存泄漏。这时候需要检查代码和相关配置,确保资源正确释放。 如果堆内存快照大,那生成快照的时候可能有包含堆外内存的信息,所以需要其他手段。另外,检查JVM参数是否正确,比如MaxDirectMemorySize是否设置合理,或者是否有未限制的地方。 还有,系统级别的内存工具,比如top、htop、vmstat,可以观察内存的使用情况,看看是否有其他进程占用内存,或者系统的缓存是否过过问题集中在特定的Java服务上,所以可能还是该服务自身的问题。 另外,JVM的GC日志分析是否有帮助?看看GC的频率和效果,如果堆内存正常,但GC正常,可能问题在堆内。这时候可能需要更多的监控数据,比如使用jstat查看内存各区域的使用情况,或者开启NMT来跟踪。 总结下来,可能的步骤是: 1. 使用NMT分析JVM内部内存分配。 2. 检查pmap和/proc下的内存信息,找出大块的内存区域。 3. 检查是否有使用堆外内存的代码或库,并确保正确管理。 4. 使用系统工具监控整体内存使用,确认是否存在其他问题。 5. 考虑是否有文件映射、线程创建过多导致栈内存占用等情况。 过具体怎么做,可能需要一步步来验证。比如先开启NMT,然后重启服务,观察内存增长情况,看哪些部分在断增加。或者对比同时间点的pmap输出,找出变化较大的内存区域。同时,检查应用日志,看看是否有异常或频繁的资源申请释放操作。 可能还需要更新JDK版本,因为某些版本的JDK存在内存泄漏的bug。或者检查操作系统和依赖库的版本兼容性。 总之,这个问题需要系统地排查堆外内存的使用情况,结合JVM和操作系统的工具进行分析,找到具体的内存消耗点,然后针对性地优化或修复。</think>针对Java微服务RES内存但堆内存快照无分析价值的问题,可按以下步骤排查和解决: --- ### **一、确认内存分配来源** 1. **区分堆内存与堆外内存** - **堆内存**:通过`jmap`或`jcmd`生成的Heap Dump分析(若Dump文件小,说明堆内存正常)。 - **堆外内存**:常见于以下场景: - Direct Buffer(NIO的`ByteBuffer.allocateDirect()`) - JNI调用(本地库分配的内存) - 内存映射文件(`MappedByteBuffer`) - 元空间(Metaspace)、线程栈、JIT编译代码等。 --- ### **二、使用工具定位堆外内存问题** 1. **Native Memory Tracking (NMT)** - **启用NMT**:启动JVM时添加参数: ```bash -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics ``` - **查看内存统计**: ```bash jcmd <pid> VM.native_memory detail ``` 重点关注`Internal`、`Other`等非堆区域。 2. **pmap分析内存映射** - 查看进程内存分布: ```bash pmap -x <pid> | sort -n -k3 ``` 查找占用大的匿名内存块(如`[anon]`)。 3. **检查/proc/<pid>/smaps** - 分析具体内存段细节: ```bash cat /proc/<pid>/smaps | grep -i rss | awk '{sum+=$2} END {print sum}' ``` 定位占用内存区域(如`mmap`文件映射或线程栈)。 --- ### **三、常见堆外内存泄漏场景** 1. **Direct Buffer泄漏** - **原因**:未正确释放`DirectByteBuffer`(依赖GC触发`Cleaner`)。 - **排查**: - 通过`jcmd <pid> VM.info`查看`DirectByteBuffer`使用量。 - 代码检查:确保`ByteBuffer.allocateDirect()`后调用`((DirectBuffer) buffer).cleaner().clean()`(需谨慎)。 2. **MappedByteBuffer泄漏** - **原因**:未关闭内存映射文件。 - **解决**:通过反射调用`sun.misc.Cleaner`释放(需兼容性处理)。 3. **JNI库泄漏** - **排查**:检查JNI调用的本地代码是否存在未释放内存的操作。 --- ### **四、系统级内存分析** 1. **监控系统内存** - 使用`top`、`htop`、`vmstat`观察整体内存和Swap使用情况。 - 通过`grep -i commit /proc/meminfo`检查系统提交内存总量。 2. **对比进程内存与JVM内存** - **公式**:`RES内存 ≈ 堆内存 + 元空间 + 线程栈 + Direct Buffer + 其他Native内存` - 若`RES`远大于堆内存,需重点排查堆外部分。 --- ### **五、优化与修复** 1. **限制堆外内存** - 设置JVM参数限制Direct Buffer大小: ```bash -XX:MaxDirectMemorySize=256m ``` 2. **调整元空间大小** - 防止Metaspace膨胀: ```bash -XX:MaxMetaspaceSize=256m ``` 3. **减少线程数** - 控制线程栈内存占用(默认1MB/线程): ```bash -Xss256k ``` 4. **升级依赖库** - 如Netty等库可能存在Direct Buffer泄漏的旧版本问题。 --- ### **六、示例操作流程** 1. **步骤1:开启NMT并重启服务** ```bash java -XX:NativeMemoryTracking=detail -jar your-service.jar ``` 2. **步骤2:定期抓取NMT数据** ```bash jcmd <pid> VM.native_memory baseline jcmd <pid> VM.native_memory detail.diff ``` 3. **步骤3:结合pmap定位匿名内存块** ```bash pmap -x <pid> | grep 'anon' | sort -n -k3 ``` --- ### **七、参考资料** - 工具: - Eclipse Memory Analyzer(MAT):分析Heap Dump。 - gdb:调试Native内存(需谨慎使用)。 - 文档: - [Oracle NMT指南](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html) - [Linux pmap手册](https://man7.org/linux/man-pages/man1/pmap.1.html) 通过上述方法,可系统化定位RES的根源,针对性解决堆外内存泄漏或配置问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值