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

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

最近我们线上有同学反馈,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 注入 rocketmq 消费相关的逻辑,发现 agent 中的一个 matches 方法抛出了 IllegalStateException 异常,提示找不到这个类型 org.apache.rocketmq.client

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值