Java动态加载和卸载.java文件,垃圾收集?

我正在创建一个将长时间运行的Java应用程序,这需要更新的function而不会关闭。 我决定通过以.java文件(从数据库中拉出一个字节数组)的forms加载它来提供这个更新的function,这些文件在内存中编译并实例化。 如果你有一个更好的方式我全都耳朵。

我遇到的问题是,当我在人工环境中进行一些测试时,每次加载这些“脚本”的周期内存占用量会略有增加。

注意:这实际上是我第一次使用java做这样或类似的事情。 我之前在C#中已经完成了这样的事情,加载和卸载.cs文件,并且还有内存占用问题…解决我将它们加载到一个单独的appdomain中,当我重新编译文件时,我只是卸载了那个appdomain并创建了一个新的一个。

入口点


这是我在长时间使用(许多重新编译循环)后用来模拟内存占用的入口方法。 我在短时间内运行它很快就会吃掉500MB +。

这只是临时目录中的两个虚拟脚本。

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 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 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 compiledScripts = new ArrayList( ); for ( Container c : containers ) compiledScripts.add( ( Script )manager.getClassLoader( null ).loadClass( c.getClassName( ) ).newInstance( ) ); return ( Script[ ] )compiledScripts.toArray( new Script[ 0 ] ); } 

MemoryFileManager类


这是我为编译器创建的自定义JavaFileManager实现,因此我可以将输出存储在内存中而不是物理.class文件中。

 public class MemoryFileManager extends ForwardingJavaFileManager { private HashMap 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; } } } 

ClassFile类


这是我的多用途SimpleJavaFileObject实现,用于将源.java文件和已编译的.class文件存储在内存中。

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

脚本界面


最后是简单的Script界面。

 public interface Script { public void Begin( ) throws Exception; } 

在编程时我仍然有点新意,我已经使用堆栈一段时间来找到我遇到的小问题的解决方案,这是我第一次提出问题,所以如果我包含太多信息我会道歉或者如果这太长了; 我只是想确保我彻底。

您似乎正在使用应用程序的默认类加载器来加载已编译的类 – 这使得无法对类进行垃圾回收。

因此,您必须为新编译的类创建单独的类加载器 。 这就是app服务器的用途。

但是,即使您为已编译的类使用单独的类加载器,也可能很难通过垃圾收集来获取这些类,因为类加载器及其加载的所有类都不符合垃圾收集的条件,只要一个任何其他类的实例都被引用到其他地方(即你的应用程序的其余部分)。

这被称为类加载器泄漏和appservers的常见问题,导致重新部署使用更多内存并最终失败。 诊断和修复类加载器泄漏可能非常棘手; 这篇文章有所有细节。