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

12

我正在创建一个长时间运行的Java应用程序,需要在不关闭应用程序的情况下更新功能。我决定通过以.java文件的形式加载更新的功能(从数据库中作为字节数组获取),在内存中进行编译和实例化来提供这些更新的功能。如果你有更好的方法,我很乐意听取建议。

我遇到的问题是,在人工环境中测试时,每次加载这些“脚本”循环时,内存占用会略微增加。

注意:这实际上是我第一次使用Java或做类似的事情。我之前在C#中完成了类似的任务,通过加载和卸载.cs文件来实现,并且也有内存占用问题...... 为了解决这个问题,我将其加载到单独的应用程序域中,当重新编译文件时,我只需卸载该应用程序域并创建一个新的。

入口点


这是我使用的入口方法来模拟长时间使用后的内存占用情况(多次重新编译循环)。我运行它一段时间,它很快就会占用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< 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 ] );
}

MemoryFileManager类


这是我为编译器创建的自定义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;
        }
    }
}

ClassFile类


这是我用来存储源代码.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;
}

我在编程方面还是新手,遇到一些小问题时,我已经使用堆栈一段时间来寻找解决方法。这是我第一次提问,如果我包含了太多信息或者内容太长,我很抱歉;我只是想确保我讲清楚了问题的全部细节。


你如何衡量内存占用?在Java中,程序实际使用的内存和它仅仅为了可用而保留的内存之间存在着深刻的差异。 - Joonas Pulakka
如果问题提得清晰明了,我会很感兴趣看到答案。你有没有碰巧看过Java反射? - FloppyDisk
我在任务管理器中注意到特定的javaw.exe进程增加了内存使用量后,开始使用Eclipse Memory Analyzer。似乎垃圾收集器没有采取任何措施来收集未使用的残留物...另外,如果我删除睡眠并将其设置为while(true),由于内存不足而崩溃。 - Jordan
我没有看到任何明显的问题。你正确地为每个类使用了单独的类加载器,这是我的第一个猜测。你可以尝试的一件事是转储和分析堆,以查看哪些类占用了所有内存以及它们如何与gc根链接。一个快速而简单的方法是使用jps获取进程ID,使用jmap转储堆,并使用jhat进行分析。 - Russell Zahniser
对于一个写得好的问题,点个赞。谢谢! - ArjunShankar
1个回答

5
您似乎正在使用应用程序的默认类加载器来加载编译好的类 - 这使得这些类无法被垃圾回收。
因此,您必须为您刚刚编译的类创建单独的类加载器。这就是应用服务器的做法。
然而,即使您为编译好的类使用了单独的类加载器,要让这些类被垃圾回收也可能很棘手,因为只要任何一个这些类的实例被引用到其他地方(例如您的应用程序的其余部分),类加载器和它所加载的所有类都不符合垃圾回收条件。
这被称为类加载器泄漏,是应用服务器常见的问题,会导致重新部署使用更多的内存,并最终失败。诊断和修复类加载器泄漏可能非常棘手;该文章详细介绍了全部内容。

感谢提供这些有用的链接,特别是关于类加载器泄漏的那个。 - Jordan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接