2012-03-02 46 views
2

我試圖在運行時動態加載JRuby(所以我可以使用任意JRuby安裝和版本執行Ruby代碼)。我的計劃大致是創建一個可以訪問jruby.jar的ClassLoader,然後使用它加載必要的JRuby運行時等。一切都很好,直到我需要多次執行此操作。如果我摧毀了第一個JRuby運行時,第三或第四個將導致OutOfMemory:PermGen空間。在運行時加載JRuby和ClassLoader泄漏

我已經減少了這個例子。該示例使用「直接」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對象(在任何給定時間,我預計不會超過一個),但我不知道如何找到爲什麼這些不會被垃圾收集...

+0

您應該添加指向JRuby錯誤報告的鏈接作爲答案並接受它,因爲這看起來確實是JRuby錯誤。 – 2012-03-04 09:29:47

回答

1

編輯:報告這是一個bug:JRUBY-6522,現在已修復。

在Eclipse Memory Analyzer中進行了深入研究之後,我在其中一個URLClassLoader實例上單擊了「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的機架確實在servlet容器在servlet容器管理的是自己的上下文線程池來處理請求。這似乎是一個需要保持一個完全獨立的線程池,只有在那些線程執行Ruby代碼,然後確保當servlet被卸載它們被摧毀...

這是非常相關的:Tomcat Memory Leak Protection

另請參閱JVM錯誤報告:Provide reclaimable thread local values without Thread termination

1

嘗試着看一下stackoverflow:loading classes with different classloaders to unload them from the JVM when not needed以及那裏的參考資料。一個成熟的Web容器(如Tomcat)的來源應該可以在加載/卸載堆棧中的某處找到問題的答案。

PermGen存儲加載類(和生成的動態代理)的字節碼。當所有對類和類加載器的引用都被清除時,它應該被GC恰當地壓縮。但是你的代碼證明了一些東西讓你的JRuby類鎖定並可以從主類加載器訪問。它可能是某些類型的回調映射,JRuby在加載時註冊自己。