最近我们线上有同学反馈,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
Bootstrap 类加载器引发的 Native 内存泄露分析

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

被折叠的 条评论
为什么被折叠?



