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.