我正在尝试在运行时动态加载JRuby(因此我可以使用任意JRuby安装和版本来执行Ruby代码)。我的计划大致是创建一个可以访问jruby.jar
的ClassLoader,然后用它来加载必要的JRuby运行时等。一切都很好,直到我需要多次这样做。如果我销毁第一个JRuby运行时,第三个或第四个将导致OutOfMemory:PermGen空间。
我把它简化为一个最小的例子。该示例使用“直接”API以及JRuby Embed API。 “直接”API部分已注释掉,但两者都表现出相同的行为:经过几次迭代后,PermGen内存不足。 (使用JRuby 1.6.7和JRuby 1.6.5.1测试)
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import org.junit.Test;
public class JRubyInstantiationTeardownTest {
@Test
public void test() throws Exception {
for (int i = 0; i < 100; ++i) {
URL[] urls = new URL[] {
new URL("file://path/to/jruby-1.6.7.jar")
};
ClassLoader cl = new URLClassLoader(urls, this.getClass().getClassLoader());
// "Direct" API
/*
Class<?> klass = cl.loadClass("org.jruby.Ruby");
Method newInstance = klass.getMethod("newInstance");
Method evalScriptlet = klass.getMethod("evalScriptlet", String.class);
Method tearDown = klass.getMethod("tearDown");
Object runtime = newInstance.invoke(null);
System.out.println("have " + runtime);
evalScriptlet.invoke(runtime, "puts 'hello, world'");
tearDown.invoke(runtime);
*/
// JRuby Embed API
Class<?> scriptingContainerClass = cl.loadClass("org.jruby.embed.ScriptingContainer");
Method terminate = scriptingContainerClass.getMethod("terminate");
Method runScriptlet = scriptingContainerClass.getMethod("runScriptlet", String.class);
Object container = scriptingContainerClass.newInstance();
System.out.println("have " + container);
runScriptlet.invoke(container, "puts 'hello, world'");
terminate.invoke(container);
}
}
}
问题:尝试使用ClassLoader是否合理?如果是这样,这是JRuby中的一个错误,还是我在加载类时出错?
Bonus:如果这是JRuby中的一个错误,Eclipse Memory Analysis工具如何帮助找到源代码?我可以打开一个堆转储并看到几个Ruby对象(我希望在任何给定时间不超过一个),但我不知道如何找到为什么这些不被垃圾收集......
答案 0 :(得分:1)
尝试查看stackoverflow:loading classes with different classloaders to unload them from the JVM when not needed以及那里的引用。成熟的Web容器(如Tomcat)的源代码应该在加载/卸载堆栈中的某个地方找到问题的答案。
PermGen存储加载类的字节码(以及生成的动态代理)。当所有对类及其类加载器的引用都被清除时,它应该被GC正确压缩。但是你的代码证明了某些东西可以阻止你的JRuby类从主类加载器中访问。它可能是某些回调映射,JRuby在加载时注册自己。
答案 1 :(得分:1)
编辑:将此报告为错误:JRUBY-6522,现已修复。
在Eclipse Memory Analyzer中挖掘后,我在其中一个URLClassLoader实例上单击了“path to GC”。它由org.jruby.RubyEncoding$2
引用,java.lang.ThreadLocal$ThreadLocalMap$Entry
引用。
查看该源文件,我看到正在创建一个静态ThreadLocal变量:RubyEncoding.java:266。 ThreadLocals可能永远存在,引用我的ClassLoader
和泄漏内存。
此代码示例成功:
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Test;
public class JRubyInstantiationTeardownTest {
public static int i;
@Test
public void test() throws Exception {
for (i = 0; i < 100; ++i) {
URL[] urls = new URL[] {
new URL("file:///home/pat/jruby-1.6.7/lib/jruby.jar")
};
final ClassLoader cl = new URLClassLoader(urls, this.getClass().getClassLoader());
final Class<?> rubyClass = cl.loadClass("org.jruby.Ruby");
final Method newInstance = rubyClass.getMethod("newInstance");
final Method evalScriptlet = rubyClass.getMethod("evalScriptlet", String.class);
final Method tearDown = rubyClass.getMethod("tearDown");
// "Direct" API
Callable<Void> direct = new Callable<Void>() {
public Void call() throws Exception {
// created inside thread because initialization happens immediately
final Object ruby = newInstance.invoke(null);
System.out.println("" + i + ": " + ruby);
evalScriptlet.invoke(ruby, "puts 'hello, world'");
tearDown.invoke(ruby);
return null;
}
};
// JRuby Embed API
final Class<?> scriptingContainerClass = cl.loadClass("org.jruby.embed.ScriptingContainer");
final Method terminate = scriptingContainerClass.getMethod("terminate");
final Method runScriptlet = scriptingContainerClass.getMethod("runScriptlet", String.class);
// created outside thread because ruby instance not created immediately
final Object container = scriptingContainerClass.newInstance();
Callable<Void> embed = new Callable<Void>() {
public Void call() throws Exception {
System.out.println(i + ": " + container);
runScriptlet.invoke(container, "puts 'hello, world'");
terminate.invoke(container);
return null;
}
};
// separate thread for each loop iteration so its ThreadLocal vars are discarded
final ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(direct).get();
executor.submit(embed).get();
executor.shutdown();
}
}
}
现在我想知道这是否是JRuby的可接受行为,或JRuby-Rack在servlet容器上下文中所做的事情,其中servlet容器正在管理自己的线程池来处理请求。似乎需要维护一个完全独立的线程池,只在这些线程中执行Ruby代码,然后确保在取消部署servlet时它们被销毁...
这非常相关:Tomcat Memory Leak Protection
另请参阅JVM错误报告:Provide reclaimable thread local values without Thread termination