1. 程式人生 > >【Big Data 每日一題20180822】Java動態編譯優化——URLClassLoader 記憶體洩漏問題解決

【Big Data 每日一題20180822】Java動態編譯優化——URLClassLoader 記憶體洩漏問題解決

一、動態編譯案例

要說動態編譯記憶體洩漏,首先我們先看一個案例(網上搜動態編譯的資料是千篇一律,只管實現功能,不管記憶體洩漏,並且都恬不知恥的標識為原創!!)

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

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

二、URLClassLoader問題分析和解決

1、問題發現

生產環境JVM的執行情況,OLD區爆滿,FULlGC不停的執行,專案大概2小時掛掉了,如下圖:

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

2、問題分析

URLClassLoader佔了83%的記憶體空間,遂研究了一下動態編譯這塊的程式碼,原案例程式碼如下:

  1. import javax.tools.*;

  2. import java.io.File;

  3. import java.net.URL;

  4. import java.net.URLClassLoader;

  5. import java.util.ArrayList;

  6. import java.util.List;

  7. public class DynamicCompile {

  8. private URLClassLoader parentClassLoader;

  9. private String classpath;

  10. public DynamicCompile() {

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

  12. this.buildClassPath();// 存在動態安裝的問題,需要動態編譯類路徑

  13. }

  14. private void buildClassPath() {

  15. this.classpath = null;

  16. StringBuilder sb = new StringBuilder();

  17. for (URL url : this.parentClassLoader.getURLs()) {

  18. String p = url.getFile();

  19. sb.append(p).append(File.pathSeparator); //路徑分割符linux為:window系統為;

  20. }

  21. this.classpath = sb.toString();

  22. }

  23. /**

  24. * 編譯出類

  25. *

  26. * @param fullClassName 全路徑的類名

  27. * @param javaCode java程式碼

  28. *

  29. * @return 目標類

  30. */

  31. public Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {

  32. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

  33. DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

  34. ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

  35. List<JavaFileObject> jfiles = new ArrayList<>();

  36. jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));

  37. List<String> options = new ArrayList<>();

  38. options.add("-encoding");

  39. options.add("UTF-8");

  40. options.add("-classpath");

  41. options.add(this.classpath);

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

  43. boolean success = task.call();

  44. if (success) {

  45. JavaClassObject jco = fileManager.getJavaClassObject();

  46. DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);

  47. //載入至記憶體

  48. return dynamicClassLoader.loadClass(fullClassName, jco);

  49. } else {

  50. for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {

  51. String error = compileError(diagnostic);

  52. throw new RuntimeException(error);

  53. }

  54. throw new RuntimeException("compile error");

  55. }

  56. }

  57. private String compileError(Diagnostic diagnostic) {

  58. StringBuilder res = new StringBuilder();

  59. res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");

  60. res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");

  61. res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");

  62. return res.toString();

  63. }

  64. }

URLClassLoader這裡使用的是全域性變數,並且是獲取的當前類的ClassLoader(總的) ,在最後載入完class後,並沒有關閉操作

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

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

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

3、問題解決(三種)。

1、因為這裡使用的是原始碼的記憶體級動態編譯,即:

new CharSequenceJavaFileObject(fullClassName, javaCode)

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

2、也可以使用原始碼的檔案級動態編譯,去獲取檔案對應的URLClassLoader。

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

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

  2. import org.apache.log4j.Logger;

  3. import sun.misc.ClassLoaderUtil;

  4. import javax.tools.DiagnosticCollector;

  5. import javax.tools.JavaCompiler;

  6. import javax.tools.JavaFileObject;

  7. import javax.tools.ToolProvider;

  8. import java.io.File;

  9. import java.net.URL;

  10. import java.net.URLClassLoader;

  11. import java.util.ArrayList;

  12. import java.util.List;

  13. public class DynamicEngine {

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

  15. /**

  16. * @MethodName : 建立classpath

  17. * @Description

  18. */

  19. private String buildClassPath() {

  20. StringBuilder sb = new StringBuilder();

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

  22. for (URL url : parentClassLoader.getURLs()) {

  23. String p = url.getFile();

  24. sb.append(p).append(File.pathSeparator);

  25. }

  26. return sb.toString();

  27. }

  28. /**

  29. * @param fullClassName 類名

  30. * @param javaCode 類程式碼

  31. * @return Object

  32. * @throws IllegalAccessException

  33. * @throws InstantiationException

  34. * @MethodName : 編譯java程式碼到Object

  35. * @Description

  36. */

  37. public Class javaCodeToObject(String fullClassName, final String javaCode) throws IllegalAccessException, InstantiationException {

  38. DynamicClassLoader dynamicClassLoader = null;

  39. ClassFileManager fileManager = null;

  40. List<JavaFileObject> jfiles = null;

  41. JavaClassObject jco = null;

  42. URLClassLoader urlClassLoader = null;

  43. try {

  44. //獲取系統編譯器

  45. JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

  46. // 建立DiagnosticCollector物件

  47. DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

  48. //設定系統屬性

  49. System.setProperty("useJavaUtilZip", "true");

  50. // 建立用於儲存被編譯檔名的物件

  51. // 每個檔案被儲存在一個從JavaFileObject繼承的類中

  52. fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

  53. jfiles = new ArrayList<>();

  54. jfiles.add(new CharSequenceJavaFileObject(fullClassName, javaCode));

  55. //使用編譯選項可以改變預設編譯行為。編譯選項是一個元素為String型別的Iterable集合

  56. List<String> options = new ArrayList<>();

  57. options.add("-encoding");

  58. options.add("UTF-8");

  59. options.add("-classpath");

  60. //獲取系統構建路徑

  61. options.add(buildClassPath());

  62. //不使用SharedNameTable (jdk1.7自帶的軟引用,會影響GC的回收,jdk1.9已經解決)

  63. options.add("-XDuseUnsharedTable");

  64. //設定使用javaUtilZip,避免zipFileIndex洩漏

  65. options.add("-XDuseJavaUtilZip");

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

  67. // 編譯源程式

  68. boolean success = task.call();

  69. if (success) {

  70. //如果編譯成功,用類載入器載入該類

  71. jco = fileManager.getJavaClassObject();

  72. URL[] urls = new URL[]{new File("").toURI().toURL()};

  73. //獲取類載入器(每一個檔案一個類載入器)

  74. urlClassLoader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());

  75. dynamicClassLoader = new DynamicClassLoader(urlClassLoader);

  76. Class clazz = dynamicClassLoader.loadClass(fullClassName, jco);

  77. return clazz;

  78. } else {

  79. log.error("編譯失敗: "+ fullClassName);

  80. }

  81. } catch (Exception e) {

  82. e.printStackTrace();

  83. } finally {

  84. try {

  85. //解除安裝ClassLoader所載入的類

  86. if (dynamicClassLoader != null) {

  87. dynamicClassLoader.close();

  88. ClassLoaderUtil.releaseLoader(dynamicClassLoader);

  89. }

  90. if (urlClassLoader != null) {

  91. urlClassLoader.close();

  92. }

  93. if (fileManager != null) {

  94. fileManager.flush();

  95. fileManager.close();

  96. }

  97. if (jco != null) {

  98. jco.close();

  99. }

  100. jfiles = null;

  101. } catch (Exception e) {

  102. e.printStackTrace();

  103. }

  104. }

  105. return null;

  106. }

  107. }

重新發布後,測試1天的結果如下:

至此:URLClassLoader問題解決,JVM的 OLD區正常,專案能正常執行一週左右(之前是2-4小時就記憶體洩漏掛掉了)

補充說明:

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

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

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