2012-03-27 78 views
12

我正在创建一个将长时间运行的java应用程序,这需要更新的功能而不需要关闭。我决定通过以.java文件的形式加载它(作为数据库中的字节数组)来提供这种更新的功能,这些文件在内存中编译并实例化。如果你有一个更好的方式,我全是耳朵。Java动态加载和卸载.java文件,垃圾收集?

我在跑的问题是,内存占用与当我做在人工环境中一些测试加载这些“脚本”的每个周期略有增加。

注意:这实际上是我第一次做这样的事情,或者根本就不用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<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文件,并将其实例化到脚本对象的实际方法。

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的类


这是我的多用途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; 
    } 
} 

脚本接口


最后是简单脚本接口。

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

我仍然是一种新的,当涉及到编程,我已经使用的堆栈一会儿就找到我所遇到的小问题了一些解决方案,这是我第一次问一个问题,所以我道歉如果我包含了太多的信息或者这太长了,我只是想确保我彻底。

+0

你如何测量内存占用?在Java中,程序实际使用*的内存量有多大,以及它可以使用多少内存*只是因为它可以。 – 2012-03-27 12:58:26

+0

对于一个精心布置的问题+1,我有兴趣看到这个答案。你有机会看过Java反射吗? – FloppyDisk 2012-03-27 13:00:08

+0

我在使用Eclipse Memory Analyzer注意到我的任务管理器中特定javaw.exe进程的内存使用增加。看起来好像垃圾收集器没有做任何事情来收集未使用的残余物......还有一个事实是,如果我删除睡眠并将其设置为while(true),则由于内存不足而崩溃。 – Jordan 2012-03-27 13:04:25

回答

5

您似乎在使用应用程序的默认类加载器来加载编译后的类 - 这使得无法收集类。

所以你必须create a separate classloader为你的新编译的类。这是应用程序服务器如何做到的。

但是,即使您为已编译的类使用单独的类加载器,但通过垃圾回收获取这些类可能会非常棘手,因为它加载的类加载器及其所有类都不适合垃圾回收只要任何这些类的单个实例在其他任何地方被引用(即您的应用程序的其余部分)。

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

+0

感谢您提供丰富的链接,特别是关于classloader泄漏的链接。 – Jordan 2012-03-27 16:06:25