上文 GC 理論頗受你們好評,學習了以後,相信你們對 GC 的工做原理有了比較深入的認識,這一篇咱們繼續趁熱打鐵,來學習下 GC 的實戰內容,主要包括如下幾點java
JVM 參數簡介算法
發生 OOM 的主要幾種場景及相應解決方案數組
OOM 問題排查的一些經常使用工具緩存
GC 日誌格式怎麼看數據結構
jstat 與可視化 APM 工具構建多線程
再談 JVM 參數設置併發
在開始實踐以前咱們有必要先簡單瞭解一下 JVM 參數配置,由於本文以後的實驗中提到的 JVM 中的棧,堆大小,使用的垃圾收集器等都須要經過 JVM 參數來設置less
先來看下如何運行一個 Java 程序jvm
public class Test {
public static void main(String[] args) {
System.out.println("test");
}
}
首先咱們經過 javac Test.java 將其轉成字節碼ide
其次咱們每每會輸入 java Test 這樣的命令來啓動 JVM 進程來執行此程序,其實咱們在啓動 JVM 進程的時候,能夠指定相應的 JVM 的參數,以下藍色部分
指定這些 JVM 參數咱們就能夠指定啓動 JVM 進程以哪一種模式(server 或 client),運行時分配的堆大小,棧大小,用什麼垃圾收集器等等,JVM 參數主要分如下三類
一、 標準參數(-),全部的 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 參數主要有三大類
行爲參數(Behavioral Options):用於改變 JVM 的一些基礎行爲,如啓用串行/並行 GC
參數示例 | 表示意義 |
---|---|
-XX:-DisableExplicitGC | 禁止調用System.gc();但jvm的gc仍然有效 |
-XX:-UseConcMarkSweepGC | 對老生代採用併發標記交換算法進行GC |
-XX:-UseParallelGC | 啓用並行GC |
-XX:-UseParallelOldGC | 對Full GC啓用並行,當-XX:-UseParallelGC啓用時該項自動啓用 |
-XX:-UseSerialGC | 啓用串行GC |
性能調優(Performance Tuning):用於 jvm 的性能調優,如設置新老生代內存容量比例
參數示例 | 表示意義 |
---|---|
-XX:MaxHeapFreeRatio=70 | GC後java堆中空閒量佔的最大比例 |
-XX:NewRatio=2 | 新生代內存容量與老生代內存容量的比例 |
-XX:NewSize=2.125m | 新生代對象生成時佔用內存的默認值 |
-XX:ReservedCodeCacheSize=32m | 保留代碼佔用的內存容量 |
-XX:ThreadStackSize=512 | 設置線程棧大小,若爲0則使用系統默認值 |
調試參數(Debugging Options):通常用於打開跟蹤、打印、輸出等 JVM 參數,用於顯示 JVM 更加詳細的信息
參數示例 | 表示意義 |
---|---|
-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 虛擬機規範中描述在棧上主要會發生如下兩種異常
StackOverflowError 異常
這種狀況主要是由於單個線程請求棧深度大於虛擬機所容許的最大深度(如經常使用的遞歸調用層級過深等),再好比單個線程定義了大量的本地變量,致使方法幀中本地變量表長度過大等也會致使 StackOverflowError 異常, 一句話:在單線程下,當棧楨太大或虛擬機容量過小致使內存沒法分配時,都會發生 StackOverflowError 異常。
虛擬機在擴展棧時沒法申請到足夠的內存空間,會拋出 OOM 異常
在刨根問底---一次 OOM 試驗形成的電腦雪崩引起的思考 一文中咱們已經詳細地剖析了此例子,再來看看
/**
* 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)
主要緣由有兩點
1.大對象的分配,最有可能的是大數組分配
示例以下:
/**
* 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,咱們通常採用增大堆內存的方式來解決
畫外音:有人可能會說分配的數組大小不是隻有 2 * 1024 * 1024 * 4(一個 int 元素佔 4 個字節)= 8M, 怎麼分配 12 M 還不夠,由於 JVM 進程除了分配數組大小,還有指向類(數組中元素對應的類)信息的指針、鎖信息等,實際須要的堆空間是可能超過 12M 的, 12M 也只是嘗試出來的值,不一樣的機器可能不同
2.內存泄漏
咱們知道在 Java 中,開發者建立和銷燬對象是不須要本身開闢空間的,JVM 會自動幫咱們完成,在應用程序整個生命週期中,JVM 會定時檢查哪些對象可用,哪些再也不使用,若是對象再也不使用的話理論上這塊內存會被回收再利用(即GC),若是沒法回收就會發生內存泄漏
/**
* 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」相似,主要是因爲分配大內存數組或內存泄漏致使的, 解決方案以下:
檢查項目中是否有大量的死循環或有使用大內存的代碼,優化代碼。
dump 內存(後文會講述如何 dump 出內存),檢查是否存在內存泄露,若是沒有,可考慮經過 -Xmx 參數設置加大內存。
四、java.lang.OutOfMemoryError:Permgen space
在 Java 8 之前有永久代(實際上是用永久代實現了方法區的功能)的概念,存放了被虛擬機加載的類,常量,靜態變量,JIT 編譯後的代碼等信息,因此若是錯誤地頻繁地使用 String.intern() 方法或運行期間生成了大量的代理類都有可能致使永久代溢出,解決方案以下
是否永久代設置的太小,若是能夠,適應調大一點
檢查代碼是否有大量的反射操做
dump 以後經過 mat 檢查是否存在大量因爲反射生成的代碼類
五、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 要求的總內存空間大小大於可用的本機內存,則操做系統會將內存中的部分數據交換到硬盤上
七、Out of memory:Kill process or sacrifice child
爲了理解這個異常,咱們須要知識一些操做系統的知識,咱們知道,在操做系統中執行的程序,都是以進程的方式運行的,而進程是由內核調度的,在內核的調度任務中,有一個「Out of memory killer」的調度器,它會在系統可用內存不足時被激活,而後選擇一個進程把它幹掉,哪一個進程會被幹掉呢,簡單地說會優先幹掉佔用內存大的應用型進程
解決這種 OOM 最直接簡單的方法就是升級內存,或者調整 OOM Killer 的優先級,減小應用的沒必要要的內存使用等等
看了以上的各類 OOM 產生的狀況,能夠看出:GC 和是否發生 OOM 沒有必然的因果關係! GC 主要發生在堆上,而 從以上列出的幾種發生 OOM 的場景能夠看出,空間不足沒法再建立線程,或者存在死循環一直在分配對象致使 GC 沒法回收對象或者一次分配大內存數組(超過堆的大小)等均可能致使 OOM, 因此 OOM 與 GC 並無必然的因果關係
接下來咱們來看下如何排查形成 OOM 的緣由,內存泄漏是最多見的形成 OOM 的一種緣由,因此接下來咱們以來看看怎麼使用工具來排查這種問題,使用到的工具主要有兩大類
一、使用 mat(Eclipse Memory Analyzer) 來分析 dump(堆轉儲快照) 文件
主要步驟以下
運行 Java 時添加 「-XX:+HeapDumpOnOutOfMemoryError」 參數來導出內存溢出時的堆信息,生成 hrof 文件, 添加 「-XX:HeapDumpPath」能夠指定 hrof 文件的生成路徑,若是不指定則 hrof 文件生成在與字節碼文件相同的目錄下
使用 MAT(Eclipse Memory Analyzer)來分析 hrof 文件,查出內存泄漏的緣由
接下來咱們就來看看如何用以上的工具查看以下內存泄漏案例
/**
* 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 的線程的堆棧信息,明肯定位到是哪一行形成的
二、使用 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 來說解, 以上日誌中標的每個數字與如下序號一一對應
開頭的 0.080,0.088 表明了 GC 發生的時間,這個數字的含義是從 Java 虛擬機啓動以來通過的秒數
[GC 或者 [Full GC 說明了此次垃圾收集的停頓類型,注意不是用來區分新生代 GC 仍是老年化 GC 的,若是有 Full,說明此次 GC 是發生了 Stop The World 的,若是是調用 System.gc() 所觸發的收集,這裏會顯示 [Full GC(System)
以後的 Allocation Failure 表明了觸發 GC 的緣由,在這個程序中咱們設置了新生代的大小爲 10M(-Xmn10M),Eden:S0:S1 = 8:1:1(-XX:SurvivorRatio=8),也就是說 Eden 區佔了 8M, 當分配 allocation4 時,因爲將要分配的總大小爲 10M,超過了 Eden 區,因此此時會發生 GC
接下來的 [DefNew,[Tenured,[Metaspace 表示 GC 發生的區域,這裏顯示的區域名與使用的 GC 收集器是密切相關的,在此例中因爲新生代咱們使用了 Serial 收集器,此收集器新生代名爲「Default New Generation」,因此顯示的是 [DefNew,若是是 ParNew 收集器,新生代名稱就會變爲 [ParNew`,意爲 「Parallel New Generation」,若是採用 「Parallel Scavenge」收集器,則配套的新生代名稱爲「PSYoungGen」,老年代與新生代同樣,名稱也是由收集器決定的
再日後 6815K->280K(9216K) 表示 「GC 前該內存區域已使用容量 -> GC 後該內存區域已使用容量(該內存區域總容量)」
0.0043690 secs 表示該塊內存區域 GC 所佔用的時間,單位是秒
6815K->6424K(19456K) 表示「GC 前 Java 堆已使用容量 -> GC 後 Java 堆已使用容量(java 堆總容量)」。
0.0044111 secs 表示整個 GC 執行時間,注意和 6 中 0.0043690 secs 的區別,後者專指相關區域所花的 GC 時間,而前者指的 GC 的總體堆內存變化所花時間(新生代與老生代的的內存整理),因此前者是確定大於後者的!
最後一個 [Times: user=0.01 sys=0.00, real=0.02 secs] 這裏的 user, sys 和 real 與Linux 的 time 命令所輸出的時間一致,分別表明用戶態消耗的 CPU 時間,內核態消耗的 CPU 時間,和操做從開始到結束所通過的牆鍾時間,牆鍾時間包括各類非運算的等待耗時,例如等待磁盤 I/O,等待線程阻塞,而 CPU 時間不包括這些耗時,但當系統有多 CPU 或者多核的話,多線程操做會疊加這些 CPU 時間,因此 user 或 sys 時間是可能超過 real 時間的。
知道了 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 這些工具排查問題的技巧,不過這些工具的介紹本文只是提到了一些皮毛,你們能夠在再深刻了解相應工具的一些進階技能,這能對本身排查問題等大有裨益!文中的例子你們能夠去試驗一下,修改一下參數看下會發生哪些神奇的現象,親自動手作一遍能對排查問題的思路更加清晰哦
歡迎關注公衆號與筆者共同交流哦^_^
http://songkun.me/2018/12/19/2018-12-19-java-why-xmx-xms-should-be-same/
https://plumbr.io/outofmemoryerror
https://yq.aliyun.com/articles/512832
想知道更多?長按/掃碼關注我吧↓↓↓