一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

本文详细分析了一次由于 Bootstrap ClassLoader 加载类导致的 Native 内存泄露问题。在接入预发 javaagent 后,由于特定异常处理触发 Log4j2 的类加载逻辑,导致 native 内存不断增长。问题源于 RocketMQ 相关类的加载失败,由于 agent 的 jar 被加入 Bootstrap 类加载器路径,而依赖的 rocketmq-client.jar 未包含在内。解决方案包括调整 JVM 参数和优化日志配置。
摘要由CSDN通过智能技术生成

最近我们线上有同学反馈,java 服务在接入了支持预发的 javaagent 以后会出现缓存的内存增长,去掉 agent 启动以后内存增长正常。于是分析了一下这个问题,写了这篇文章。

备注:JVM 堆内存最大 1000M

主要会涉及下面这些内容:

  • JVM native 内存分析的通用方法

  • JVM Bootstrap ClassLoader 源码分析

  • gdb 的一些调试技巧

  • bytebuddy 打破双亲委派的类加载器

  • 不好好干好日志的本分,处处恶心第一名的 log4j2 是如何处理错误堆栈的

背景介绍

线上全链路预发支持不能只支持 http 接口,还得支持 dubbo rpc、rocketmq、httpclient 等。

  • http:针对 http 调用,都会添加一个流量标识的 header:x-ccloud-pre (1-预发流量 2-正式流量),可以支持okhttp、okhttp3、httpclient 4.x、Spring RestTemplate

  • dubbo:预发实例都会增加一个 dubbo.provider.group=gray 的参数,通过 group 来区分正式/预发的 provider,agent 内部实现根据流量标识来过滤 provider 的逻辑,并且通过 attachment 把流量标识往下游服务透传。

  • RocketMQ:生产者只向本环境的 MQ 投递消息,消费者只消费本环境 MQ,原理就是根据是否是预发流量,如果是预发请求则将要投递的 topic 修改为预发的 topic。

以上实现都是通过一个 javaagent 来实现的,以实现业务方零代码修改接入。

分析过程

首先确认过不是堆内存的问题,因此需要用上 NativeMemoryTracking 来分析 native 内存,于是加上 -XX:NativeMemoryTracking=detail。但是只有这一个工具还不够,还需要结合 pmap、tcmalloc、jemalloc 来分析。因为内存增长缓慢,这里开启了一个后台定期执行 pmap 的脚本,用来分析内存增长在那一块内存区域内,然后放在哪里跑一晚上。

while true
do
        sleep 900
        name=`date +"%Y_%m_%d_%H_%M_%S"`
        echo $name
        pmap -x 72 > "pmap_$name.out"
done

通过 diff 对比分析,找到了内存缓存增长的区域,随后 dump 出这一块内存,dump 的方式可以通过 gdb,也可以通过读取 /proc/$pid/maps 的方式来实现。

通过 dump 出来的内存,首先通过 strings 命令看看里面有没有认识的字符串。

​很快发现里面有很多我们熟悉的类,比如: com.cvte.psd.pr.agent.rocketmq.consumer.push.RocketMqListenerOrderlyWrapperCreator$RocketListenerOrderlyWrapper,这个内部类实现了org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly接口,请记住这个接口,后面会频繁出现。

RocketListenerOrderlyWrapper 做的事情也很简单,就是对 mq 消息处理进行了代理处理。RocketListenerOrderlyWrapper 实现如下:

import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;

public class RocketMqListenerOrderlyWrapperCreator implements PreReleaseWrapperCreator<MessageListenerOrderly> {

    public static class RocketListenerOrderlyWrapper implements MessageListenerOrderly {
        private MessageListenerOrderly originListener;
        private PreReleaseManager preReleaseManager;
        public RocketListenerOrderlyWrapper(MessageListenerOrderly originListener, PreReleaseManager preReleaseManager) {
            this.originListener = originListener;
        }

        @Override
        public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
            // ...
        }
    }
}

这个类就是用来包装 RocketMQ 的消息,来实现预发特性的。

上面 dump 出来的内容到底都是字符串,还是 class 文件的常量池的一部分呢?通过 16 进制分析工具可以进一步分析。把上面的 dump 文件导入到 010 Editor(www.sweetscape.com/010editor/ )中,搜索 java 字节码的魔数(0xCAFEBABE),可以看这个这段内存中有 2.6W 个 class 文件。

​可以删掉第一个 0xCAFEBABE 前面的字节,把剩下的文件当做 class 文件解析。

​为什么会有这么多类文件出现在 native 内存中呢?通过 nmt 可以进一步辅助分析。这里可以看到类加载相关的内存 malloc 有 597M 左右,虽然 malloc 不代表真实的使用量(可能 malloc 以后不写,或者用完 free),这个值这么大还是不太正常。

接下来用 arthas 注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值