Java ClassLoader 在新线程(可运行)中使用时停止工作

Posted

技术标签:

【中文标题】Java ClassLoader 在新线程(可运行)中使用时停止工作【英文标题】:Java ClassLoader stops working when used in new Thread (Runnable) 【发布时间】:2014-06-17 00:59:30 【问题描述】:

我正在开发一个 Eclipse 插件。当插件运行时(例如在 eclipse 的调试实例中),它会从调试工作区中的所有 java-projects 加载某些类。反射用于此目的。为了更好地了解我在这里所做的是一些代码:

for (IPath outDir : _outputDirs)

    IPath fullTestPackageDir = outDir.append(testPackageDir);
    File dir = fullTestPackageDir.toFile();
    if (dir.exists() && dir.isDirectory())
    
        for (File classFile : dir.listFiles())
        
            String binaryClassName = packageName + "." + FileUtils.getNameWithoutExtension(classFile);
            Class<ITest> testClass = tryLoadClass(binaryClassName, ITest.class);
            if (testClass != null)
            
                testClasses.add(testClass);
            
        
    

以下是我在上面使用的tryLoadClass 的实现,按照 cmets 的要求(一点也不神奇)。 _urlClassLoader 是从 URLs 的数组(调试工作区中 java-projects 的输出文件夹)创建的,并以当前线程的上下文 ClassLoader 作为父级。

@SuppressWarnings("unchecked")
private <T> Class<T> tryLoadClass(String binaryClassName, Class<T> baseType)

    try
    
        Class<?> testClass = _urlClassLoader.loadClass(binaryClassName);

        if (!baseType.isAssignableFrom(testClass))
            return null;

        return (Class<T>)testClass;
    
    catch (ClassNotFoundException e)
    
        System.err.println("class not found");
        return null;
    

稍后,这些类被实例化并调用一个函数 (run):

Class<ITest> testClass = testClasses.get(i);

try

    ITest test = testClass.getConstructor(IRuntime.class).newInstance(_runtime);
    test.run();

catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e)

    e.printStackTrace();

在插件包中有几个实用程序类,例如MyUtil,由插件导出,可以被上面加载的类引用。准确地说,MyUtilrun 函数中被引用。

上面的代码一切正常。但是如果我将test.run() 移动到另一个线程中,我会得到MyUtil 的ClassNotFoundException。又是代码(和上面差不多,只替换了第6行)。 ITest 继承自 Runnable

Class<ITest> testClass = testClasses.get(i);

try

    ITest test = testClass.getConstructor(IRuntime.class).newInstance(_runtime);
    Thread thread = new Thread(test, "TestThread");
    thread.start();

catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e)

    e.printStackTrace();

我已经尝试了很多东西,比如获取当前线程的ContextClassLoader 并将其传递给新线程,但不是运气。我发现我可以使用当前线程的ContextClassLoader 显式加载MyUtil,但只有在原始线程中使用它时才可以。如果我将该 ClassLoader 传递给新线程,它就会停止工作。

我认为我的问题是我对 ClassLoaders 的工作原理有一些错误的理解......

以下是有关异常的更多详细信息:

Caused by: java.lang.ClassNotFoundException: de.my.company.MyPlugin
    at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
    ... 2 more

因为有一个关于如何加载“de.my.company.MyPlugin”的问题:它只是在包/插件中,这是调试工作区中的 java 项目所需要的,因此我认为它在“ base" 类路径(这里不知道正确的术语......)

/编辑: 我刚刚意识到我没有提到一些可能很重要的事情:test.run()(以及启动新线程)是在 Eclipse Job 的上下文中完成的。但我想我已经算过了:当我删除 Job-context 时问题仍然存在(直接从 Eclipse 命令的处理程序中发出代码)。但是,如果ITest实现被正常实例化(即在eclipse调试实例中;我在一个简单的main()函数中对其进行了测试),则问题不存在。

我认为问题是由于我的自定义_urlClassLoader 用于加载ITest。因为 Java 尝试使用一开始就加载容器类的类加载器(afaik)加载任何引用的类,所以我的 _urlClassLoader 将再次使用。但是,单独的 _urlClassLoader 无法加载请求的类(它的“类路径”上只有其他 java 项目的输出文件夹)。相反,必须使用其父级。如果我理解正确,这应该会自动发生,但正是这部分不再适用于新线程......

【问题讨论】:

你能告诉我们更多关于tryLoadClass的信息吗? de.my.company.MyPlugin 类在哪里存在以及如何将其添加到 URLClassLoader 的类路径中(如异常跟踪中所示)? 你能在线程完成运行之前验证 URLClassloader 没有关闭吗?我问是因为 URLClassloader 应该在使用后关闭,test.run() 将阻塞直到所有工作完成,但thread.start() 运行异步。 关于您的问题的奇怪之处在于您在整个类加载完成的地方创建了Thread。难怪使用新的Thread 的上下文加载器并不能解决问题,您可以通过将类的 instance 传递给它的构造函数来创建Thread,所以应该怎么做你用那个Thread 来影响那个类的加载?你应该追查ClassNotFoundException真正出现的地方,它不可能是新线程。 如果是新线程,则与贴出的代码无关。 @vanOekel:原线程未关闭;我在thread.start() 之后插入了一个无限循环。 @Holger:我可以在“一切正常”和ClassNotFoundException 之间切换,只需将test.run() 替换为构造和启动新线程的两行代码即可。我也认为此时在类加载方面应该没有任何区别。 @Holger:我在问题的最后添加了一些信息;我认为问题与实际执行的代码(= 'ITest.run()' 的实现)的加载方式与 VM 的加载方式不同(使用 _urlClassLoader)有关。至少,如果我正常实例化一个ITest实现(即在eclipse调试实例中),类加载问题就不存在了。 【参考方案1】:

我对 OSGi 加载类的方式并不深入,但我知道标准机制足以说明以下几点:

当使用不同的Thread 时,JVM 解析类的方式(使用引用者的ClassLoader)不会改变 URLClassLoader 实现的对父加载器的委托是无条件发生的

因此,如果这些部分没有改变,那么改变其行为的是 OSGi 框架提供的父加载器。我发现this blog entry 描述了一种适合观察到的行为的机制:

上下文查找器

上下文查找器是一种由 Equinox 框架安装为默认上下文类加载器的类加载器。调用时,它会在 Java 执行堆栈中搜索除系统类加载器之外的类加载器。

因此,当您启动一个新的Thread 并提供您使用URLClassLoader 动态加载的Runnable 时,该Thread 的堆栈跟踪中没有可用作上下文的包。

一个简单的解决方法是从你的包中添加代码到新 Thread 的堆栈跟踪中,即更改

Thread thread = new Thread(test, "TestThread");

进入

Thread thread = new Thread(new Runnable() 
  public void run()  test.run(); 
, "TestThread");

当然test必须改成final

这样,来自 Eclipse 插件的调用者(匿名内部类)位于新的 Thread 堆栈的顶部。由于您的URLClassLoader 的父级已经初始化为 OSGi 类加载器,因此不需要设置上下文类加载器(除非在加载的代码中使用线程上下文加载器进行一些动态加载)。

【讨论】:

完美!我刚试了一下,但是看了你的解释我已经很有信心了。您认为继续下去的最佳方法是什么?您建议的简单解决方法很好,但我更喜欢安装一个 ClassLoader,它可以在我的包中找到类,而不必查看堆栈跟踪。为此目的编写我自己的 ClassLoader 实现感觉就像重新发明***...... 如前所述,我对 OSGi 的事情并不深入,但也许在你的插件类上调用 getClass().getClassLoader() 会给你一个绑定到你的插件而不是默认加载器的 ClassLoader那个搜索算法。如果是这样,使用 ClassLoader 作为 URLClassLoader 的父级可以解决问题。

以上是关于Java ClassLoader 在新线程(可运行)中使用时停止工作的主要内容,如果未能解决你的问题,请参考以下文章

Java基础 JVM 垃圾回收classloader

classloader原理

Classloader

java中类加载器ClassLoader,双亲加载机制,启动类加载器,应用类加载器,线程上下文类加载器

Myeclipse中启动服务器tomcat时 会弹出页面ClassLoader.java没有运行状况的提示

Java运行时环境---ClassLoader类加载机制