5 月 25 日金曜日、グリニッジ標準時 16:00 頃にゼロから書き直しました
(コードがよりきれいになり、バグを再現できるようになり、質問がより明確になりました)
元の問題:ネット経由でクライアントからファイルを受け取り、URLClassLoader を介してローカルに保存された .jar ファイルからロードされる特定のクラスでそれらを処理するために必要なサーバー アプリを作成しています。ほとんどすべてが正常に動作しますが、これらの jar ファイルは (サーバー アプリを再起動せずに) ホット スワップされ、ホットフィックスが適用されます。ロードされると、ClassFormatError
「切り捨てられたクラス」または「末尾の余分なバイト」に関するコメントとともにスローされます。それは当然のことですが、アプリケーション全体が不安定になり、その後奇妙な動作を始めます - これらのClassFormatError
例外が発生し続けますURLClassLoader の新しいインスタンスを使用し、別のアプリ スレッドで発生したにもかかわらず、更新された同じ jar からクラスを再度ロードしようとすると。
アプリは 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 をコンパイルする必要があります。唯一の違いは、"This is the Nth plugin:" の番号です。次に、メイン アプリケーションを起動し、ロードされた「1.jar」という名前のプラグイン ファイルを他の .jar にすばやく置き換え、ホットスワップを模倣して元に戻します。繰り返しClassFormatError
ますが、ある時点で予想されることですが、jar が完全にコピーされた場合 (および破損していない場合) にも発生し続け、そのファイルをロードしようとするクライアント スレッドを事実上強制終了します。このサイクルから抜け出す唯一の方法は、プラグインを別のものに置き換えることです。本当に奇妙に思えます。
実際の原因:
コードをさらに単純化し、clientThread
クラスを取り除き、インスタンス化してループJarLoader
内で開始するだけで、すべてが明確になりました。がスローされると、スタック トレースが出力されるだけでなく、実際に JVM 全体がクラッシュしました (コード 1 で終了)。理由は今思うほど明白ではありません (少なくとも私にはそうではありませんでした): extends , not . したがって、キャッチされない例外/エラーのためにJVMは通過し、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();
}
驚くことに、うまくいくようです!エラーは 1 回だけ発生し、ファイル クラスは正常にロードされるようになりました。しかし、私は仮説を立てたばかりで、実際の原因を理解していないので、まだ心配しています.今は動作しますが、後ではるかに複雑なコード内で動作するという保証はありません.
では、Java をより深く理解している人は、本当の原因は何かを教えてくれますか、少なくとも方向性を示してくれますか? 既知のバグか、予想される動作かもしれませんが、すでに複雑すぎて自分で理解することはできません。私はまだ初心者です。そして、GC の強制に本当に頼ることができますか?