天天看點

Java動态編譯優化——URLClassLoader 記憶體洩漏問題解決

一、動态編譯案例

要說動态編譯記憶體洩漏,首先我們先看一個案例(網上搜動态編譯的資料是千篇一律,隻管實作功能,不管記憶體洩漏,并且都恬不知恥的辨別為原創!!)

Java URLClassLoader 動态編譯案例:https://blog.csdn.net/huangshanchun/article/details/72835647

這篇文章和我google搜的其他文章、資料一樣,屬于JDK1.6以後的版本。确實能實作動态編譯并加載,但是卻存在嚴重的URLClassLoader記憶體洩漏的問題,并且存在SharedNameTable 和 ZipFileIndex的記憶體洩漏問題。

其中SharedNameTable問題我已經解決:參考

二、URLClassLoader問題分析和解決

1、問題發現

生産環境JVM的運作情況,OLD區爆滿,FULlGC不停的執行,項目大概2小時挂掉了,如下圖:

Java動态編譯優化——URLClassLoader 記憶體洩漏問題解決

在使用VisualVM和 JProfile 兩者工具遠端分析 測試環境和生産環境的項目後,轉儲堆Dump檔案,并轉存到本地分析。 發現動态編譯這塊存在URLClassLoader的記憶體洩漏,如下圖所示:

Java動态編譯優化——URLClassLoader 記憶體洩漏問題解決
Java動态編譯優化——URLClassLoader 記憶體洩漏問題解決

2、問題分析

URLClassLoader占了83%的記憶體空間,遂研究了一下動态編譯這塊的代碼,原案例代碼如下:

import javax.tools.*;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

public class DynamicCompile {
    private URLClassLoader parentClassLoader;
    private String classpath;
    public DynamicCompile() {
        this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();
        this.buildClassPath();// 存在動态安裝的問題,需要動态編譯類路徑
    }

    private void buildClassPath() {
        this.classpath = null;
        StringBuilder sb = new StringBuilder();
        for (URL url : this.parentClassLoader.getURLs()) {
            String p = url.getFile();
            sb.append(p).append(File.pathSeparator); //路徑分割符linux為:window系統為;
        }
        this.classpath = sb.toString();
    }
    /**
     * 編譯出類
     *
     * @param fullClassName 全路徑的類名
     * @param javaCode      java代碼
     *
     * @return 目标類
     */
    public Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

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

        List<String> options = new ArrayList<>();
        options.add("-encoding");
        options.add("UTF-8");
        options.add("-classpath");
        options.add(this.classpath);

        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);
            //加載至記憶體
            return dynamicClassLoader.loadClass(fullClassName, jco);
        } else {
            for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
                String error = compileError(diagnostic);
                throw new RuntimeException(error);
            }
            throw new RuntimeException("compile error");
        }
    }

    private String compileError(Diagnostic diagnostic) {
        StringBuilder res = new StringBuilder();
        res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");
        res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");
        res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");
        return res.toString();
    }
}           

複制

URLClassLoader這裡使用的是全局變量,并且是擷取的目前類的ClassLoader(總的) ,在最後加載完class後,并沒有關閉操作

this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();           

複制

我想,那麼用完之後我給這個parentClassLoader進行close不就解決了? 我想的太簡單了。

切忌:此處的URLClassLoader不能關閉,因為用的是目前所在類的ClassLoader,如果你關閉了,那麼會導緻你目前程式的其他類會ClassNotFoundException

3、問題解決(三種)。

1、因為這裡使用的是源代碼的記憶體級動态編譯,即:

new CharSequenceJavaFileObject(fullClassName, javaCode)           

複制

是以,可以用自定義的FileManager 去擷取classLoader ,參考:https://www.cnblogs.com/whuqin/p/4981948.html

但是這裡因為是用的ClassLoader而不是URLClassLoader,其實也沒法進行close。具體我沒去測試有沒有記憶體洩漏。

2、也可以使用源代碼的檔案級動态編譯,去擷取檔案對應的URLClassLoader。

3、既然不能關閉全局的ClassLoader,又想用URLClassLoader,看了官網URLClassLoader的API後,想到其實可以自己new 一個URLClassLoader來處理動态編譯後的Class加載。 畢竟自己new出來的可以直接關閉,不會影響全局類的加載,具體如下:

package com.yunerp.web.util.run.compile;

import org.apache.log4j.Logger;
import sun.misc.ClassLoaderUtil;

import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import java.io.File;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;


public class DynamicEngine {

    private final Logger log = Logger.getLogger(this.getClass().getName());


    /**
     * @MethodName	: 建立classpath
     * @Description
     */
    private String buildClassPath() {
        StringBuilder sb = new StringBuilder();
        URLClassLoader parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();
        for (URL url : parentClassLoader.getURLs()) {
            String p = url.getFile();
            sb.append(p).append(File.pathSeparator);
        }
        return sb.toString();
    }

    /**
     * @param fullClassName 類名
     * @param javaCode      類代碼
     * @return Object
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @MethodName    : 編譯java代碼到Object
     * @Description
     */
    public Class javaCodeToObject(String fullClassName, final String javaCode) throws IllegalAccessException, InstantiationException {

        DynamicClassLoader dynamicClassLoader = null;
        ClassFileManager fileManager = null;
        List<JavaFileObject> jfiles = null;
        JavaClassObject jco = null;
        URLClassLoader urlClassLoader = null;
        try {
            //擷取系統編譯器
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            
            // 建立DiagnosticCollector對象
            DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
            //設定系統屬性
            System.setProperty("useJavaUtilZip", "true");
            // 建立用于儲存被編譯檔案名的對象
            // 每個檔案被儲存在一個從JavaFileObject繼承的類中
            fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

            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(buildClassPath());
            //不使用SharedNameTable (jdk1.7自帶的軟引用,會影響GC的回收,jdk1.9已經解決)
            options.add("-XDuseUnsharedTable");
            //設定使用javaUtilZip,避免zipFileIndex洩漏
            options.add("-XDuseJavaUtilZip");

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

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

            if (success) {
                //如果編譯成功,用類加載器加載該類
                jco = fileManager.getJavaClassObject();
                URL[] urls = new URL[]{new File("").toURI().toURL()};
                //擷取類加載器(每一個檔案一個類加載器)
                urlClassLoader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
                dynamicClassLoader = new DynamicClassLoader(urlClassLoader);
                Class clazz = dynamicClassLoader.loadClass(fullClassName, jco);
                return clazz;
            } else {
                log.error("編譯失敗: "+ fullClassName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //解除安裝ClassLoader所加載的類
                if (dynamicClassLoader != null) {
                    dynamicClassLoader.close();
                    ClassLoaderUtil.releaseLoader(dynamicClassLoader);
                }
                if (urlClassLoader != null) {
                    urlClassLoader.close();
                }
                if (fileManager != null) {
                    fileManager.flush();
                    fileManager.close();
                }
                if (jco != null) {
                    jco.close();
                }
                jfiles = null;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}           

複制

重新釋出後,測試1天的結果如下:

Java動态編譯優化——URLClassLoader 記憶體洩漏問題解決

至此:URLClassLoader問題解決,JVM的 OLD區正常,項目能正常運作一周左右(之前是2-4小時就記憶體洩漏挂掉了)

補充說明:

1、我這裡使用URLClassLoader是new的一個空檔案流,為什麼選擇這麼做,因客觀原因,必須要用源代碼的記憶體級動态編譯,這樣我無法擷取到檔案的具體全路徑。

2、其實可以優化的更徹底,即我去除options參數裡面的classpath,這樣就能不用全局的ClassLoader了, 一般來說,隻要配置了環境變量CLASSPATH,項目運作就能擷取到,但是不知道是否是伺服器環境問題,開發和測試環境Linux沒法取到classpath,導緻編譯失敗。是以這裡我還是保留了buildClassPath()方法。但是總體效果還是很明顯了,雖然我有點強迫症。隻能等後續有時間了再去研究了。

3、另外,代碼中我加上了關于useJavaUtilZip的配置,以為能解決ZipFileIndex的問題,但是實際上這個問題仍然存在,但是影響不是那麼大,等待後續或者其他人來研究了。

4、代碼規範我沒去格式化了,其實應該進行格式化一下,該封裝方法的還是封裝一下的好。

5、請各位看官尊重我的勞動成果,如轉載,請标明原作位址,并在評論告知我一聲,謝謝~