シャットダウンせずに更新された機能を必要とする長期間実行されるJavaアプリケーションを作成しています。私はこの更新された機能を、メモリでコンパイルされインスタンス化される.javaファイル(データベースからバイト配列としてプルされる)の形式でロードすることで提供することにしました。あなたがより良い方法を持っているなら、私はすべて耳です。
私が遭遇した問題は、人工環境でテストを行うと、これらの「スクリプト」をロードする各サイクルでメモリフットプリントがわずかに増加することです。
注:これは実際に、Javaでこのようなことを行うのが初めてです。以前、C#で.csファイルをロードおよびアンロードしてこのようなことを達成し、そこにメモリフットプリントの問題もありました...それらを別のアプリドメインにロードしたことを解決し、ファイルを再コンパイルすると、そのアプリドメインをアンロードして作成しました新しいもの。
これは、長期間使用した後(多くの再コンパイルサイクル)にメモリフットプリントをシミュレートするために使用している入力方法です。私はこれを短期間実行すると、すぐに500MB以上を消費します。
これは、一時ディレクトリに2つのダミースクリプトがある場合のみです。
public static void main( String[ ] args ) throws Exception {
for ( int i = 0; i < 1000; i++ ) {
Container[ ] containers = getScriptContainers( );
Script[ ] scripts = compileScripts( containers );
for ( Script s : scripts ) s.Begin( );
Thread.sleep( 1000 );
}
}
これは、スクリプトファイルのリストを収集するために使用している一時的な方法です。本番環境では、これらは実際には、データベースからのクラス名などの他の情報を含むバイト配列としてロードされます。
@Deprecated
private static Container[ ] getScriptContainers( ) throws IOException {
File root = new File( "C:\\Scripts\\" );
File[ ] files = root.listFiles( );
List< Container > containers = new ArrayList<>( );
for ( File f : files ) {
String[ ] tokens = f.getName( ).split( "\\.(?=[^\\.]+$)" );
if ( f.isFile( ) && tokens[ 1 ].equals( "java" ) ) {
byte[ ] fileBytes = Files.readAllBytes( Paths.get( f.getAbsolutePath( ) ) );
containers.add( new Container( tokens[ 0 ], fileBytes ) );
}
}
return containers.toArray( new Container[ 0 ] );
}
これは単純なコンテナクラスです。
public class Container {
private String className;
private byte[ ] classFile;
public Container( String name, byte[ ] file ) {
className = name;
classFile = file;
}
public String getClassName( ) {
return className;
}
public byte[ ] getClassFile( ) {
return classFile;
}
}
これは、.javaファイルをコンパイルし、それらをScriptオブジェクトにインスタンス化する実際のメソッドです。
private static Script[ ] compileScripts( Container[ ] containers ) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
List< ClassFile > sourceScripts = new ArrayList<>( );
for ( Container c : containers )
sourceScripts.add( new ClassFile( c.getClassName( ), c.getClassFile( ) ) );
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler( );
JavaFileManager manager = new MemoryFileManager( compiler.getStandardFileManager( null, null, null ) );
compiler.getTask( null, manager, null, null, null, sourceScripts ).call( );
List< Script > compiledScripts = new ArrayList<>( );
for ( Container c : containers )
compiledScripts.add( ( Script )manager.getClassLoader( null ).loadClass( c.getClassName( ) ).newInstance( ) );
return ( Script[ ] )compiledScripts.toArray( new Script[ 0 ] );
}
これは、コンパイラー用に作成したカスタムJavaFileManager実装であり、出力を物理.classファイルではなくメモリーに格納できます。
public class MemoryFileManager extends ForwardingJavaFileManager< JavaFileManager > {
private HashMap< String, ClassFile > classes = new HashMap<>( );
public MemoryFileManager( StandardJavaFileManager standardManager ) {
super( standardManager );
}
@Override
public ClassLoader getClassLoader( Location location ) {
return new SecureClassLoader( ) {
@Override
protected Class< ? > findClass( String className ) throws ClassNotFoundException {
if ( classes.containsKey( className ) ) {
byte[ ] classFile = classes.get( className ).getClassBytes( );
return super.defineClass( className, classFile, 0, classFile.length );
} else throw new ClassNotFoundException( );
}
};
}
@Override
public ClassFile getJavaFileForOutput( Location location, String className, Kind kind, FileObject sibling ) {
if ( classes.containsKey( className ) ) return classes.get( className );
else {
ClassFile classObject = new ClassFile( className, kind );
classes.put( className, classObject );
return classObject;
}
}
}
これは、ソースの.javaファイルとコンパイルされた.classファイルをメモリに格納するために使用する多目的SimpleJavaFileObject実装です。
public class ClassFile extends SimpleJavaFileObject {
private byte[ ] source;
protected final ByteArrayOutputStream compiled = new ByteArrayOutputStream( );
public ClassFile( String className, byte[ ] contentBytes ) {
super( URI.create( "string:///" + className.replace( '.', '/' ) + Kind.SOURCE.extension ), Kind.SOURCE );
source = contentBytes;
}
public ClassFile( String className, CharSequence contentCharSequence ) throws UnsupportedEncodingException {
super( URI.create( "string:///" + className.replace( '.', '/' ) + Kind.SOURCE.extension ), Kind.SOURCE );
source = ( ( String )contentCharSequence ).getBytes( "UTF-8" );
}
public ClassFile( String className, Kind kind ) {
super( URI.create( "string:///" + className.replace( '.', '/' ) + kind.extension ), kind );
}
public byte[ ] getClassBytes( ) {
return compiled.toByteArray( );
}
public byte[ ] getSourceBytes( ) {
return source;
}
@Override
public CharSequence getCharContent( boolean ignoreEncodingErrors ) throws UnsupportedEncodingException {
return new String( source, "UTF-8" );
}
@Override
public OutputStream openOutputStream( ) {
return compiled;
}
}
そして最後に、シンプルなスクリプトインターフェイス。
public interface Script {
public void Begin( ) throws Exception;
}
プログラミングに関してはまだ新しいのですが、私が遭遇した小さな問題のいくつかの解決策を見つけるためにしばらくスタックを使用しました。これは質問をするのが初めてなので、情報が多すぎると謝罪しますまたは、これが長すぎる場合。私は自分が徹底していることを確認したかっただけです。
アプリケーションのデフォルトのクラスローダーを使用して、コンパイルされたクラスをロードしているようです。これにより、クラスのガベージコレクションが不可能になります。
そのため、新しくコンパイルしたクラス用に別のクラスローダーを作成する必要があります。これがアプリサーバーが行う方法です。
ただし、コンパイルされたクラスに個別のクラスローダーを使用する場合でも、クラスローダーとそれがロードしたすべてのクラスは、単一の限り、ガベージコレクションの対象にならないため、ガベージコレクションでそれらのクラスを取得するのは難しい場合があります。これらのクラスのインスタンスは、他の場所(つまり、アプリケーションの残りの部分)で参照されます。
これは、クラスローダーリークと呼ばれ、アプリサーバーの一般的な問題です。これにより、再デプロイでさらに多くのメモリが使用され、最終的に失敗します。クラスローダーのリークの診断と修正は非常に難しい場合があります。記事にはすべての詳細があります。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加