Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java loading and unloading .java files dynamically, garbage collection?

I am in the process of creating a java application that will be running for long periods of time which requires updated functionality without shutting down. I've decided to provide this updated functionality by loading it in the form of .java files (pulled as a byte array from a database) which are compiled in memory and instantiated. If you have a better way I am all ears.

The problem I have run in to is that memory footprint increases slightly with each cycle of loading these "scripts" when I do some testing in an artificial environment.

Note: This is actually my first time doing something like this or much at all with java. I had accomplished something like this before in C# with loading and unloading .cs files and also had memory footprint issues there... to solve that I loaded them into a separate appdomain and when I recompiled the files I just unloaded that appdomain and created a new one.

Entry point


This is the entry method that I am using to simulate the memory footprint after long periods of use (many recompile cycles). I run this for a short period of time and it quickly eats up 500MB+.

This is only with two dummy scripts in the temporary directory.

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

Collecting a list of scripts (temporary)


This is the temporary method I am using to collect a list of the script files. During production these will actually be loaded as byte arrays with some other information like the class name from a database.

@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 ] );
 }

Container class


This is the simple container class.

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

Compiling the scripts


This is the actual method that compiles the .java files and instantiates them into Script objects.

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 class


This is the custom JavaFileManager implementation that I created for the compiler so that I can store the output in memory rather than in physical .class files.

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 class


This is my multi-purpose SimpleJavaFileObject implementation that I use to store the source .java files and the compiled .class files in memory.

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 interface


And lastly the simple Script interface.

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

I'm still kind of new when it comes to programming and I have used the stack for a while to find some solutions to small problems I have encountered, this is my first time asking a question so I apologize if I have included too much information or if this is too long; I just wanted to make sure I was thorough.

like image 460
Jordan Avatar asked Mar 27 '12 12:03

Jordan


1 Answers

You seem to be using the application's default classloader to load the compiled classes - that makes it impossible for the classes to be garbage collected.

So you have to create a separate classloader for your freshly compiled classes. This is how app servers do it.

However, even if you use a separate classloader for your compiled classes, it can be tricky to get those classes to be picked up by garbage collection, because the classloader and all the classes it loaded are not eligible for garbage collcetion as long as a single instance of any of those classes is referred to anywhere else (i.e. the rest of your application).

This is known as a classloader leak and a common problem with appservers, causing redeployments to use ever more memory and eventually fail. Diagnosing and fixing a classloader leak can be very tricky; the article has all the details.

like image 104
Michael Borgwardt Avatar answered Oct 20 '22 09:10

Michael Borgwardt