2012-03-27 6 views
12

Estoy en el proceso de crear una aplicación java que se ejecutará durante largos períodos de tiempo y que requiere una funcionalidad actualizada sin necesidad de apagarla. He decidido proporcionar esta funcionalidad actualizada cargándola en forma de archivos .java (extraídos como una matriz de bytes de una base de datos) que se compilan en memoria y se crean instancias. Si tienes una mejor manera, soy todo oídos.Java carga y descarga de archivos .java dinámicamente, recolección de basura?

El problema al que me he encontrado es que la huella de memoria aumenta ligeramente con cada ciclo de carga de estos "scripts" cuando realizo algunas pruebas en un entorno artificial.

Nota: Esta es realmente la primera vez que hago algo así o mucho con java. Había logrado algo como esto antes en C# con la carga y descarga de archivos .cs y también tenía problemas de huella de memoria allí ... para resolver que los cargué en un dominio de aplicación separado y cuando volví a compilar los archivos, acabo de descargar ese dominio de aplicación y creé un uno nuevo.

Punto de entrada


Este es el método de entrada que estoy usando para simular la huella de memoria después de largos períodos de uso (muchos ciclos de recompilación). Corro esto por un corto período de tiempo y rápidamente come 500MB +.

Esto es solo con dos scripts simulados en el directorio temporal.

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

La toma de una lista de secuencias de comandos (temporales)


Este es el método temporal que estoy utilizando para recopilar una lista de los archivos de script. Durante la producción, estos se cargarán como matrices de bytes con otra información, como el nombre de clase de una base de datos.

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

clase Container


Esta es la clase de contenedor simple.

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

Compilación de los guiones


Este es el método real que compila los archivos .java y les crea una instancia en objetos de secuencias de comandos.

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

clase MemoryFileManager


Esta es la costumbre JavaFileManager aplicación que creé para el compilador para que pueda almacenar el resultado en la memoria en lugar de en los archivos .class físicas.

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

clase de archivo de clase


Ésta es mi multiusos SimpleJavaFileObject aplicación que utilizo para almacenar los archivos .java fuente y los archivos .class compilados en la memoria.

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

interfaz de comandos


Y por último la sencilla Guión interfaz.

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

Todavía estoy especie de nuevo cuando se trata de la programación y he utilizado la pila durante un tiempo para encontrar algunas soluciones a pequeños problemas que he encontrado, esta es la primera vez que hace una pregunta así que me disculpo si he incluido demasiada información o si esto es demasiado largo; Solo quería asegurarme de ser exhaustivo.

+0

¿Cómo se mide la huella de memoria? En Java, hay una gran diferencia entre la cantidad de memoria que el programa realmente * usa * y cuánto * se reserva para su uso * simplemente porque puede hacerlo. –

+0

+1 para una pregunta bien diseñada, estoy interesado en ver las respuestas a esto. ¿Has mirado la reflexión de Java, por casualidad? – FloppyDisk

+0

Estaba usando Eclipse Memory Analyzer después de notar el aumento en el uso de memoria para el proceso específico javaw.exe en mi administrador de tareas. Parece como si el recolector de basura no estuviese haciendo nada para recolectar los restos no utilizados ... También está el hecho de que si elimino la suspensión y la configuro en while (verdadero), se bloquea debido a la falta de memoria. – Jordan

Respuesta

5

Parece que está utilizando el cargador de clases predeterminado de la aplicación para cargar las clases compiladas, lo que hace que sea imposible que las clases sean basura.

Así que tiene que create a separate classloader para sus clases recién compiladas. Así es como lo hacen los servidores de aplicaciones.

Sin embargo, incluso si utiliza un cargador de clases separado para sus clases compiladas, puede ser complicado conseguir esas clases recogidas por basura, porque el cargador de clases y todas las clases cargadas no son elegibles para collcetion de basura siempre que se haga referencia a una sola instancia de cualquiera de esas clases en cualquier otro lugar (es decir, el resto de su aplicación).

Esto se conoce como classloader leak y es un problema común con los servidores de aplicaciones, lo que causa redesplegos para utilizar cada vez más memoria y, finalmente, fallar. Diagnosticar y arreglar una fuga de cargador de clase puede ser muy complicado; el artículo tiene todos los detalles.

+0

Gracias por los enlaces informativos, particularmente el que se encuentra en las filtraciones del cargador de clases. – Jordan

Cuestiones relacionadas