在运行时加载JRuby并且ClassLoader泄漏

时间:2012-03-02 22:24:14

标签: java ruby memory-leaks jruby classloader

我正在尝试在运行时动态加载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对象(我希望在任何给定时间不超过一个),但我不知道如何找到为什么这些不被垃圾收集......

2 个答案:

答案 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