一次FULL GC問題的排查

1、背景

線上一個項目,每次機器重啓時項目都會報出大量的Timeout,同時每一個集羣節點都被監控到較爲頻繁的Full GC。以後同事雖然嘗試過JVM調優並適當調大了老年代空間,但依然不能根本上解決問題。當時該問題被初步歸咎於系統中整合的Groovy,但並未證明。問題彙總以下:java

  • 問題一:項目啓動時報出大量Timeout;
  • 問題二:項目運行時,頻繁Full GC;

隨後,我着手作另一個項目GLUE,該項目一樣須要整合Groovy,在作併發測試時,我發現了一樣的問題。緩存

通過排查並作出優化,新項目GLUE在併發測試下基本不存在Full GC的問題,在此將問題處理過程記錄以下,但願能夠給你們一點參考。數據結構

2、分析

新系統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

3、分析結果

通過分析,該項目啓動是被報大量Timeout和運行Full GC的問題基本鎖定,緣由以下:ide

  • 啓動時Timeout緣由:項目啓動完成後,該節點通過健康檢查無誤被切到線上集羣環境,接收線上流量。可是,因爲該項目上單個頁面模塊太多,上文中一張頁面加載須要執行解析函數多達31次,並且該項目還託管這許多其餘的頁面,這致使這些頁面的預熱時間比較久。可是不幸的是,項目已經經過了健康檢查,大量流量涌入阻塞等待頁面加載完成,所以致使項目啓動時被報大量Timeout。函數

  • 頻繁Full GC緣由:該項目中Groovy類加載使用單例,當對腳本(即便同一段腳本)屢次執行該方法時,會致使 「GroovyClassLoader」 裝載的Class愈來愈多。若是此處臨時加載的類不可以被及時釋放,最終將會致使PermGen OutOfMemoryError。即便狀況沒有那麼糟糕,也會引發頻繁的full GC,從而影響穩定運行時的性能。性能

3、驗證

爲了對上述猜測進行驗證,設計了一下三段代碼進行簡單測試。代碼邏輯分別爲:

  • Test1.java:並行啓動100個線程,並行解析groovy腳本,使用單例類加載器;
  • Test2.java:並行啓動100個線程,並行解析groovy腳本,使用非單例類加載器;
  • Test3.java:並行啓動100個線程,並行打印log。

本文中測試方法爲,啓動下面三段測試代碼中的Main方法,經過查看各自JVM的GC狀況從而驗證GroovyClassLoader對JVM的影響。

代碼A:Test1.java
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;
	}
}
代碼2:Test2.java
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;
	}
}
代碼3:Test3.java
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;
	}
}
測試Groovy腳本:DemoHandlerAImpl.groovy
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;
	}

}
在系統運行四分鐘後,Test1.java對應JVM的GC如圖:

輸入圖片說明

從日誌能夠發現,共解析groovy達38694次。 輸入圖片說明

在系統運行四分鐘後,Test2.java對應JVM的GC如圖:

輸入圖片說明

從日誌能夠發現,共解析groovy達39100次。 輸入圖片說明

在系統運行四分鐘後,Test3.java對應JVM的GC如圖:

輸入圖片說明

從日誌能夠發現,共解析groovy達40000次。 輸入圖片說明

3、測試結果分析

經過觀察內存曲線圖,能夠獲取測試結果:

  • Test1.java:Test1.java:PS MarkSweep有5次,PS Scavenge高達1210次,分散均勻;
  • Test2.java:Test2.java:PS MarkSweep有5次,PS Scavenge達到485次,分散均勻;
  • Test3.java:Test3.java:PS MarkSweep有0次,PS Scavenge僅5次,且僅在線程啓動時觸發PS Scavenge。

從上述測試結果能夠獲得結論:

  • 一、Groovy類加載器,頻繁解析Groovy代碼將會致使PS MarkSweep;
  • 二、單例Groovy類加載器,比非單例更容易致使PS Scavenge;
  • 三、單例和多實例Groovy類加載器方式,PS MarkSweep基本一致,由於兩種方式parseClass生成的Class數量基本一致,即佔用的PermGen空間基本一致,因此兩種方式在Full GC上的表現基本一致,若是要減小Full GC,減小parseClass纔是根本解決方法;可是兩者PS Scavenge卻有數倍的差異,是由於單例方式parseClass過程當中冗餘大量的中間對象,這些中間對象會被PS Scavenge掉,不會引發大的問題。所以,減小parseClass次數纔是解決的正途。

4、總結優化

  • 一、爲避免啓動時Timeout,應該在項目徹底預熱完成後再切入線上環境;
  • 二、避免在在單次調用時觸發屢次groovy腳本解析,解析過程自己比較耗時,可並行處理,或者將多個腳本合併爲單個腳本;
  • 三、針對每一個groovy腳本解析後生成的Java對象實例作緩存,而不是代碼自己作緩存;
  • 四、僅僅在接收到清除緩存的廣播時解析生成新的Java實例對象,避免groovy的頻繁解析,減小Class裝載頻率;
  • 五、週期性的異步刷新類加載器,避免因全局類加載器頻繁parseClass致使的PS Scavenge。

PermGen回收

PermGen中對象回收規則:ClassLoader能夠被回收,其下的全部加載過的沒有對應實例的類信息(保存在PermGen)可被回收。所以,JVM回收以後,能夠將GroovyClassLoader加載的冗餘新信息回收掉。

可是。GC在JVM中一般是由一個或一組進程來實現的,它自己也和用戶程序同樣佔用heap空間,運行時也佔用CPU。所以,當GC運行時間較長時,用戶可以感到 Java程序的停頓。所以,儘可能避免GC。

相關文章
相關標籤/搜索