这里有几个问题要明确。
already loaded in another classloader
首先在jvm本身的限制里同一个位置的so文件,只能由一个ClassLoader加载。如果不同的ClassLoader加载,就会抛出 already loaded in another classloader
参考: https://github.com/alibaba/arthas/issues/961
arthas里的办法是,每次load so时,先复制到一个临时文件里。 https://github.com/alibaba/arthas/issues/1796 。这样子就绕过了jvm的限制。
这个从原理上做一个猜测,不一定对:
- so加载到jvm里,实际上是要用mmap的方式,比如全局变量什么的就会在内存里有一份对应的地址
- 如果多个classloader加载同一个位置的so,那么就会出现内存共享,就很容易出错了。
- 所以jvm不允许同一个文件的so被加载多次
- 如果复制到不同的文件,那么mmap就是不同的内存,所以不会有问题。
libattach.so 到底应该由谁加载
- 应用是jdk启动,如果在在classpath里有 tools.jar,那么应该由 SystemClassloader来加载
- 如果应用是以 jre 的方式启动,那么在jre下面,应该找不到 tools.jar,也找不到
libattach.so
。(当然很多代码会尝试在java home下面查找tools.jar,实际上会加载到jdk里的 tools.jar,但这个并不是jre带的)
我们再来看attach的过程,实际上是要加载 com.sun.tools.attach.VirtualMachine
,调用它的attach
函数。
com.sun.tools.attach.VirtualMachine
类是在 tools.jar
里的
VirtualMachine.attach
最终要调用 System.loadLibrary
来加载so,下面是bytebuddy的调用栈
Thread [main] (Suspended (uncaught exception UnsatisfiedLinkError))
owns: Class<T> (net.bytebuddy.agent.ByteBuddyAgent) (id=17)
ClassLoader.loadLibrary0(Class<?>, File) line: 1961
ClassLoader.loadLibrary(Class<?>, String, boolean) line: 1853
Runtime.loadLibrary0(Class<?>, String) line: 872
System.loadLibrary(String) line: 1124
BsdVirtualMachine.<clinit>() line: 321
BsdAttachProvider.attachVirtualMachine(String) line: 63
VirtualMachine.attach(String) line: 208
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
Method.invoke(Object, Object...) line: 498
Attacher.install(Class<?>, String, String, boolean, String) line: 106
ByteBuddyAgent.install(ByteBuddyAgent$AttachmentProvider, String, String, ByteBuddyAgent$AgentProvider, boolean) line: 628
ByteBuddyAgent.install(ByteBuddyAgent$AttachmentProvider, ByteBuddyAgent$ProcessProvider) line: 606
ByteBuddyAgent.install(ByteBuddyAgent$AttachmentProvider) line: 558
ByteBuddyAgent.install() line: 535
Test.main(String[]) line: 35
这时,我们用arthas来确定com.sun.tools.attach.VirtualMachine
是哪里加载的:
$ sc -d com.sun.tools.attach.VirtualMachine
class-info com.sun.tools.attach.VirtualMachine
code-source /Users/hengyunabc/.sdkman/candidates/java/8.0.265-zulu/zulu-8.jdk/Contents/Home/jre/../lib/tools.jar
name com.sun.tools.attach.VirtualMachine
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name VirtualMachine
modifier abstract,public
annotation jdk.Exported
interfaces
super-class +-java.lang.Object
class-loader +-java.net.URLClassLoader@378bf509
classLoaderHash 378bf509
[arthas@69647]$ classloader -l
name loadedCount hash parent
BootstrapClassLoader 2677 null null
com.taobao.arthas.agent.ArthasClassloader@41e1548d 1372 41e1548d sun.misc.Launcher$ExtClassLoader@53acfae9
java.net.URLClassLoader@378bf509 63 378bf509 null
sun.misc.Launcher$AppClassLoader@18b4aac2 46 18b4aac2 sun.misc.Launcher$ExtClassLoader@53acfae9
sun.misc.Launcher$ExtClassLoader@53acfae9 66 53acfae9 null
Affect(row-cnt:5) cost in 4 ms.
[arthas@69647]$ classloader -l -c 378bf509
file:/Users/hengyunabc/.sdkman/candidates/java/8.0.265-zulu/zulu-8.jdk/Contents/Home/jre/../lib/tools.jar
可以看到com.sun.tools.attach.VirtualMachine
是 bytebuddy new URLClassLoader 来加载的。
所以我们可以说tools.jar
本质上就是一个独立的jar,如果应用没有在启动的 classpath时指定,那么后续完全取决于用户的代码怎么去加载它。
其它:怎么获取jvm已经加载的lib列表
参考:
可以用反射的方式,获取ClassLoader已经加载的Library信息。
到底应该怎么处理libattach.so
问题?
目前来看,没有特定的答案。只能说bytebuddy采用的是 new URLClassLoader的方式来加载。 如果还有其它的类库,也用类似的方式来加载,就会出现开始的问题。
使用SystemClassLoader来加载libattach
,就能解决问题么?其实并不是。最简单的重现代码:
public class Test {
public static void main(String[] args) {
System.loadLibrary("attach");
ByteBuddyAgent.install();
}
异常信息:
Exception in thread "main" java.lang.IllegalStateException: Error during attachment using: net.bytebuddy.agent.ByteBuddyAgent$AttachmentProvider$Compound@6576fe71
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:633)
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:606)
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:558)
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:535)
at Test.main(Test.java:44)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at net.bytebuddy.agent.Attacher.install(Attacher.java:106)
at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:628)
... 4 more
Caused by: java.lang.UnsatisfiedLinkError: Native Library /Users/hengyunabc/.sdkman/candidates/java/8.0.265-zulu/zulu-8.jdk/Contents/Home/jre/lib/libattach.dylib already loaded in another classloader
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1915)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1853)
at java.lang.Runtime.loadLibrary0(Runtime.java:872)
at java.lang.System.loadLibrary(System.java:1124)
at sun.tools.attach.BsdVirtualMachine.<clinit>(BsdVirtualMachine.java:321)
at sun.tools.attach.BsdAttachProvider.attachVirtualMachine(BsdAttachProvider.java:63)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
... 10 more
所以,本质上不是libattach
加载的问题,而是com.sun.tools.attach.VirtualMachine
类加载的问题。
可能的解决办法是把com.sun.tools.attach.VirtualMachine
统一入到SystemClassLoader里加载,这样子就不会有问题了。
比如在应用启动时,查找到tools.jar
,然后append到SystemClassLoader里。
public static void addToClasspath(File file) {
try {
URL url = file.toURI().toURL();
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(classLoader, url);
} catch (Exception e) {
throw new RuntimeException("Unexpected exception", e);
}
}
后续
- 这个问题,只在jdk 8 及以下的版本才会出现,更高的版本不再有 tools.jar了
- 在arthas-spring-boot-starter里,尝试在启动时append tools.jar到SystemClassLoader
- 强制gc的方式,一是不一定能生效(应用可能用jvm参数禁掉),二是可能会对应用有大负担,不合适