目錄
一般情況下,我們生產環境中所遇到的bug或問題基本可以分爲四類:
- 第一類是比較簡單的bug,一般日誌會有錯誤堆棧,或者異常信息,這種基本都可以通過遠程debug或者代碼推理找到具體原因從而解決問題。
- 第二類是稍微難一點的bug,一般與集成的第三方jar包有關,比如spring的網關,ribbon,數據庫連接池,fastjson等,基本可以通過源碼定位問題。
- 第三類是難一點的bug,一般要求研發人員具有豐富的排查和解決問題的能力,程序性能問題、中間件,操作系統資源相關的問題,這類問題一般可通過jvm排查程序問題,操作系統命令查看系統資源,及排查相關中間件問題。
- 第四類是困難的,基本需要長期解決的,一般需要專業的經驗豐富的研發人員,基本屬於中間件的問題,比如hbase,es,kafka等。一般這類問題是比較有規律的出現,此規律可能與時間有關,可能與空間有關。
這篇文章主要講述第三類的問題的jvm排查程序,通過學習瞭解jvm的基本構成及工具來達到解決此類問題的能力。
jvm的基礎知識
下面講解的版本是jdk1.8。
內存模型
程序計數器
我們知道對於一個處理器(如果是多核cpu那就是一核),在一個確定的時刻都只會執行一條線程中的指令,一條線程中有多個指令,爲了線程切換可以恢復到正確執行位置,每個線程都需有獨立的一個程序計數器,不同線程之間的程序計數器互不影響,獨立存儲。
java棧
每個方法被執行的時候都會創建棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息,每一個方法被調用的過程,就對應一個棧幀在虛擬機中從入棧到出棧的過程。
Java虛擬機棧可能出現兩種類型的異常:
- 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError。
- 虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常。
堆
存儲new出來的對象,數組及字符串常量池,垃圾回收的主戰場。
本地方法棧
本地方法棧則爲虛擬機使用到的native方法服務,可能底層調用的c或者c++
方法區
又稱非堆,線程共享區域,用於存儲已被虛擬機加載的類信息、常量、靜態變量,如static修飾的變量加載類的時候就被加載到方法區中。
對象大小計算
在64位的操作系統中,開啓指針壓縮後,一個空對象佔16Byte,真大a
對象結構大小
名稱(單位byte) | 32位 | 64位 | 開啓指針壓縮後(指針對64位有效且默認開啓) |
---|---|---|---|
對象頭(Header) | 8 | 16 | 12 |
數組對象頭 | 12 | 24 | 16 |
引用(reference) | 4 | 8 | 4 |
對象頭
- Mark Word記錄了對象和鎖有關的信息
- 指向類的指針,Java對象的類數據保存在方法區
- 數組長度,僅在數組對象中存在,該數據在32位和64位JVM中長度都是32bit。
對其補充
第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
線程模型
ThreadPoolExecutor創建線程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
任務執行策略
用於傳輸和保存等待執行任務的阻塞隊列。可以使用此隊列與線程池進行交互:
如果運行的線程數少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。
如果運行的線程數等於或多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不添加新的線程。
如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出maximumPoolSize,在這種情況下,任務將被拒絕。
線程飽和策略
當線程池和隊列都滿了,則表明該線程池已達飽和狀態。
ThreadPoolExecutor.AbortPolicy:處理程序遭到拒絕,則直接拋出運行時異常RejectedExecutionException。(默認策略)
ThreadPoolExecutor.CallerRunsPolicy:調用者所在線程來運行該任務,此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
ThreadPoolExecutor.DiscardPolicy:無法執行的任務將被刪除。
ThreadPoolExecutor.DiscardOldestPolicy:如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然後重新嘗試執行任務(如果再次失敗,則重複此過程)。
四種線程池對比
線程池方法 | 初始化線程池數 | 最大線程池數 | 線程存活時間 | 時間單位 | 工作隊列 |
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60 | s | SynchronousQueue |
newFixedThreadPool | 入參指定大小 | 入參指定大小 | 0 | ms | LinkedBlockingQueue |
newScheduledThreadPool | 入參指定大小 | Integer.MAX_VALUE | 0 | 微秒 | DelayedWorkQueue |
newSingleThreadExecutor | 1 | 1 | 0 | ms | LinkedBlockingQueue |
GC詳解
垃圾回收算法
- 標記清理算法
- 複製算法
- 標記整理算法
-
分代收集算法
當前商業虛擬機的垃圾收集都採用“分代收集”(Generational Collection)算法,根據對象存活週期的不同將內存劃分爲幾塊並採用不用的垃圾收集算法。
一般是把 Java 堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
垃圾收集器
- serial(串行)收集器
它的“單線程”的意義並不僅僅說明它只會使用一個 CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
2.ParNew(串行)收集器
serial(串行)收集器的多線程版本。
3.Parallel Scavenge收集器
Parallel Scavenge 收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。
4.Serial Old 收集器
5.Parallel Old收集器
6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。
7.G1收集器
基礎命令
- top:查看資源佔用
- top -Hp pid:查看某一個進程的線程資源使用情況
- jps:查看java的進程信息
- jstack:查看java進程的堆棧信息
- jmap: 查看java的堆內存使用情況,及每個類佔用字節
- jstat: 查看垃圾回收統計及其他
- free:查看linux的內存使用
- sar -n DEV 1 2:查看網卡流量
- iostat:查看磁盤讀寫情況
- jconsole:jvm桌面控制檯
- jvisualvm:jvm桌面控制檯
- df -Th:查看磁盤使用率
jvm案例排查講解