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小時掛掉了,如下圖:
在使用VisualVM和 JProfile 兩者工具遠端分析 測試環境和生產環境的專案後,轉儲堆Dump檔案,並轉存到本地分析。 發現動態編譯這塊存在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天的結果如下:
至此:URLClassLoader問題解決,JVM的 OLD區正常,專案能正常執行一週左右(之前是2-4小時就記憶體洩漏掛掉了)
補充說明:
1、我這裡使用URLClassLoader是new的一個空檔案流,為什麼選擇這麼做,因客觀原因,必須要用原始碼的記憶體級動態編譯,這樣我無法獲取到檔案的具體全路徑。
2、其實可以優化的更徹底,即我去除options引數裡面的classpath,這樣就能不用全域性的ClassLoader了, 一般來說,只要配置了環境變數CLASSPATH,專案執行就能獲取到,但是不知道是否是伺服器環境問題,開發和測試環境Linux沒法取到classpath,導致編譯失敗。所以這裡我還是保留了buildClassPath()方法。但是總體效果還是很明顯了,雖然我有點強迫症。只能等後續有時間了再去研究了。
3、另外,程式碼中我加上了關於useJavaUtilZip的配置,以為能解決ZipFileIndex的問題,但是實際上這個問題仍然存在,但是影響不是那麼大,等待後續或者其他人來研究了。
4、程式碼規範我沒去格式化了,其實應該進行格式化一下,該封裝方法的還是封裝一下的好。
5、請各位看官尊重我的勞動成果,如轉載,請標明原作地址,並在評論告知我一聲,謝謝~
相關文章
- Java動態編譯優化——ZipFileIndex記憶體洩漏問題分析解決Java編譯優化Index記憶體
- Java記憶體洩漏解決之道Java記憶體
- 解決記憶體洩漏(1)-ApacheKylin InternalThreadLocalMap洩漏問題分析記憶體Apachethread
- Android 效能優化 - 徹底解決記憶體洩漏Android優化記憶體
- 防範JAVA記憶體洩漏解決方案Java記憶體
- 1.記憶體優化(一)記憶體洩漏記憶體優化
- Java記憶體洩漏Java記憶體
- 如何解決JVM OutOfMemoryError記憶體洩漏問題?JVMError記憶體
- 【Java面試題】之記憶體洩漏Java面試題記憶體
- ThreadLocal記憶體洩漏問題thread記憶體
- AFN的記憶體洩漏問題記憶體
- redisson記憶體洩漏問題排查Redis記憶體
- Android效能優化篇之記憶體優化--記憶體洩漏Android優化記憶體
- handlder引起的記憶體洩漏問題以及解決辦法記憶體
- JVM調優——Java動態編譯過程中的記憶體溢位問題JVMJava編譯記憶體溢位
- 翻譯 | 理解Java中的記憶體洩漏Java記憶體
- iOS八種記憶體洩漏問題iOS記憶體
- iOS 11 textField記憶體洩漏問題iOS記憶體
- bea記憶體洩漏問題診斷記憶體
- HibernateDaoSupport 記憶體洩漏的問題!記憶體
- Android效能優化之記憶體洩漏Android優化記憶體
- Handler記憶體洩漏分析及解決記憶體
- Android記憶體優化(三)避免可控的記憶體洩漏Android記憶體優化
- 解決git記憶體洩露問題Git記憶體洩露
- 記憶體洩漏問題分析之非託管資源洩漏記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)Android優化記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)Android優化記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)Android優化記憶體
- [譯] Swift 中的記憶體洩漏Swift記憶體
- 關於PHP記憶體洩漏的問題PHP記憶體
- BufferedImage記憶體洩漏和溢位問題記憶體
- 【轉】Java的記憶體洩漏Java記憶體
- 效能優化——記憶體洩漏(1)入門篇優化記憶體
- 記憶體洩漏與排查流程——安卓效能優化記憶體安卓優化
- 效能優化——記憶體洩漏(2)工具分析篇優化記憶體
- Android 輕鬆解決記憶體洩漏Android記憶體
- Handler記憶體洩漏原因及解決方案記憶體
- 記憶體洩漏記憶體