Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题
简介: 从 Java Agent 报错开始,到 JVM 原理,到 glibc 线程安全,再到 pthread tls,逐步探究 Java Agent 诡异报错。
作者:鲁严波
从 Java Agent 报错开始,到 JVM 原理,到 glibc 线程安全,再到 pthread tls,逐步探究 Java Agent 诡异报错。
背景
由于阿里云多个产品都提供了 Java Agent 给用户使用,在多个 Java Agent 一起使用的场景下,造成了总体 Java Agent 耗时增加,各个 Agent 各自存储,导致内存占用、资源消耗增加。
MSE 发起了 one-java-agent 项目,能够协同各个 Java Agent;同时也支持更加高效、方便的字节码注入。
其中,各个 Java Agent 作为 one-java-agent 的 plugin,在 premain 阶段是通过多线程启动的方式来加载,从而将启动速度由 O(n)降低到 O(1),降低了整体 Java Agent 整体的加载时间。
问题
但最近在新版 Agent 验证过程中,one-java-agent 的 premain 阶段,发现有如下报错:
2022-06-15 06:22:47 [oneagent plugin arms-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: arms-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/home/admin/.opt/ArmsAgent/plugins/ArmsAgent/arms-bootstrap-1.7.0-SNAPSHOT.jar
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InternalError: null
at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
... 4 common frames omitted
2022-06-16 09:51:09 [oneagent plugin ahas-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: ahas-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/home/admin/.opt/ArmsAgent/plugins/ahas-java-agent/ahas-java-agent.jar
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
at java.lang.Thread.run(Thread.java:855)
Caused by: java.lang.IllegalArgumentException: null
at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
... 4 common frames omitted
熟悉 Java Agent 的同学可能能注意到,这是调用 Instrumentation.appendToSystemClassLoaderSearch 报错了。
但首先 appendToSystemClassLoaderSearch 的路径是存在的;其次,这个报错的真实原因是在 C++部分,比较难排查。
但不管怎样,还是要深究下为什么出现这个错误。
首先我们梳理下具体的调用流程,下面的分析都是基于此来分析的:
- Instrumentation.appendToSystemClassLoaderSearch (java)
- appendToClassLoaderSearch0 (JNI)
`- appendToClassLoaderSearch
|- AddToSystemClassLoaderSearch
| `-create_class_path_zip_entry
| `-stat
`-convertUft8ToPlatformString
`- iconv
打日志、确定现场
因为这个问题在容器环境下,有 10% 的概率出现,比较容易复现,于是就用 dragonwell8 的最新代码,加日志,确认下现场。
首先在 JNI 的实际入口处,也就是 appendToClassLoaderSearch 的方法入口添加日志:

加了上面的日志后,发现问题更加令人头秃了:
- 没有报错的时候,appendToClassLoaderSearch entry 会输出。
- 有报错的时候,appendToClassLoaderSearch entry 反而没有输出,没执行到这儿?
这个和报错的日志对不上啊,难道是 stacktrace 信息骗了我们?
过了难熬的一晚上后,第二天请教了 dragonwell 的同学,大佬打日志的姿势是这样的:
- tty->print_cr("internal error");
- 如果上面用不了,再用 printf("xxx\n");fflush(stdout);
这样加日志后,果然我们的日志都能打出来了。
这是踩的第一个坑,printf 要加上 fflush 才能保证输出成功。
分析代码
后面又是不断加日志,最终发现 create_class_path_zip_entry 返回 NULL。
找不到对应的 jar 文件?
继续排查,发现是 stat 报错,返回 No such file or directory。但是前面也提到了,jarFile 的路径是存在的,难道 stat 不是线程安全的?
查了下文档[1],发现 stat 是线程安全的。
于是又回过头来再看,这时候注意到 stat 的路径是不正常的:有的时候路径是空,有的时候路径是/home/admin/.opt/ArmsAgent/plugins/ahas-java-agent/ahas-java-agent.jarSHOT.jar,从字符末尾可以看到,基本上是因为两个字符写到了同一片内存导致的;而且对应字符串长度也变成了一个不规律的数字了。
那么问题就很明确了,开始查找这个字符串的生成。这个字符是 convertUft8ToPlatformString 生成的。
字符编码转换有问题?
于是开始调试 utf8ToPlatform 的逻辑,这时候为了避免频繁加日志、重启容器,所以直接在 ECS 上运行 gdb 调试 jvm。
结果发现,在 Linux 下,utf8ToPlatform 就是直接 memcpy,而且 memcpy 的目标地址是在栈上。
这怎么看都不太可能有线程安全问题啊?
后来仔细查了下,发现和环境变量有关,ECS 上编码相关的环境变量是 LANG=en_US.UTF-8,在容器上 centos:7 默认没有这个环境变量,此种情况下,jvm 读到的是 ANSI_X3.4-1968。
这儿是第二个坑,环境变量会影响本地编码转换。
结合如上现象和代码,发现在容器环境下,还是要经过 iconv,从 UTF-8 转到 ANSI_X3.4-1968 编码的。
其实,这儿也可以推测出来,如果手动在容器中设置了 LANG=en_US.UTF-8,这个问题就不会再出现。额外的验证也证实了这点。
然后又加日志,最终确认是 iconv 的时候,目标字符串写挂了。
难道是 iconv 线程不安全?
iconv不是线程安全的!
查一下 iconv 的文档,发现它不是完全线程安全的:

通俗的说,iconv 之前,需要先用 iconv_open 打开一个 iconv_t,而且这个 iconv_t,不支持多线程同时使用。
至此,问题已经差不多定位清楚了,因为 jvm 把 iconv_t 写成了全局变量,这样在多个线程 append 的时候,就有可能同时调用 iconv,导致竞态问题。
这儿是第三个坑,iconv 不是线程安全的。
如何修复
先修复 one-java-agent
对于 Java 代码,非常容易修改,只需要加一个锁就可以了:

但是这儿有一个设计问题,instrument 对象已经在代码中到处散落了,现在突然要加一个锁,几乎所有用到的地方都要改,代码改造成本比较大。
于是最终还是通过 proxy 类来解决:

这样其他地方就只需要使用 InstrumentationWrapper 就可以了,也不会触发这个问题。
jvm要不要修复
然后我们分析下 jvm 侧的代码,发现就是因为 iconv_t 不是线程安全的,导致 appendToClassLoaderSearch0 方法不是线程安全的,那能不能优雅的解决掉呢?
如果是 Java 程序,直接用 ThreadLoal 来存储 iconv_t 就能解决了。
但是 cpp 这边,虽然 C++ 11 支持 thread_local,但首先 jdk8 还没用 C++ 11(这个可以参考 JEP );其次,C++ 11 的也仅仅支持 thread_local 的 set 和 get,thread_local 的初始化、销毁等生命周期管理还不支持,比如没办法在线程结束时自动回收 iconv_t 资源。
那咱们就 fallback 到 pthread?因为 pthread 提供了 thread-specific data,可以做类似的事情。
- pthread_key_create 创建 thread-local storage 区域
- pthread_setspecific 用于将值放入 thread-local storage
- pthread_getspecific 用于从 thread-local storage 取出值
- 最重要的,pthread_once 满足了 pthread_key_t 只能初始化一次的需求。
- 另外也需要提到的,pthread_once 的第二个参数,就是线程结束时的回调,我们就可以用它来关闭 iconv_t,避免资源泄漏。
总之 pthread 提供了 thread_local 的全生命周期管理。于是,最终代码如下,用 make_key 初始化 thread-local storage:


于是编译 JDK 之后,打镜像、批量重启数次 pod,就没有再出现文章开头提到的问题了。
总结
在整个过程中,从 Java 到 JNI/JVMTi,再到 glibc,再到 pthread,踩了很多坑:
- printf 要加上 fflush 才能保证输出成功
- 环境变量会影响本地字符编码转换
- iconv 不是线程安全的
- 使用 pthread thread-local storage 来实现线程局部变量的全生命周期管理
从这个案例中,沿着调用栈、代码,逐步还原问题、并提出解决方案,希望大家能对 Java/JVM 多了解一点。
参考链接:
[1] 文档:
https://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html
[2] one-java-agent 修复的链接:
https://github.com/alibaba/one-java-agent/issues/31
[3] dragonwell 修复的链接:
https://github.com/alibaba/dragonwell8/pull/346
[4] one-java-agent 给大家带来了更加方便、无侵入的微服务治理方式:
https://www.aliyun.com/product/aliware/mse
MSE 注册配置中心专业版首购享 9 折优惠,MSE 云原生网关预付费全规格享 85 折优惠。点击“此处”,即刻享受优惠!
Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题的更多相关文章
- java ArrayList 踩坑记录
做编程的一个常识:不要在循环过程中删除元素本身(至少是我个人的原则).否则将发生不可预料的问题. 而最近,看到一个以前的同学写的一段代码就是在循环过程中删除元素,我很是纳闷啊.然后后来决定给他改掉.然 ...
- C#调用java方法踩坑记
首先,我的java代码写了一个遗传算法,这是我硕士毕业论文的核心算法,项目是基于C#的web项目.但是现在又不想用C#重写遗传算法代码,于是就想用C#去调用java的代码.在网上找了方法,一般有两种: ...
- Java 开发中如何正确踩坑
为什么说一个好的员工能顶 100 个普通员工 我们的做法是,要用最好的人.我一直都认为研发本身是很有创造性的,如果人不放松,或不够聪明,都很难做得好.你要找到最好的人,一个好的工程师不是顶10个,是顶 ...
- JAVA实用案例之文件导出(JasperReport踩坑实录)
写在最前面 想想来新公司也快五个月了,恍惚一瞬间. 翻了翻博客,因为太忙,也有将近五个多月没认真总结过了. 正好趁着今天老婆出门团建的机会,记录下最近这段时间遇到的大坑-JasperReport. 六 ...
- 『OGG 02』Win7 配置 Oracle GoldenGate Adapter Java 踩坑指南
上一文章 <__Win7 配置OGG(Oracle GoldenGate).docx>定下了 两个目标: 目标1: 给安装的Oracle_11g 创建 两个用户 admin 和 root ...
- java用毫秒数做日期计算的一个踩坑记录
错误示例: Date today = new Date(); Date nextMonth = new Date(today.getTime() + 30* 1000*60*60*24); print ...
- 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密
你真的了解字典(Dictionary)吗? 从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...
- java 多线程踩过的坑
多线程踩坑记录:1.多线程切记不可以同时操作同一个原子数据.解释:存在一个条数据库A数据,不可以在2个或2个以上的线程中同时操作A数据.会引发重复操作.2.多线程操作方法不要加synchronized ...
- Java踩坑之路
陆陆续续学Java也快一年多了,从开始的一窍不通到现在的初窥门径,我努力过,迷茫过,痛过,乐过,反思过,沉淀过.趁着新年,我希望能把这些东西记下来,就当是我一路走来的脚印. 一.初识网站应用 记得第一 ...
- 了解Java线程优先级,更要知道对应操作系统的优先级,不然会踩坑
Java 多线程系列第 6 篇. 这篇我们来看看 Java 线程的优先级. Java 线程优先级 Thread 类中,使用如下属性来代表优先级. private int priority; 我们可以通 ...
随机推荐
- day05-Lombok、SpringInitializer
Lombok.Spring-Initializer 1.Lombok 1.1Lombok介绍 Lombok的作用是: 简化Javabean的开发,可以使用Lombok的注解让代码更加简洁 Java项目 ...
- Linux常用指令2
1.系统常用命令 1)在文件中查找内容 grep >grep hello passwd //在passwd文件中搜索hello内容,会把hello所在行的内容打印到终端显示 2)查看系统中活跃 ...
- VR虚拟现实技术下的汽车展厅:优劣势及运作方式
虚拟现实汽车展厅其实是一种在线商店,可让客户在模拟环境中体验产品.这对无法亲自到店的人很有帮助.客户可以使用虚拟现实耳机来探索可用的不同型号和颜色.这可以帮助他们就购买哪辆汽车做出更明智的决定.虚拟现 ...
- 记录--vue3问题:如何实现微信扫码授权登录?
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.需求 微信扫码授权,如果允许授权,则登录成功,跳转到首页. 二.问题 1.微信扫码授权有几种实现方式? 2.说一下这几种实现方式的原理 ...
- WPF MVVM 集合内容更改时如何添加操作,触发通知
用过mvvm设计WFP程序的人都知道,在我们mvvm中有一个非常重要的接口叫做 INotifyPropertyChanged 这个接口的主要作用是用于触发属性更改时向我们xaml中绑定此属性值的控件发 ...
- 用免费GPU部署自己的stable-diffusion项目(AI生成图片)
2021年时出现了 openAI 的 DALL,但是不开源.2022年一开年,DALL-E 2发布,依然不开源.同年7月,Google 公布其 Text-to-Image 模型 Imagen,并且几乎 ...
- 2024-03-23:用go语言,一张桌子上总共有 n 个硬币 栈 。每个栈有 正整数 个带面值的硬币, 每一次操作中,你可以从任意一个栈的 顶部 取出 1 个硬币,从栈中移除它,并放入你的钱包里。
2024-03-23:用go语言,一张桌子上总共有 n 个硬币 栈 .每个栈有 正整数 个带面值的硬币, 每一次操作中,你可以从任意一个栈的 顶部 取出 1 个硬币,从栈中移除它,并放入你的钱包里. ...
- SpringBoot3集成PostgreSQL
标签:PostgreSQL.Druid.Mybatis.Plus: 一.简介 PostgreSQL是一个功能强大的开源数据库系统,具有可靠性.稳定性.数据一致性等特点,且可以运行在所有主流操作系统上, ...
- C# 使用AForge调用摄像头
AForge官网地址:http://www.aforgenet.com/framework/ using System; using System.Collections.Generic; using ...
- copy 导入包含特殊符号的文本
客户提供了一份数据记录需要导入数据库,但是文本中有一个列的内容是反斜杠"\" ,因为""是特殊的转义字符,需要使用两个"\"才能表示,如果直 ...