[alibaba/arthas]arthas-spring-boot-starter启动失败:Error create bean with name 'arthasAgent' libattach.so already loaded in another classloader

2024-04-24 377 views
6
环境信息
  • arthas-spring-boot-starter:3.5.1
  • Arthas 版本: 3.5.1
  • 操作系统版本: linux 2.6.32-696.el6.x86_64
  • 目标进程的JVM版本: 1.8.0——221-b32 hotspot
重现问题的步骤
  1. 启动后报错
期望的结果

能够正常启动,然后能够连接到tunnel-server

实际运行的结果

启动报错: Caused by:org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'arthasAgent' defined image

image

回答

1

有没有用了kotlin? 可能是其它依赖repackage了 bytebuddy。

2

没有用kotlin

4

在ide里,输入bytebuddy的类,再按提示看下是不是有 repackage的。或者检查下应用有没有配置了其它的java agent。

7

我们公司的项目,我在本地上跑没问题。在测试环境上跑时候才报错的。后面看了一下,测试环境上部门部署了安全扫描的东西,使用javaassist对字节码有操作。但是我单独使用arthas-3.5.1,不使用starter可以启动成功呢。请问有办法解决吗?

项目中没有其他地方引用byte-buddy库 image

7

在jvm里,对于同一个 so 加载有限制:就是只能由同一个 classloader加载。

出现这个问题的原因是,有其它的依赖打包了 jdk 的 attach相关的代码,然后这段代码被其它的 ClassLoader执行了,比如在Spring Boot里可能是 Spring Boot的ClassLoader。参考: http://hengyunabc.github.io/spring-boot-classloader/

然后当 bytebuddy尝试attach时(应该使用的是 SystemClassLoader),就会出现这个异常。

因此,只能让错误打包了jdk attach代码的依赖修改掉。


这个issue里是应用自身依赖了 mycat,mycat打包了jvm 的 attach相关的jar。

7

在jvm里,对于同一个 so 加载有限制:就是只能由同一个 classloader加载。

出现这个问题的原因是,有其它的依赖打包了 jdk 的 attach相关的代码,然后这段代码被其它的 ClassLoader执行了,比如在Spring Boot里可能是 Spring Boot的ClassLoader。参考: http://hengyunabc.github.io/spring-boot-classloader/

然后当 bytebuddy尝试attach时(应该使用的是 SystemClassLoader),就会出现这个异常。

因此,只能让错误打包了jdk attach代码的依赖修改掉。

这个issue里是应用自身依赖了 mycat,mycat打包了jvm 的 attach相关的jar。

您好,我最近对该问题做了深入的研究后发现,导致该问题的原因并非公司代码写得有问题,而是由于com.sun.tools.attach.VirtualMachine#detach方法被调用之后,java.lang.ClassLoader.NativeLibrary#finalize方法没有被及时调用,导致在下一次调用com.sun.tools.attach.VirtualMachine#attach方法时,发现使用了不同的ClassLoader加载了名为attach的库。以下是我模拟和复现此问题的类,以及手工触发gc的一种解决方法:

package com.flysky.study.bytecode.bytebuddy.agent;

import net.bytebuddy.agent.ByteBuddyAgent;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 模拟"libattach.so already loaded in another classloader"错误,以及解决方法<br>
 * 为了模拟不同类加载器加载相同的attach库,需要确保classpath不能包含tools.jar文件路径<br>
 *
 */
public class JdkAttach {
    public JdkAttach() {
    }

    public void loadAgent() throws Exception {
        Object virtualMachineInstance = null;
        Class<?> virtualMachineType = null;
        try {
            ClassLoader toolsClassLoader = getToolsClassLoader();
            virtualMachineType = toolsClassLoader.loadClass("com.sun.tools.attach.VirtualMachine");

            String processId = getProcessId();
            virtualMachineInstance = virtualMachineType
                    .getMethod("attach", String.class)
                    .invoke(null, processId);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (virtualMachineType != null && virtualMachineInstance != null) {
                virtualMachineType.getMethod("detach").invoke(virtualMachineInstance);
                System.out.println("detach调用完成");
            }
        }
    }

    public String getProcessId() {
        String runtimeName = ManagementFactory.getRuntimeMXBean().getName();
        int processIdIndex = runtimeName.indexOf('@');
        if (processIdIndex == -1) {
            throw new IllegalStateException("Cannot extract process id from runtime management bean");
        } else {
            return runtimeName.substring(0, processIdIndex);
        }
    }

    private ClassLoader getToolsClassLoader() throws MalformedURLException {
        String javaHome = System.getProperty("java.home");
        File toolsJarFile = new File(javaHome, "../lib/tools.jar");

        if (!toolsJarFile.isFile()) {
            throw new IllegalStateException("文件不存在:" + toolsJarFile.getAbsolutePath());
        }
        URL url = toolsJarFile.toURI().toURL();
        //构建一个parent为ClassLoader.getSystemClassLoader()加载器,因此如果classpath中包含tools.jar,最终加载attach的类加载器就会相同
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        return classLoader;
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=============================================================================");
        System.out.println("------需要确保classpath不能包含tools.jar文件路径:");
        System.out.println("------1、idea运行默认会将tools.jar添加到classpath中,因此需要使用java命令来排除干扰");
        System.out.println("------2、也不要将tools.jar路径添加到CLASSPATH环境变量中");
        System.out.println("=============================================================================");

        new JdkAttach().loadAgent();
        invokeSystemGc(args.length > 0 ? args[0] : "");
        ByteBuddyAgent.install();//此方法最终也会使用parent为ClassLoader.getSystemClassLoader()加载器来加载
    }

    private static void invokeSystemGc(String arg) {
        if ("gc=true".equals(arg)) {
            System.out.println("调用System.gc()");
            System.gc();
        } else {
            String javaClassPath = System.getProperty("java.class.path");
            if (javaClassPath.contains("tools.jar")) {
                System.out.println("java.class.path包含tools.jar文件:" + javaClassPath);
            }
        }
    }

}

这个问题只要有两个不同的库使用了attach方法,并且没有将tools.jar添加到classpath,就会出现此问题,因此我觉得彻底解决这个问题很是值得。目前思路有两个: 1、在attach之前先调用System.gc() 2、在attach之前移除java.lang.ClassLoader#loadedLibraryNames中包含的attach库名

2

这里有几个问题要明确。

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参数禁掉),二是可能会对应用有大负担,不合适
0

感谢大佬解答。 ===这个问题,只在jdk 8 及以下的版本才会出现,更高的版本不再有 tools.jar了 ---->只要9以后应该就不会出现此问题了。但现在使用8的太多,特别是大公司

===在arthas-spring-boot-starter里,尝试在启动时append tools.jar到SystemClassLoader ---->这个可以解决artahs先加载lib的情况,但如果arthas是后加载的,就像当前issue遇到的这种情况,就无法解决了。而先加载的jar是其他部门提供的jar,因此想要在其启动时append tools.jar到SystemClassLoader,目前不可能啊。

强制gc的方式,一是不一定能生效(应用可能用jvm参数禁掉),二是可能会对应用有大负担,不合适 ---->是有这种可能。不过在加载arthas出现异常“libattach.so already loaded in another classloader”时候,再System.gc(),只要应用正常启动起来,产生的负担应该可以忽略吧。

====另外一种方式是,移除java.lang.ClassLoader#loadedLibraryNames中包含的attach库名 这种方式可以保障100%启动成功。这两种方式都有用到,而且只有在attach出现异常时候,才会进行的补救措施。大佬可以帮忙看一下我的pr 解决#1838问题

============== 其它:怎么获取jvm已经加载的lib列表 ----->使用artahs方式很便捷: getstatic java.lang.ClassLoader systemNativeLibraries -x 2 vmtool --action getInstances --className java.lang.ClassLoader --express 'instances.{nativeLibraries}' -x 26961639493201_ pic 3 26971639493252_ pic

2

我觉得,光追踪到库有没有被加载以及被哪个类加载器加载还不够。只有知道加载这个lib的调用链是什么,才能够在混杂的项目中找到问题的根源。即跟踪java.lang.System#load方法的调用链即可找到,以下是我当时追踪的代码,打成agent包之后,使用-javaagent运行即可实现追踪:

package com.flysky.study.bytecode.bytebuddy.agent;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import java.util.Collections;

public class SystemLoadAgent {
    public static void main(String[] args) throws Exception {
        premain(null, ByteBuddyAgent.install());
        String attach = "/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/libattach.dylib";
//        System.loadLibrary(attach);
        System.load(attach);
    }
    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, IOException {

        File temp = Files.createTempDirectory("tmp").toFile();
        ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, inst)
                .inject(Collections.singletonMap(
                new TypeDescription.ForLoadedType(SystemLoadAdvice.class),
                ClassFileLocator.ForClassLoader.read(SystemLoadAdvice.class)
        ));

        Class<?> target = Class.forName("java.lang.System");
        ClassLoader classLoader = target.getClassLoader();
        new ByteBuddy()
                .redefine(target, ClassFileLocator.ForClassLoader.ofBootLoader())
                .visit(Advice.to(SystemLoadAdvice.class).on(ElementMatchers.named("load")))
                .make()
                .load(classLoader, ClassReloadingStrategy.of(inst));
    }
}
package com.flysky.study.bytecode.bytebuddy.agent;

import net.bytebuddy.asm.Advice;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;

public class SystemLoadAdvice {
    @Advice.OnMethodEnter(inline = false)
    public static void enter(@Advice.AllArguments Object[] args) {

        System.out.println("enter!");

        System.out.println("- @AllArguments !!");
        for (Object o : args) {
            String s = o.toString();
            System.out.println("- - - type : " + o.getClass().getName() + ", value : " + s);
//            if (s.contains("attach"))
            System.err.println(getStackTrace(new Exception("trace:" + s)));
        }
    }

    public static String getStackTrace(Throwable e) {
        final Writer result = new StringWriter();
        final PrintWriter printWriter = new PrintWriter(result);
        e.printStackTrace(printWriter);
        return result.toString();
    }
}
9

仔细想了下,可能这样子比较合理:

  1. 提供一个配置项 和 API,帮助应用自助append tools.jar,应用如果有需要,可以先自己处理好
  2. 当明确是因为 libattach.so already loaded in another classloader 失败时,默认执行下gc,再重新初始化。用户也可以显式禁止这个行为
  3. 不尝试修改ClassLoader#loadedLibraryNames ,这个风险比较大,也不是很合理
1

大佬您好,我深入研究了一下,我觉得你第二点建议很好。请帮忙review一下我的pr, 彻底解决libattach.so already loaded in another classloader问题 #2064 而第三点的话,我有点个人的建议,如果不对,请赐教: 1、libattach.so只是用在jvmti获取虚拟机的时候,用完即扔,只是扔得不够快,才导致的这个问题。 2、libattach.so库为动态库,而动态库是可共享的,因此库内部不应该有全局变量或者即使有那么其内部自身也必须有它的保护机制。因此我认为修改ClassLoader#loadedLibraryNames风险不大。 3、多次加载libattach.so动态库,内存是否会存在多个拷贝呢?导致内存浪费呢?答案是不会。通过arthas发现,两次加载的共享库句柄完全相同,证明是同一份库文件,内存不会浪费: image

5

不修改ClassLoader#loadedLibraryNames,有多个原因:

  1. 这是一个类的内部变量,本身就尽量不要修改,它是非公开的,以后也可能会变化
  2. jvm内部是怎么修改利用这个变量的,并不清楚。如果在同一刻,jvm内部在更新它,然后 starter的代码也在更新它,就会出现不可知的结果,jvm可能本身就会出错了

所以,非必要情况下,不要去暴力修改ClassLoader#loadedLibraryNames

6

本质上这个问题是怎么使用 tools.jar 的问题。

  1. 对于大部分应用来说,如果它自身是需要tools.jar的,那么在 classpath里就会带上。这个也是大部分应用的常态
  2. 但是也确实有一些应用是启动时,classpath里没有带上 tools.jar,那么像 ByteBuddy 在 attach jvm 自身时,它需要加载到 tools.jar 。
  3. ByteBuddy的做法是,自己查找到tools.jar的位置,然后 new 一个ClassLoader,在urls里带上tools.jar。这个新的ClassLoader parent是SystemClassLoader
  4. ByteBuddy的做法有合理的地方,也有不合理的地方。合理的地方是它自己去加载tools.jar,那么不会对应用的其它代码造成影响。不合理的地方就是,如果有其它的库效仿ByteBuddy的行为,或者repackage了ByteBuddy,就会出现上面的错误。
  5. 可能ByteBuddy可以考虑增加功能:当SystemClassLoader里加载不到tools.jar里的类时,就尝试查找tools.jar,如果找到就append tools.jar到 SystemClassLoader
  6. 就算是增加一个把 tools.jar append到 SystemClassLoader的行为,这个也只有一定的合理性。本质上还是应用需要tools.jar,但classpath并没有带上
3

第1点大部分不成立,因为目前大多数第三方应用都是默认从../lib/tools.jar加载,这才是常态啊,比如像druid,大部分都不要求必须要在classpath上。该问题到此结束吧,再讨论也没完没了,讨论到此能够看完的人自己也可以找到原因和解决方案了。因此这个PR 彻底解决libattach.so already loaded in another classloader问题 #2064就帮我关闭了吧,既然解决方案不通用,那么需要的童鞋自己加。