JVM調優——Java動態編譯過程中的記憶體溢位問題

執筆記憶的空白發表於2018-12-06

由於測試環境專案每2小時記憶體就溢位一次, 分析問題,發現Java動態載入Class並執行那塊存在記憶體溢位問題, 遂本地調測。

一、找到動態編譯那塊的程式碼,具體如下

/**
     * @MethodName	: 編譯java程式碼到Object
     * @Description
     * @param fullClassName   類名
     * @param javaCode  類程式碼
     * @return Object
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public Class javaCodeToObject(String fullClassName, String javaCode) throws IllegalAccessException, InstantiationException {
        Object instance = null;
        //獲取系統編譯器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 建立DiagnosticCollector物件
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

        // 建立用於儲存被編譯檔名的物件
        // 每個檔案被儲存在一個從JavaFileObject繼承的類中
        ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

        List<JavaFileObject> jfiles = new ArrayList<>();
        jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));

        //使用編譯選項可以改變預設編譯行為。編譯選項是一個元素為String型別的Iterable集合
        List<String> options = new ArrayList<>();
        options.add("-encoding");
        options.add("UTF-8");
        options.add("-classpath");
        options.add(this.classpath);
        //不使用SharedNameTable (jdk1.7自帶的軟引用,會影響GC的回收,jdk1.9已經解決)
        options.add("-XDuseUnsharedTable");

        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);

        // 編譯源程式
        boolean success = task.call();

        if (success) {
            //如果編譯成功,用類載入器載入該類
            JavaClassObject jco = fileManager.getJavaClassObject();
            DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);
            Class clazz = dynamicClassLoader.loadClass(fullClassName,jco);
            try {
                dynamicClassLoader.close();
                //解除安裝ClassLoader所載入的類
                ClassLoaderUtil.releaseLoader(dynamicClassLoader);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return clazz;
        } else {
            //如果想得到具體的編譯錯誤,可以對Diagnostics進行掃描
            String error = "";
            for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
                error = error + compilePrint(diagnostic);
            }
        }
        return null;
    }

二、本地寫測試類,並且啟動執行

本地動態載入1000個類,測試檢視記憶體空間變化

 public static void main(String[] args) {

        String  code = "import java.util.HashMap;\n" +
                "import com.yunerp.web.vaadin.message.alert;\n" +
                "import java.util.List;\n" +
                "import java.util.ArrayList;\n" +
                "import com.yunerp.web.vaadin.util.modularfuntion.base.BaseUtil;\n" +
                "import com.yunerp.web.vaadin.util.function.TableFuntionUtil;\n" +
                "import com.yunerp.web.vaadin.util.modularfuntion.stoUtil.StoUtil;\n" +
                "import java.util.Map;import com.yunerp.web.vaadin.util.modularfuntion.user.mini.HomePageUtil;\n" +
                "import com.yunerp.web.util.run.WebInterface;\n" +
                "\n" +
                "public class web2905763164651825363 implements WebInterface {\n" +
                " public  Object execute(Map<String,Object> param) {\n" +
                " System.out.println(param.get(\"key\"));" +
                "  return null;\n" +
                " }\n" +
                "}";
        String name = "web2905763164651825363";

        for(int i=0;i<1000;i++){
            long time1 = System.currentTimeMillis();
            DynamicEngine de = DynamicEngine.getInstance();
            try {
                Class cl = de.javaCodeToObject(name,code);
                WebInterface webInterface = (WebInterface)cl.newInstance();
                Map<String,Object> param = new HashMap<>();
                param.put("key",i);
                webInterface.execute(param);
                
            }catch (Exception e) {
                e.printStackTrace();
            }
            System.gc();
            long time2 = System.currentTimeMillis();
            System.out.println("次數:"+i+"            time:"+(time2-time1));
        }
    }

 

三、使用JConsole和JVisualVM工具進行檢測。

工具的使用方法:JConsole和JVisualVM工具使用

本地專案啟動後,使用JConsole和 JVisualVM工具進行檢測,發現在動態載入類時, 堆空間記憶體直線上升,但是所載入的類和例項都被釋放了,而且ClassLoader也釋放了,但是記憶體還是在 上升,發現結果如下:

 

在檢視堆空間快照的時候,發現JDK自帶的  com.sun.tools.javac.util.SharedNameTable.NameImpl 類及其例項所在的記憶體空間比達到52%。  具體如下:

 

四、分析問題

查了很多文獻,也問了很多朋友,都對SharedNameTable這個類很陌生,最終還是在google上找到我想要的解答。具體如下兩個連結

連結:https://stackoverflow.com/questions/14617340/memory-leak-when-using-jdk-compiler-at-runtime  

大概意思是:

Java 7引入了這個錯誤:為了加速編譯,他們引入了SharedNameTable,它使用軟引用來避免重新分配,但不幸的是只會導致JVM膨脹失控,因為這些軟引用永遠不會被回收直到JVM達到-Xmx記憶體限制。據稱它將在Java 9中修復。與此同時,還有一個(未記錄的)編譯器選項來禁用它:-XDuseUnsharedTable

 

參考連結2:https://stackoverflow.com/questions/33548218/memory-leak-in-program-using-compiler-api

 

五、 記憶體溢位問題解決

在編譯選項options中加入 "-XDuseUnsharedTable" ,重新編譯執行,記憶體溢位問題解決

        //使用編譯選項可以改變預設編譯行為。編譯選項是一個元素為String型別的Iterable集合
        List<String> options = new ArrayList<>();
        options.add("-encoding");
        options.add("UTF-8");
        options.add("-classpath");
        options.add(this.classpath);
        //不使用SharedNameTable (jdk1.7自帶的軟引用,會影響GC的回收,jdk1.9已經解決)
        options.add("-XDuseUnsharedTable");

重新執行的效果圖如下:

至此,問題完美解決。

 

 

相關文章