Загрузка JRuby во время выполнения и утечка ClassLoader - PullRequest
2 голосов
/ 03 марта 2012

Я пытаюсь загрузить JRuby динамически во время выполнения (чтобы я мог выполнить код Ruby, используя произвольные установки и версии JRuby).Я планирую примерно создать ClassLoader, который имеет доступ к jruby.jar, затем использовать его для загрузки необходимой среды выполнения 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 или я что-то не так делаю с загрузкой моего класса?

Бонус: если бы это была ошибка в JRuby, как что-то вроде инструмента анализа памяти Eclipse может помочь найти источник?Я могу открыть дамп кучи и увидеть несколько объектов Ruby (где я ожидал бы не более одного в любой момент времени), но я не уверен, как выяснить, почему это не сборщик мусора ...

Ответы [ 2 ]

1 голос
/ 03 марта 2012

Редактировать: сообщалось об этом как об ошибке: JRUBY-6522 , теперь исправлено.

После поиска в Eclipse Memory Analyzer я щелкнул "путь к ГХ" на одном изЭкземпляры URLClassLoader.На него ссылалась 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 делает в контексте контейнера сервлета, где контейнер сервлета управляет своим собственным пулом потоков для обработки запросов.Кажется, что нужно поддерживать совершенно отдельный пул потоков, выполнять только код Ruby в этих потоках, а затем гарантировать, что они будут уничтожены, когда сервлет не развернут ...

Это очень важно: Защита от утечки памяти Tomcat

См. Также отчет об ошибке JVM: Предоставление локальных значений для исправляемых потоков без завершения потока

1 голос
/ 03 марта 2012

Попробуйте взглянуть на stackoverflow: загрузка классов различными загрузчиками классов, чтобы выгрузить их из JVM, когда в этом нет необходимости и ссылки оттуда. Источники зрелого веб-контейнера (такого как Tomcat) должны иметь ответы на вашу проблему где-нибудь в стеке загрузки / выгрузки.

PermGen хранит байт-код для загруженных классов (и сгенерированных динамических прокси). Он должен быть правильно уплотнен GC, когда все ссылки на классы и их загрузчик классов очищены. Но ваш код доказывает, что что-то держит ваши классы JRuby заблокированными и доступными из загрузчика основного класса. Это может быть какая-то карта обратного вызова, которую JRuby регистрирует при загрузке.

...