線上一個項目,每次機器重啓時項目都會報出大量的Timeout,同時每一個集羣節點都被監控到較爲頻繁的Full GC。以後同事雖然嘗試過JVM調優並適當調大了老年代空間,但依然不能根本上解決問題。當時該問題被初步歸咎於系統中整合的Groovy,但並未證明。問題彙總以下:java
隨後,我着手作另一個項目GLUE,該項目一樣須要整合Groovy,在作併發測試時,我發現了一樣的問題。緩存
通過排查並作出優化,新項目GLUE在併發測試下基本不存在Full GC的問題,在此將問題處理過程記錄以下,但願能夠給你們一點參考。數據結構
新系統GLUE底層基於Groovy實現,系統經過執行 「groovy.lang.GroovyClassLoader.parseClass(groovyScript)」 進行Groovy代碼解析,Groovy爲了保證解析後執行的都是最新的腳本內容,每進行一次解析都會生成一次新命名的Class文件,底層代碼以下圖:併發
所以,若是Groovy類加載器設置爲單例,當對腳本(即便同一段腳本)屢次執行該方法時,會致使 「GroovyClassLoader」 裝載的Class愈來愈多。若是此處臨時加載的類不可以被及時釋放,最終將會致使PermGen OutOfMemoryError。即便狀況沒有那麼糟糕,也會引發頻繁的full GC,從而影響穩定運行時的性能。app
而後,我翻閱了線上啓動時大量Timeout以及Full GC的項目代碼。發現該項目一樣適用「GroovyClassLoader」進行groovy腳本解析,斷點接入以下:異步
首先,我發現該項目中的Groovy類加載器是單例; 其次,該項目中的加載一次頁面,將會調用多達31次「groovy.lang.GroovyClassLoader.parseClass(groovyScript)」方法進行groovy腳本解析。這很震驚,可是慶幸的是,該系統對解析後的Class作了緩存。tcp
通過分析,該項目啓動是被報大量Timeout和運行Full GC的問題基本鎖定,緣由以下:ide
啓動時Timeout緣由:項目啓動完成後,該節點通過健康檢查無誤被切到線上集羣環境,接收線上流量。可是,因爲該項目上單個頁面模塊太多,上文中一張頁面加載須要執行解析函數多達31次,並且該項目還託管這許多其餘的頁面,這致使這些頁面的預熱時間比較久。可是不幸的是,項目已經經過了健康檢查,大量流量涌入阻塞等待頁面加載完成,所以致使項目啓動時被報大量Timeout。函數
頻繁Full GC緣由:該項目中Groovy類加載使用單例,當對腳本(即便同一段腳本)屢次執行該方法時,會致使 「GroovyClassLoader」 裝載的Class愈來愈多。若是此處臨時加載的類不可以被及時釋放,最終將會致使PermGen OutOfMemoryError。即便狀況沒有那麼糟糕,也會引發頻繁的full GC,從而影響穩定運行時的性能。性能
爲了對上述猜測進行驗證,設計了一下三段代碼進行簡單測試。代碼邏輯分別爲:
本文中測試方法爲,啓動下面三段測試代碼中的Main方法,經過查看各自JVM的GC狀況從而驗證GroovyClassLoader對JVM的影響。
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import groovy.lang.GroovyClassLoader; public class Test { public static void main(String[] args) throws InterruptedException, IOException { final String code = readAll("DemoHandlerAImpl.groovy"); final AtomicInteger count = new AtomicInteger(0); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } Object object = parseClass(code); System.out.println("COUNT1=" + count.incrementAndGet() + ", " + object.hashCode()); } } }); } } static GroovyClassLoader classLoader = new GroovyClassLoader(); public static Object parseClass(String code){ return classLoader.parseClass(code); } public static String readAll(String logFile){ try { InputStream ins = null; BufferedReader reader = null; try { ins = new FileInputStream(Thread.currentThread().getContextClassLoader().getResource(logFile).getPath()); reader = new BufferedReader(new InputStreamReader(ins, "utf-8")); if (reader != null) { String content = null; StringBuilder sb = new StringBuilder(); while ((content = reader.readLine()) != null) { sb.append(content).append("\n"); } return sb.toString(); } } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import groovy.lang.GroovyClassLoader; public class Test2 { public static void main(String[] args) throws InterruptedException, IOException { final String code = readAll("DemoHandlerAImpl.groovy"); final AtomicInteger count = new AtomicInteger(0); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } Object object = parseClass(code); System.out.println("COUNT2=" + count.incrementAndGet() + ", " + object.hashCode()); } } }); } } static GroovyClassLoader classLoader = new GroovyClassLoader(); public static Object parseClass(String code){ classLoader = new GroovyClassLoader(); return classLoader.parseClass(code); } public static String readAll(String logFile){ try { InputStream ins = null; BufferedReader reader = null; try { ins = new FileInputStream(Thread.currentThread().getContextClassLoader().getResource(logFile).getPath()); reader = new BufferedReader(new InputStreamReader(ins, "utf-8")); if (reader != null) { String content = null; StringBuilder sb = new StringBuilder(); while ((content = reader.readLine()) != null) { sb.append(content).append("\n"); } return sb.toString(); } } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import groovy.lang.GroovyClassLoader; public class Test3 { public static void main(String[] args) throws InterruptedException, IOException { final String code = readAll("DemoHandlerAImpl.groovy"); final Object object = parseClass(code); final AtomicInteger count = new AtomicInteger(0); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("COUNT3=" + count.incrementAndGet() + ", " + object.hashCode()); } } }); } } static GroovyClassLoader classLoader = new GroovyClassLoader(); public static Object parseClass(String code){ classLoader = new GroovyClassLoader(); return classLoader.parseClass(code); } public static String readAll(String logFile){ try { InputStream ins = null; BufferedReader reader = null; try { ins = new FileInputStream(Thread.currentThread().getContextClassLoader().getResource(logFile).getPath()); reader = new BufferedReader(new InputStreamReader(ins, "utf-8")); if (reader != null) { String content = null; StringBuilder sb = new StringBuilder(); while ((content = reader.readLine()) != null) { sb.append(content).append("\n"); } return sb.toString(); } } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * 場景A:託管 「配置信息」 ,尤爲適用於數據結構比較複雜的配置項 * 優勢:在線編輯;推送更新;+ 直觀; * @author xuxueli 2016-4-14 15:36:37 */ public class DemoHandlerAImpl { public Object handle(Map<String, Object> params) { // 【基礎類型配置】 boolean ifOpen = true; // 開關 int smsLimitCount = 3; // 短信發送次數閥值 String brokerURL = "failover:(tcp://127.0.0.1:61616,tcp://127.0.0.2:61616)"; // 套接字配置 // 【列表配置】 Set<Integer> blackShops = new HashSet<Integer>(); // 黑名單列表 blackShops.add(15826714); blackShops.add(15826715); blackShops.add(15826716); blackShops.add(15826717); blackShops.add(15826718); blackShops.add(15826719); // 【KV配置】 Map<Integer, String> emailDispatch = new HashMap<Integer, String>(); // 不一樣BU標題文案配置 emailDispatch.put(555, "淘寶"); emailDispatch.put(666, "天貓"); emailDispatch.put(777, "聚划算"); // 【複雜集合配置】 Map<Integer, List<Integer>> openCitys = new HashMap<Integer, List<Integer>>(); // 不一樣城市推薦商戶配置 openCitys.put(11, Arrays.asList(15826714, 15826715)); openCitys.put(22, Arrays.asList(15826714, 15651231, 86451231)); openCitys.put(33, Arrays.asList(48612323, 15826715)); return smsLimitCount; } }
從日誌能夠發現,共解析groovy達38694次。
從日誌能夠發現,共解析groovy達39100次。
從日誌能夠發現,共解析groovy達40000次。
經過觀察內存曲線圖,能夠獲取測試結果:
從上述測試結果能夠獲得結論:
PermGen中對象回收規則:ClassLoader能夠被回收,其下的全部加載過的沒有對應實例的類信息(保存在PermGen)可被回收。所以,JVM回收以後,能夠將GroovyClassLoader加載的冗餘新信息回收掉。
可是。GC在JVM中一般是由一個或一組進程來實現的,它自己也和用戶程序同樣佔用heap空間,運行時也佔用CPU。所以,當GC運行時間較長時,用戶可以感到 Java程序的停頓。所以,儘可能避免GC。