从头开始重写@ 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 线程的方式,但我不确定。while
main
ClassFormatError
ClassFormatError
Error
Exception
catch(Exception E)
(临时)解决方案:
一旦未捕获的错误原因变得清晰,我试图在“clientThread”中捕获它。它有点工作(我删除了堆栈跟踪打印输出并打印了我自己的消息),但主要问题仍然存在:ClassFormatError
即使正确捕获,在我替换或删除有问题的 .jar 之前一直发生。所以我大胆猜测某种缓存可能是罪魁祸首,并通过将其添加到 clientThreadtry
块来强制 URLClassLoader 引用失效和垃圾收集:
catch(Error e)
{
System.out.println("Aw, an error happened.");
j=null;
System.gc();
}
令人惊讶的是,它似乎有效!现在错误只发生一次,然后文件类正常加载,因为它应该。但是由于我只是做了一个假设,但没有理解真正的原因,我仍然担心 - 它现在可以工作,但不能保证它以后会在更复杂的代码中工作。
那么,对Java有更深入了解的人能否告诉我真正的原因是什么,或者至少尝试给出一个方向?也许这是一些已知的错误,甚至是预期的行为,但它已经太复杂了,我自己无法理解——我还是个新手。我真的可以依靠强制GC吗?