从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吗?