上文咱們學習了 GC 的理論基礎,相信你們對 GC 的工做原理有了比較深入的認識,這一篇咱們繼續趁熱打鐵,來學習下 GC 的實戰內容,主要包括如下幾點html
在開始實踐以前咱們有必要先簡單瞭解一下 JVM 參數配置,由於本文以後的實驗中提到的 JVM 中的棧,堆大小,使用的垃圾收集器等都須要經過 JVM 參數來設置java
先來看下如何運行一個 Java 程序算法
public class Test {
public static void main(String[] args) {
System.out.println("test");
}
}
複製代碼
指定這些 JVM 參數咱們就能夠指定啓動 JVM 進程以哪一種模式(server 或 client),運行時分配的堆大小,棧大小,用什麼垃圾收集器等等,JVM 參數主要分如下三類shell
一、 標準參數(-),全部的 JVM 實現都必須實現這些參數的功能,並且向後兼容;例如 -verbose:gc(輸出每次GC的相關狀況)數組
二、 非標準參數(-X),默認 JVM 實現這些參數的功能,可是並不保證全部 JVM 實現都知足,且不保證向後兼容,棧,堆大小的設置都是經過這個參數來配置的,用得最多的以下緩存
參數示例 | 表示意義 |
---|---|
-Xms512m | JVM 啓動時設置的初始堆大小爲 512M |
-Xmx512m | JVM 可分配的最大堆大小爲 512M |
-Xmn200m | 設置的年輕代大小爲 200M |
-Xss128k | 設置每一個線程的棧大小爲 128k |
三、非Stable參數(-XX),此類參數各個 jvm 實現會有所不一樣,未來可能會隨時取消,須要慎重使用, -XX:-option 表明關閉 option 參數,-XX:+option 表明要關閉 option 參數,例如要啓用串行 GC,對應的 JVM 參數即爲 -XX:+UseSerialGC。非 Stable 參數主要有三大類微信
參數示例 | 表示意義 |
---|---|
-XX:-DisableExplicitGC | 禁止調用System.gc();但jvm的gc仍然有效 |
-XX:-UseConcMarkSweepGC | 對老生代採用併發標記交換算法進行GC |
-XX:-UseParallelGC | 啓用並行GC |
-XX:-UseParallelOldGC | 對Full GC啓用並行,當-XX:-UseParallelGC啓用時該項自動啓用 |
-XX:-UseSerialGC | 啓用串行GC |
參數示例 | 表示意義 |
---|---|
-XX:MaxHeapFreeRatio=70 | GC後java堆中空閒量佔的最大比例 |
-XX:NewRatio=2 | 新生代內存容量與老生代內存容量的比例 |
-XX:NewSize=2.125m | 新生代對象生成時佔用內存的默認值 |
-XX:ReservedCodeCacheSize=32m | 保留代碼佔用的內存容量 |
-XX:ThreadStackSize=512 | 設置線程棧大小,若爲0則使用系統默認值 |
參數示例 | 表示意義 |
---|---|
-XX:HeapDumpPath=./java_pid.hprof | 指定導出堆信息時的路徑或文件名 |
-XX:-HeapDumpOnOutOfMemoryError | 當首次遭遇OOM時導出此時堆中相關信息 |
-XX:-PrintGC | 每次GC時打印相關信息 |
-XX:-PrintGC Details | 每次GC時打印詳細信息 |
畫外音:以上只是列出了比較經常使用的 JVM 參數,更多的 JVM 參數介紹請查看文末的參考資料數據結構
明白了 JVM 參數是幹啥用的,接下來咱們進入實戰演練,下文中全部程序運行時對應的 JVM 參數都以 VM Args 的形式寫在開頭的註釋裏,讀者若是在執行程序時記得要把這些 JVM 參數給帶上哦多線程
有些人可能會以爲奇怪, GC 不是會自動幫咱們清理垃圾以騰出使用空間嗎,怎麼還會發生 OOM, 咱們先來看下有哪些場景會發生 OOM併發
一、Java 虛擬機規範中描述在棧上主要會發生如下兩種異常
/** * VM Args:-Xss160k */
public class Test {
private void dontStop() {
while(true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test oom = new Test();
oom.stackLeakByThread();
}
}
複製代碼
運行以上代碼會拋出「java.lang.OutOfMemoryError: unable to create new native thread」的異常,緣由不難理解,操做系統給每一個進程分配的內存是有限制的,好比 32 位的 Windows 限制爲 2G,虛擬機提供了參數來控制 Java 堆和方法的這兩部內存的最大值,剩餘的內存爲 「2G - Xmx(最大堆容量)= 線程數 * 每一個線程分配的虛擬機棧(-Xss)+本地方法棧 」(程序計數器消耗內存不多,可忽略),每一個線程都會被分配對應的虛擬機棧大小,因此總可建立的線程數確定是固定的, 像以上代碼這樣不斷地建立線程固然會形成最終沒法分配,不過這也給咱們提供了一個新思路,若是是由於創建過多的線程致使的內存溢出,而咱們又想多建立線程,能夠經過減小最大堆(-Xms)和減小虛擬機棧大小(-Xss)來實現。
二、堆溢出 (java.lang.OutOfMemoryError:Java heap space)
主要緣由有兩點
示例以下:
/** * VM Args:-Xmx12m */
class OOM {
static final int SIZE=2*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
複製代碼
咱們指定了堆大小爲 12M,執行 「java -Xmx12m OOM」命令就發生了 OOM 異常,若是指定 13M 則以上程序就能正常執行,因此對於因爲大對象分配致使的堆溢出這種 OOM,咱們通常採用增大堆內存的方式來解決
畫外音:有人可能會說分配的數組大小不是隻有 210241024*4(一個 int 元素佔 4 個字節)= 8M, 怎麼分配 12 M 還不夠,由於 JVM 進程除了分配數組大小,還有指向類(數組中元素對應的類)信息的指針、鎖信息等,實際須要的堆空間是可能超過 12M 的, 12M 也只是嘗試出來的值,不一樣的機器可能不同
/** * VM Args:-Xmx4m */
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map<Key,String> m = new HashMap<Key,String>();
while(true) {
for(int i=0;i<10000;i++) {
if(!m.containsKey(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}
}
}
複製代碼
執行以上代碼就會發生內存泄漏,第一次循環,map 裏存有 10000 個 key value,但以後的每次循環都會新增 10000 個元素,由於 Key 這個 class 漏寫了 equals 方法,致使對於每個新建立的 new Key(i) 對象,即便 i 相同也會被認定爲屬於兩個不一樣的對象,這樣 m.containsKey(new Key(i)) 結果均爲 false,結果就是 HashMap 中的元素將一直增長,解決方式也很簡單,爲 Key 添加 equals 方法便可,以下
@Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}
複製代碼
對於這種內存泄漏致使的 OOM, 單純地增大堆大小是沒法解決根本問題的,只不過是延緩了 OOM 的發生,最根本的解決方式仍是要經過 heap dump analyzer 等方式來找出內存泄漏的代碼來修復解決,後文會給出一個例子來分析
三、java.lang.OutOfMemoryError:GC overhead limit exceeded
Sun 官方對此的定義:超過98%的時間用來作 GC 而且回收了不到 2% 的堆內存時會拋出此異常
致使的後果就是因爲通過幾個 GC 後只回收了不到 2% 的內存,堆很快又會被填滿,而後又頻繁發生 GC,致使 CPU 負載很快就達到 100%,另外咱們知道 GC 會引發 「Stop The World 」的問題,阻塞工做線程,因此會致使嚴重的性能問題,產生這種 OOM 的緣由與「java.lang.OutOfMemoryError:Java heap space」相似,主要是因爲分配大內存數組或內存泄漏致使的, 解決方案以下:
四、java.lang.OutOfMemoryError:Permgen space
在 Java 8 之前有永久代(實際上是用永久代實現了方法區的功能)的概念,存放了被虛擬機加載的類,常量,靜態亦是,JIT 編譯後的代碼等信息,因此若是錯誤地頻繁地使用 String.intern() 方法或運行期間生成了大量的代理類都有可能致使永久代溢出,解決方案以下
五、 java.lang.OutOfMemoryError:Requested array size exceeds VM limit
該錯誤由 JVM 中的 native code 拋出。 JVM 在爲數組分配內存以前,會執行基於所在平臺的檢查:分配的數據結構是否在此平臺中是可尋址的,平臺通常容許分配的數據大小在 1 到 21 億之間,若是超過了這個數就會拋出這種異常
碰到這種異常通常咱們只要檢查代碼中是否有建立超大數組的地方便可。
六、 java.lang.OutOfMemoryError: Out of swap space
Java 應用啓動的時候分被分配必定的內存空間(經過 -Xmx 及其餘參數來指定), 若是 JVM 要求的總內存空間大小大於可用的本機內存,則操做系統會將內存中的部分數據交換到硬盤上
若是此時 swap 分區大小不足或者其餘進程耗盡了本機的內存,則會發生 OOM, 能夠經過增大 swap 空間大小來解決,但若是在交換空間進行 GC 形成的 「Stop The World」增長大個數量級,因此增大 swap 空間必定要慎重,因此通常是經過增大本機內存或優化程序減小內存佔用來解決。七、 Out of memory:Kill process or sacrifice child
爲了理解這個異常,咱們須要知識一些操做系統的知識,咱們知道,在操做系統中執行的程序,都是以進程的方式運行的,而進程是由內核調度的,在內核的調度任務中,有一個「Out of memory killer」的調度器,它會在系統可用內存不足時被激活,而後選擇一個進程把它幹掉,哪一個進程會被幹掉呢,簡單地說會優先幹掉佔用內存大的應用型進程
如圖示,進程 4 佔用內存最大,最有可能被幹掉解決這種 OOM 最直接簡單的方法就是升級內存,或者調整 OOM Killer 的優先級,減小應用的沒必要要的內存使用等等
看了以上的各類 OOM 產生的狀況,能夠看出:GC 和是否發生 OOM 沒有必然聯繫!, GC 主要發生在堆上,而 從以上列出的幾種發生 OOM 的場景能夠看出,因爲空間不足沒法再建立線程,因爲存在死循環一直在分配對象致使 GC 沒法回收對象或一次分配大內存數組(超過堆的大小)等均可能致使 OOM, 因此 OOM 與 GC 並無太大的關聯
接下來咱們來看下如何排查形成 OOM 的緣由,內存泄漏是最多見的形成 OOM 的一種緣由,因此接下來咱們以來看看怎麼使用工具來排查這種問題,使用到的工具主要有兩大類
一、使用 mat(Eclipse Memory Analyzer) 來分析 dump(堆轉儲快照) 文件
主要步驟以下
接下來咱們就來看看如何用以上的工具查看以下經典的內存泄漏案例
/** * VM Args:-Xmx10m */
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
while (true) {
list.add("OutOfMemoryError soon");
}
}
}
複製代碼
爲了讓以上程序快速產生 OOM, 我把堆大小設置成了 10M, 這樣執行 「java -Xmx10m -XX:+HeapDumpOnOutOfMemoryError Main」後很快就發生了 OOM,此時咱們就拿到了 hrof 文件,下載 MAT 工具,打開 hrof,進行分析,打開以後選擇 「Leak Suspects Report」進行分析,能夠看到發生 OOM 的線程的堆棧信息,明肯定位到是哪一行形成的
如圖示,能夠看到 Main.java 文件的第 12 行致使了此次的 OOM二、使用 jvisualvm 來分析
用第一種方式必須等 OOM 後才能 dump 出 hprof 文件,但若是咱們想在運行中觀察堆的使用狀況以便查出可能的內存泄漏代碼就無能爲力了,這時咱們能夠藉助 jvisualvm 這款工具, jvisualvm 的功能強大,除了能夠實時監控堆內存的使用狀況,還能夠跟蹤垃圾回收,運行中 dump 中堆內存使用狀況、cpu分析,線程分析等,是查找分析問題的利器,更騷的是它不光能分析本地的 Java 程序,還能夠分析線上的 Java 程序運行狀況, 自己這款工具也是隨 JDK 發佈的,是官方力推的一款運行監視,故障處理的神器。咱們來看看如何用 jvisualvm 來分析上文所述的存在內存泄漏的以下代碼
import java.util.Map;
import java.util.HashMap;
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map<Key,String> m = new HashMap<Key,String>();
while(true) {
for(int i=0;i<10000;i++) {
if(!m.containsKey(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}
}
}
複製代碼
打開 jvisualvm (終端輸入 jvisualvm 執行便可),打開後,將堆大小設置爲 500M,執行命令 java Xms500m -Xmx500m KeylessEntry,此時能夠觀察到左邊出現了對應的應用 KeylessEntry,雙擊點擊 open
打開以後能夠看到展現了 CPU,堆內存使用,加載類及線程的狀況
注意看堆(Heap)的使用狀況,一直在上漲
此時咱們再點擊 「Heap Dump」
過一下子便可看到內存中對象的使用狀況
能夠看到相關的 TreeNode 有291w 個,遠超正常狀況下的 10000 個!說明 HashMap 一直在增加,自此咱們能夠定位出問題代碼所在!
三、使用 jps + jmap 來獲取 dump 文件
jps 能夠列出正在運行的虛擬機進程,並顯示執行虛擬機主類及這些進程的本地虛擬機惟一 ID,如圖示
拿到進程的 pid 後,咱們就能夠用 jmap 來 dump 出堆轉儲文件了,執行命令以下
jmap -dump:format=b,file=heapdump.phrof pid
複製代碼
拿到 dump 文件後咱們就能夠用 MAT 工具來分析了。 但這個命令在生產上必定要慎用!由於JVM 會將整個 heap 的信息 dump 寫入到一個文件,heap 比較大的話會致使這個過程比較耗時,而且執行過程當中爲了保證 dump 的信息是可靠的,會暫停應用!
接下來咱們看看 GC 日誌怎麼看,日誌能夠有效地幫助咱們定位問題,因此搞清楚 GC 日誌的格式很是重要,來看下以下例子
/** * VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseSerialGC -XX:SurvivorRatio=8 */
public class TestGC {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 這裏會出現一次 Minor GC
}
}
複製代碼
執行以上代碼,會輸出以下 GC 日誌信息
10.080: 2[GC 3(Allocation Failure) 0.080: 4[DefNew: 56815K->280K(9216K),6 0.0043690 secs] 76815K->6424K(19456K), 80.0044111 secs]9 [Times: user=0.00 sys=0.01, real=0.01 secs]
以上是發生 Minor GC 的 GC 是日誌,若是發生 Full GC 呢,格式以下
10.088: 2[Full GC 3(Allocation Failure) 0.088: 4[Tenured: 50K->210K(10240K), 60.0009420 secs] 74603K->210K(19456K), [Metaspace: 2630K->2630K(1056768K)], 80.0009700 secs]9 [Times: user=0.01 sys=0.00, real=0.02 secs]
二者格式其實差很少,一塊兒來看看,主要以本例觸發的 Minor GC 來說解, 以上日誌中標的每個數字與如下序號一一對應
知道了 GC 日誌怎麼看,咱們就能夠根據 GC 日誌有效定位問題了,如咱們發現 Full GC 發生時間過長,則結合咱們上文應用中打印的 OOM 日誌可能能夠快速定位到問題
jstat 是用於監視虛擬機各類運行狀態信息的命令行工具,能夠顯示本地或者遠程虛擬機進程中的類加載,內存,垃圾收集,JIT 編譯等運行數據,jstat 支持定時查詢相應的指標,以下
jstat -gc 2764 250 22
複製代碼
定時針對 2764 進程輸出堆的垃圾收集狀況的統計,能夠顯示 gc 的信息,查看gc的次數及時間,利用這些指標,把它們可視化,對分析問題會有很大的幫助,如圖示,下圖就是我司根據 jstat 作的一部分 gc 的可視化報表,能快速定位發生問題的問題點,若是你們須要作 APM 可視化工具,建議配合使用 jstat 來完成。
通過前面對 JVM 參數的介紹及相關例子的實驗,相信你們對 JVM 的參數有了比較深入的理解,接下來咱們再談談如何設置 JVM 參數,
一、首先 Oracle 官方推薦堆的初始化大小與堆可設置的最大值通常是相等的,即 Xms = Xmx,由於起始堆內存過小(Xms),會致使啓動初期頻繁 GC,起始堆內存較大(Xmx)有助於減小 GC 次數
二、調試的時候設置一些打印參數,如-XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log,這樣能夠從gc.log裏看出一些端倪出來
三、系統停頓時間過長多是 GC 的問題也多是程序的問題,多用 jmap 和 jstack 查看,或者killall -3 Java,而後查看 Java 控制檯日誌,能看出不少問題
四、 採用併發回收時,年輕代小一點,年老代要大,由於年老大用的是併發回收,即便時間長點也不會影響其餘程序繼續運行,網站不會停頓
五、仔細瞭解本身的應用,若是用了緩存,那麼年老代應該大一些,緩存的HashMap不該該無限制長,建議採用LRU算法的Map作緩存,LRUMap的最大長度也要根據實際狀況設定
要設置好各類 JVM 參數,還能夠對 server 進行壓測, 預估本身的業務量,設定好一些 JVM 參數進行壓測看下這些設置好的 JVM 參數是否能知足要求
本文經過詳細介紹了 JVM 參數及 GC 日誌, OOM 發生的緣由及相應地調試工具,相信讀者應該掌握了基本的 MAT,jvisualvm 這些工具排查問題的技巧,不過這些工具的介紹本文只是提到了一些皮毛,你們能夠在再深刻了解相應工具的一些進階技能,這能對本身排查問題等大有裨益!文中的例子你們能夠去試驗一下,修改一下參數看下會發生哪些神奇的現象,親自動手作一遍能對排查問題的思路更加清晰哦
歡迎關注公衆號與筆者共同交流哦^_^
參考
更多算法 + 計算機基礎知識 + Java 等文章,歡迎關注個人微信公衆號哦。