0

从头开始重写@ 5 月 25 日星期五,格林威治标准时间 16:00 左右

(现在代码更干净了,bug可以重现,问题更清楚了)

原始问题:我正在编写一个服务器应用程序,它需要通过网络接受来自客户端的文件并使用某些类处理它们,这些类是通过 URLClassLoader 从本地存储的 .jar 文件加载的。几乎一切正常,但是这些 jar 文件会不时地热交换(无需重新启动服务器应用程序)以应用修补程序,如果我们不幸同时更新 .jar 文件,则来自它的类是正在加载,ClassFormatError被抛出,并带有关于“截断类”或“末尾多余字节”的注释。这是意料之中的,但是整个应用程序变得不稳定并且在那之后开始表现得很奇怪——这些ClassFormatError异常一直在发生当我们尝试从更新的同一个 jar 再次加载类时,即使我们使用 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 - 可插入 .jar 的简单接口:

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);
    }
}

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

实际原因:

一旦我进一步简化了我的代码并摆脱了clientThread类,这一切都变得清晰起来,只需实例化并JarLoader在. 当被抛出时,它不仅将堆栈跟踪打印出来,而且实际上使整个 JVM 崩溃(以代码 1 退出)。原因并不像现在看起来那么明显(至少不适合我):extends , not . 因此它通过并且JVM由于未捕获的异常/错误而退出,但是因为我产生了导致另一个产生的(客户端)线程出错的线程,只有那个线程崩溃了。我想这是因为 Linux 处理 Java 线程的方式,但我不确定。whilemainClassFormatErrorClassFormatErrorErrorExceptioncatch(Exception E)

(临时)解决方案:

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

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

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

那么,对Java有更深入了解的人能否告诉我真正的原因是什么,或者至少尝试给出一个方向?也许这是一些已知的错误,甚至是预期的行为,但它已经太复杂了,我自己无法理解——我还是个新手。我真的可以依靠强制GC吗?

4

0 回答 0