URLClassLoader,“hot-swapping”jar文件和ClassFormatError - 奇怪的行为

时间:2012-05-23 14:22:39

标签: java jar urlclassloader

从5月25日星期五,格林尼治标准时间16:00左右重写:

(现在代码更清晰,可以复制错误,问题更清晰)

原始问题:我正在编写一个服务器应用程序,它需要通过网络接受来自客户端的文件,并使用某些类处理它们,这些类是通过URLClassLoader从本地存储的.jar文件加载的。几乎所有东西都能正常工作,但是那些jar文件是不时热交换的(没有重新启动服务器应用程序)来应用修补程序,如果我们不幸的是在同一时间更新.jar文件,那么它是正在加载,ClassFormatError被抛出,带有关于“截断类”或“末尾多余字节”的注释。这是可以预料的,但整个应用程序变得不稳定并且在此之后开始表现得很奇怪 - 当我们尝试从更新的同一个jar中再次加载类时,那些ClassFormatError异常继续发生 ,即使我们使用URLClassLoader的新实例,它也发生在不同的应用程序线程中。

该应用程序正在Debian Squeeze 6.0.3 / Java 1.4.2上运行和编译,迁移不在我的掌控之中。

这是一个模仿应用行为的简单代码,大致描述了问题:

1)主应用程序和每个客户端线程的类:

package BugTest;

public class BugTest 
{
  //This is a stub of "client" class, which is created upon every connection in real app
  public static class clientThread extends Thread
    {
    private JarLoader j = null;
    public void run()
      {
        try 
          {
          j = new JarLoader("1.jar","SamplePlugin.MyMyPlugin","SampleFileName");
          j.start();
          }
        catch(Exception e)
          {
          e.printStackTrace();
          }
      }
    }

  //Main server thread; for test purposes we'll simply spawn new clients twice a second.
  public static void main(String[] args)
    {
    BugTest bugTest = new BugTest();
    long counter = 0;        
    while(counter < 500)
        {
        clientThread My = null;
        try
            {
            System.out.print(counter+") "); counter++;
            My = new clientThread();
            My.start();
            Thread.currentThread().sleep(500);
            }
        catch(Exception e)
            {
            e.printStackTrace();
            }
        }
    }
}

2)JarLoader - 从.jar加载类的包装器,扩展了Thread。这里我们加载一个实现某个接口a:

的类
package BugTest;

import JarPlugin.IJarPlugin;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class JarLoader extends Thread
{
  private String jarDirectory = "jar/";
  private IJarPlugin Jar;
  private String incomingFile = null;

  public JarLoader(String JarFile, String JarClass, String File) 
         throws FileNotFoundException, MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException
    {
    File myjarfile = new File(jarDirectory);
    myjarfile=new File(myjarfile,JarFile);
    if (!myjarfile.exists())
      throw new FileNotFoundException("Jar File Not Found!");
    URLClassLoader ucl = new URLClassLoader(new URL[]{myjarfile.toURL()});
    Class JarLoadedClass =ucl.loadClass(JarClass);
    // ^^ The aforementioned ClassFormatError happens at that line ^^
    Jar = (IJarPlugin) JarLoadedClass.newInstance();
    this.setDaemon(false);
    incomingFile = File
    }

  public void run()
    {
    Jar.SetLogFile("log-plug.txt");
    Jar.StartPlugin("123",incomingFile);
    }
}

3)IJarPlugin - 可插入.jars的简单接口:

package JarPlugin;

public interface IJarPlugin 
{
  public void StartPlugin(String Id, String File);
  public void SetLogFile(String LogFile);
}

4)实际的插件:

package SamplePlugin;
import JarPlugin.IJarPlugin;

public class MyMyPlugin implements IJarPlugin
{
   public void SetLogFile(String File)
    {
    System.out.print("This is the first plugin: ");
    }
  public void StartPlugin(String Id, String File)
    {
    System.out.println("SUCCESS!!! Id: "+Id+",File: "+File);
    }
}

要重现该错误,我们需要使用相同的类名编译一些不同的.jars,其唯一的区别是“这是第N个插件:”中的数字。然后启动主应用程序,然后用其他.jars快速替换名为“1.jar”的已加载插件文件,然后返回,模仿热交换。同样,ClassFormatError在某些时候是可以预料到的,但是即使jar被完全复制(并且没有以任何方式损坏)它也会继续发生,有效地杀死试图加载该文件的任何客户端线程;摆脱这个循环的唯一方法是用另一个插件替换插件。看起来很奇怪。

实际原因:

一旦我更简化了代码并摆脱了clientThread类,只需实例化并在JarLoader while循环main }。当抛出ClassFormatError时,它不仅打印出堆栈跟踪,而且实际上崩溃了整个JVM(退出代码为1)。原因并不像现在看起来那么明显(至少不适合我):ClassFormatError扩展Error,而不是Exception。因此它通过catch(Exception E)并且由于未捕获的异常/错误而退出JVM,但是因为我生成的线程导致来自另一个衍生(客户端)线程的错误,只有该线程崩溃。我想这是因为Linux处理Java线程的方式,但我不确定。

(临时)解决方案:

一旦未被捕获的错误原因变得清晰,我试图在“clientThread”中捕获它。它有点工作(我删除了堆栈跟踪打印输出并打印了我自己的消息),但主要问题仍然存在:ClassFormatError,即使被正确捕获,仍然发生,直到我更换或删除有问题的.jar。所以我粗略猜测某种缓存可能是罪魁祸首,并强制URLClassLoader引用失效和垃圾收集,方法是将其添加到clientThread try块:

catch(Error e)
  {
  System.out.println("Aw, an error happened.");
  j=null;
  System.gc();
  } 

令人惊讶的是,它似乎有效!现在错误只发生一次,然后文件类正常加载,正如它应该的那样。但由于我只是做了一个假设,但没有理解真正的原因,我仍然担心 - 它现在有效,但不能保证它会在更复杂的代码中使用。

那么,任何对Java有更深入理解的人都可以启发我的真正原因,或者至少尝试指导方向吗?也许这是一些已知的错误,甚至是预期的行为,但是对我来说已经太复杂了 - 我仍然是一个新手。我真的可以依靠强制GC吗?

0 个答案:

没有答案