本課時我們主要分析一個案例,那就是一個“高死亡率”報表系統的優化之路。
傳統觀念上的報表系統,可能訪問量不是特別多,點擊一個查詢按鈕,后臺 SQL 語句的執行需要等數秒。如果使用 jstack 來查看執行線程,會發現大多數線程都阻塞在數據庫的 I/O 上。
上面這種是非常傳統的報表。還有一種類似于大屏監控一類的實時報表,這種報表的并發量也是比較可觀的,但由于它的結果集都比較小,所以我們可以像對待一個高并發系統一樣對待它,問題不是很大。
本課時要講的,就是傳統觀念上的報表。除了處理時間比較長以外,報表系統每次處理的結果集,普遍都比較大,這給 JVM 造成了非常大的壓力。
下面我們以一個綜合性的實例,來看一下一個“病入膏肓”的報表系統的優化操作。
有一個報表系統,頻繁發生內存溢出,在高峰期間使用時,還會頻繁的發生拒絕服務,這是不可忍受的。
#### 服務背景
本次要優化的服務是一個 SaaS 服務,使用 Spring Boot 編寫,采用的是 CMS 垃圾回收器。如下圖所示,有些接口會從 MySQL 中獲取數據,有些則從 MongoDB 中獲取數據,涉及的結果集合都比較大。
由于有些結果集的字段不是太全,因此需要對結果集合進行循環,可通過 HttpClient 調用其他服務的接口進行數據填充。也許你會認為某些數據可能會被復用,于是使用 Guava 做了 JVM 內緩存。
大體的服務依賴可以抽象成下面的圖。

初步排查,JVM 的資源太少。當接口 A 每次進行報表計算時,都要涉及幾百兆的內存,而且在內存里駐留很長時間,同時有些計算非常耗 CPU,特別的“吃”資源。而我們分配給 JVM 的內存只有 3 GB,在多人訪問這些接口的時候,內存就不夠用了,進而發生了 OOM。在這種情況下,即使連最簡單的報表都不能用了。
沒辦法,只有升級機器。把機器配置升級到 4core8g,給 JVM 分配 6GB 的內存,這樣 OOM 問題就消失了。但隨之而來的是頻繁的 GC 問題和超長的 GC 時間,平均 GC 時間竟然有 5 秒多。
#### 初步優化
我們前面算過,6GB 大小的內存,年輕代大約是 2GB,在高峰期,每幾秒鐘則需要進行一次 MinorGC。報表系統和高并發系統不太一樣,它的對象,存活時長大得多,并不能僅僅通過增加年輕代來解決;而且,如果增加了年輕代,那么必然減少了老年代的大小,由于 CMS 的碎片和浮動垃圾問題,我們可用的空間就更少了。雖然服務能夠滿足目前的需求,但還有一些不太確定的風險。
* 第一,了解到程序中有很多緩存數據和靜態統計數據,為了減少 MinorGC 的次數,通過分析 GC 日志打印的對象年齡分布,把 MaxTenuringThreshold 參數調整到了 3(請根據你自己的應用情況設置)。**這個參數是讓年輕代的這些對象,趕緊回到老年代去,不要老呆在年輕代里**。
* 第二,我們的 GC 時間比較長,就一塊開了參數 **CMSScavengeBeforeRemark**,使得在 CMS remark 前,先執行一次 Minor GC 將新生代清掉。同時配合上個參數,其效果還是比較好的,一方面,對象很快晉升到了老年代,另一方面,年輕代的對象在這種情況下是有限的,在整個 MajorGC 中占的時間也有限。
* 第三,由于緩存的使用,有大量的弱引用,拿一次長達 10 秒的 GC 來說。我們發現在 GC 日志里,處理 weak refs 的時間較長,達到了 4.5 秒。
```
2020-01-28T12:13:32.876+0800:?526569.947:?[weak?refs?processing,?4.5240649?secs]
```
所以加入了參數 **ParallelRefProcEnabled** 來并行處理 Reference,以加快處理速度,縮短耗時。
同時還加入了其他一些優化參數,比如通過調整觸發 GC 的參數來進行優化。
```
-Xmx6g?-Xms6g?-XX:MaxTenuringThreshold=3?-XX:+AlwaysPreTouch?-XX:+Par
allelRefProcEnabled?-XX:+CMSScavengeBeforeRemark?-XX:+UseConcMarkSwe
epGC?-XX:CMSInitiatingOccupancyFraction=80?-XX:+UseCMSInitiatingOccu
pancyOnly??-XX:MetaspaceSize=256M?-XX:MaxMetaspaceSize=256M
```
優化之后,效果不錯,但并不是特別明顯。經過評估,針對高峰時期的情況進行調研,我們決定再次提升機器性能,改用 8core16g 的機器。但是,這會帶來另外一個問題。
**高性能的機器帶來了非常大的服務吞吐量**,通過 jstat 進行監控,能夠看到年輕代的分配速率明顯提高,但隨之而來的 MinorGC 時長卻變的不可控,有時候會超過 1 秒。累積的請求造成了更加嚴重的后果。
這是由于堆空間明顯加大造成的回收時間加長。為了獲取較小的停頓時間,我們在堆上采用了 G1 垃圾回收器,把它的目標設定在 200ms。G1 是一款非常優秀的垃圾收集器,不僅適合堆內存大的應用,同時也簡化了調優的工作。通過主要的參數初始和最大堆空間、以及最大容忍的 GC 暫停目標,就能得到不錯的性能。所以為了照顧大對象的生成,我們把小堆區的大小修改為 16 M。修改之后,雖然 GC 更加頻繁了一些,但是停頓時間都比較小,應用的運行較為平滑。
```
-Xmx12g?-Xms12g?-XX:+UseG1GC?-XX:InitiatingHeapOccupancyPercent=45???-XX:MaxGCPauseMillis=200??-XX:G1HeapRegionSize=16m?-XX:MetaspaceSize=256m?-XX:MaxMetaspaceSize=256m
```
這個時候,任務來了:業務部門發力,預計客戶增長量增長 10 ~ 100 倍,報表系統需要評估其可行性,以便進行資源協調。可問題是,這個“千瘡百孔”的報表系統,稍微一壓測,就宕機,那如何應對十倍百倍的壓力呢?
使用 MAT 分析堆快照,發現很多地方可以通過代碼優化,那些占用內存特別多的對象,都是我們需要優化的。
#### 代碼優化
我們使用擴容硬件的方式,暫時緩解了 JVM 的問題,但是根本問題并沒有觸及到。為了減少內存的占用,肯定要清理無用的信息。通過對代碼的仔細分析,首先要改造的就是 SQL 查詢語句。
很多接口,其實并不需要把數據庫的每個字段都查詢出來,當你在計算和解析的時候,它們會不知不覺地“吃掉”你的內存。所以我們只需要獲取所需的數據就夠了,也就是把 select * 這種方式修改為具體的查詢字段,對于報表系統來說這種優化尤其明顯。
再一個就是 Cache 問題,通過排查代碼,會發現一些命中率特別低,占用內存又特別大的對象,放到了 JVM 內的 Cache 中,造成了無用的浪費。
解決方式,就是把 Guava 的 Cache 引用級別改成弱引用(WeakKeys),盡量去掉無用的應用緩存。對于某些使用特別頻繁的小 key,使用分布式的 Redis 進行改造即可。
為了找到更多影響因子大的問題,我們部署了獨立的環境,然后部署了 JVM 監控。在回放某個問題請求后,觀察 JVM 的響應,通過這種方式,發現了更多的優化可能。
報表系統使用了 POI 組件進行導入導出功能的開發,結果客戶在沒有限制的情況下上傳、下載了條數非常多的文件,直接讓堆內存飆升。為了解決這種情況,我們在導入功能加入了文件大小的限制,強制客戶進行拆分;在下載的時候指定范圍,嚴禁跨度非常大的請求。
在完成代碼改造之后,再把機器配置降級回 4core8g,依然采用 G1 垃圾回收器,再也沒有發生 OOM 的問題了,GC 問題也得到了明顯的緩解。
#### 拒絕服務問題
上面解決的是 JVM 的內存問題,可以看到除了優化 JVM 參數、升級機器配置以外,代碼修改帶來的優化效果更加明顯,但這個報表服務還有一個嚴重的問題。
剛開始我們提到過,由于沒有微服務體系,有些數據需要使用 HttpClient 來獲取進行補全。提供數據的服務有的響應時間可能會很長,也有可能會造成服務整體的阻塞。

如上圖所示,接口 A 通過 HttpClient 訪問服務 2,響應 100ms 后返回;接口 B 訪問服務 3,耗時 2 秒。HttpClient 本身是有一個最大連接數限制的,如果服務 3 遲遲不返回,就會造成 HttpClient 的連接數達到上限,最上層的 Tomcat 線程也會一直阻塞在這里,進而連響應速度比較快的接口 A 也無法正常提供服務。
這是出現頻率非常高的的一類故障,在工作中你會大概率遇見。概括來講,就是同一服務,由于一個耗時非常長的接口,進而引起了整體的服務不可用。
這個時候,通過 jstack 打印棧信息,會發現大多數竟然阻塞在了接口 A 上,而不是耗時更長的接口 B。這是一種錯覺,其實是因為接口 A 的速度比較快,在問題發生點進入了更多的請求,它們全部都阻塞住了。
證據本身具有非常強的迷惑性。由于這種問題發生的頻率很高,排查起來又比較困難,我這里專門做了一個小工程,用于還原解決這種問題的一個方式,參見 report-demo 工程。
demo 模擬了兩個使用同一個 HttpClient 的接口。如下圖所示,fast 接口用來訪問百度,很快就能返回;slow 接口訪問谷歌,由于眾所周知的原因,會阻塞直到超時,大約 10 s。?

使用 wrk 工具對這兩個接口發起壓測。
```
wrk?-t10?-c200?-d300s?http://127.0.0.1:8084/slow
wrk?-t10?-c200?-d300s?http://127.0.0.1:8084/fast
```

此時訪問一個簡單的接口,耗時竟然能夠達到 20 秒。
```
time?curl?http://localhost:8084/stat
fast648,slow:1curl?http://localhost:8084/stat??0.01s?user?0.01s?system?0%?cpu?20.937?total
```
使用 jstack 工具 dump 堆棧。首先使用 jps 命令找到進程號,然后把結果重定向到文件(可以參考 10271.jstack 文件)。
過濾一下 nio 關鍵字,可以查看 tomcat 相關的線程,足足有 200 個,這和 Spring Boot 默認的 maxThreads 個數不謀而合。更要命的是,有大多數線程,都處于 BLOCKED 狀態,說明線程等待資源超時。
```
cat?10271.jstack?|grep?http-nio-80?-A?3
```

使用腳本分析,發現有大量的線程阻塞在 fast 方法上。我們上面也說過,這是一個假象,可能你到了這一步,會心生存疑,以至于無法再向下分析。
```
$?cat?10271.jstack?|grep?fast?|?wc?-l
?????137
$?cat?10271.jstack?|grep?slow?|?wc?-l
??????63
```
分析棧信息,你可能會直接查找 locked 關鍵字,如下圖所示,但是這樣的方法一般沒什么用,我們需要做更多的統計。?

注意下圖中有一個處于 BLOCKED 狀態的線程,它阻塞在對鎖的獲取上(wating to lock)。大體瀏覽一下 DUMP 文件,會發現多處這種狀態的線程,可以使用如下腳本進行統計。?

```
cat?10271.tdump|?grep?"waiting?to?lock?"?|?awk?'{print?$5}'?|?sort?|?uniq?-c?|?sort?-k1?-r
??26?<0x0000000782e1b590>
??18?<0x0000000787b00448>
??16?<0x0000000787b38128>
??10?<0x0000000787b14558>
???8?<0x0000000787b25060>
???4?<0x0000000787b2da18>
???4?<0x0000000787b00020>
???2?<0x0000000787b6e8e8>
???2?<0x0000000787b03328>
???2?<0x0000000782e8a660>
???1?<0x0000000787b6ab18>
???1?<0x0000000787b2ae00>
???1?<0x0000000787b0d6c0>
???1?<0x0000000787b073b8>
???1?<0x0000000782fbcdf8>
???1?<0x0000000782e11200>
???1?<0x0000000782dfdae0>
```
我們找到給 0x0000000782e1b590 上鎖的執行棧,可以發現全部卡在了 HttpClient 的讀操作上。在實際場景中,可以看下排行比較靠前的幾個鎖地址,找一下共性。?

返回頭去再看一下代碼。我們發現 HttpClient 是共用了一個連接池,當連接數超過 100 的時候,就會阻塞等待。它的連接超時時間是 10 秒,這和 slow 接口的耗時不相上下。?
```
private?final?static?HttpConnectionManager?httpConnectionManager?=?new?SimpleHttpConnectionManager(true);
????static?{
????????HttpConnectionManagerParams?params?=?new?HttpConnectionManagerParams();
????????params.setMaxTotalConnections(100);
????????params.setConnectionTimeout(1000?*?10);
????????params.setSoTimeout(defaultTimeout);
????????httpConnectionManager.setParams(params);
```
slow 接口和 fast 接口同時在爭搶這些連接,讓它時刻處在飽滿的狀態,進而讓 tomcat 的線程等待、占滿,造成服務不可用。
問題找到了,解決方式就簡單多了。我們希望 slow 接口在阻塞的時候,并不影響 fast 接口的運行。這就可以對某一類接口進行限流,或者對不重要的接口進行熔斷處理,這里不再深入講解(具體可參考 Spring Boot 的限流熔斷處理)。
現實情況是,對于一個運行的系統,我們并不知道是 slow 接口慢還是 fast 接口慢,這就需要加入一些額外的日志信息進行排查。當然,如果有一個監控系統能夠看到這些數據是再好不過了。
項目中的 HttpClientUtil2 文件,是改造后的一個版本。除了調大了連接數,它還使用了多線程版本的連接管理器(MultiThreadedHttpConnectionManager),這個管理器根據請求的 host 進行劃分,每個 host 的最大連接數不超過 20。還提供了 getConnectionsInPool 函數,用于查看當前連接池的統計信息。采用這些輔助的手段,可以快速找到問題服務,這是典型的情況。由于其他應用的服務水平低而引起的連鎖反應,一般的做法是熔斷、限流等,在此不多做介紹了。
#### jstack 產生的信息
為了觀測一些狀態,我上傳了幾個 Java 類,你可以實際運行一下,然后使用 jstack 來看一下它的狀態。
```
waiting on condition
```
示例參見 SleepDemo.java。
```
public?class?SleepDemo?{
????public?static?void?main(String[]?args)?{
????????new?Thread(()->{
????????????try?{
????????????????Thread.sleep(Integer.MAX_VALUE);
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????},"sleep-demo").start();
????}
}
```
這個狀態出現在線程等待某個條件的發生,來把自己喚醒,或者調用了 sleep 函數,常見的情況就是等待網絡讀寫,或者等待數據 I/O。如果發現大多數線程都處于這種狀態,證明后面的資源遇到了瓶頸。
此時線程狀態大致分為以下兩種:
* java.lang.Thread.State: WAITING (parking):一直等待條件發生;
* java.lang.Thread.State: TIMED_WAITING (parking 或 sleeping):定時的,即使條件不觸發,也將定時喚醒。
```
"sleep-demo"?#12?prio=5?os_prio=31?cpu=0.23ms?elapsed=87.49s?tid=0x00007fc7a7965000?nid=0x6003?waiting?on?condition??[0x000070000756d000]
???java.lang.Thread.State:?TIMED_WAITING?(sleeping)
????at?java.lang.Thread.sleep(java.base@13.0.1/Native?Method)
????at?SleepDemo.lambda$main$0(SleepDemo.java:5)
????at?SleepDemo$$Lambda$16/0x0000000800b45040.run(Unknown?Source)
????at?java.lang.Thread.run(java.base@13.0.1/Thread.java:830)
```
值的注意的是,Java 中的可重入鎖,也會讓線程進入這種狀態,但通常帶有 parking 字樣,parking 指線程處于掛起中,要注意區別。代碼可參見 LockDemo.java:
```
import?java.util.concurrent.locks.Lock;
import?java.util.concurrent.locks.ReentrantLock;
public?class?LockDemo?{
????public?static?void?main(String[]?args)?{
????????Lock?lock?=?new?ReentrantLock();
????????lock.lock();
????????new?Thread(()?->?{
????????????try?{
????????????????lock.lock();
????????????}?finally?{
????????????????lock.unlock();
????????????}
????????},?"lock-demo").start();
????}
```
堆棧代碼如下:
```
"lock-demo"?#12?prio=5?os_prio=31?cpu=0.78ms?elapsed=14.62s?tid=0x00007ffc0b949000?nid=0x9f03?waiting?on?condition??[0x0000700005826000]
???java.lang.Thread.State:?WAITING?(parking)
????at?jdk.internal.misc.Unsafe.park(java.base@13.0.1/Native?Method)
????-?parking?to?wait?for??<0x0000000787cf0dd8>?(a?java.util.concurrent.locks.ReentrantLock$NonfairSync)
????at?java.util.concurrent.locks.LockSupport.park(java.base@13.0.1/LockSupport.java:194)
????at?java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(java.base@13.0.1/AbstractQueuedSynchronizer.java:885)
????at?java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(java.base@13.0.1/AbstractQueuedSynchronizer.java:917)
????at?java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(java.base@13.0.1/AbstractQueuedSynchronizer.java:1240)
????at?java.util.concurrent.locks.ReentrantLock.lock(java.base@13.0.1/ReentrantLock.java:267)
????at?LockDemo.lambda$main$0(LockDemo.java:11)
????at?LockDemo$$Lambda$14/0x0000000800b44840.run(Unknown?Source)
????at?java.lang.Thread.run(java.base@13.0.1/Thread.java:830)
waiting for monitor entry
```
我們上面提到的 HttpClient 例子,就是大部分處于這種狀態,線程都是 BLOCKED 的。這意味著它們都在等待進入一個臨界區,需要重點關注。
```
"http-nio-8084-exec-120"?#143?daemon?prio=5?os_prio=31?cpu=122.86ms?elapsed=317.88s?tid=0x00007fedd8381000?nid=0x1af03?waiting?for?monitor?entry??[0x00007000150e1000]
???java.lang.Thread.State:?BLOCKED?(on?object?monitor)
????at?java.io.BufferedInputStream.read(java.base@13.0.1/BufferedInputStream.java:263)
????-?waiting?to?lock?<0x0000000782e1b590>?(a?java.io.BufferedInputStream)
????at?org.apache.commons.httpclient.HttpParser.readRawLine(HttpParser.java:78)
????at?org.apache.commons.httpclient.HttpParser.readLine(HttpParser.java:106)
????at?org.apache.commons.httpclient.HttpConnection.readLine(HttpConnection.java:1116)
????at?org.apache.commons.httpclient.HttpMethodBase.readStatusLine(HttpMethodBase.java:1973)
????at?org.apache.commons.httpclient.HttpMethodBase.readResponse(HttpMethodBase.java:1735)
in Object.wait()
```
示例代碼參見 WaitDemo.java:
```
public?class?WaitDemo?{
????public?static?void?main(String[]?args)?throws?Exception?{
????????Object?o?=?new?Object();
????????new?Thread(()?->?{
????????????try?{
????????????????synchronized?(o)?{
????????????????????o.wait();
????????????????}
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????},?"wait-demo").start();
????????Thread.sleep(1000);
????????synchronized?(o)?{
????????????o.wait();
????????}
????}
```
說明在獲得了監視器之后,又調用了 java.lang.Object.wait() 方法。
關于這部分的原理,可以參見一張經典的圖。每個監視器(Monitor)在某個時刻,只能被一個線程擁有,該線程就是“Active Thread”,而其他線程都是“Waiting Thread”,分別在兩個隊列“Entry Set”和“Wait Set”里面等候。在“Entry Set”中等待的線程狀態是“Waiting for monitor entry”,而在“Wait Set”中等待的線程狀態是“in Object.wait()”。

```
"wait-demo"?#12?prio=5?os_prio=31?cpu=0.14ms?elapsed=12.58s?tid=0x00007fb66609e000?nid=0x6103?in?Object.wait()??[0x000070000f2bd000]
???java.lang.Thread.State:?WAITING?(on?object?monitor)
????at?java.lang.Object.wait(java.base@13.0.1/Native?Method)
????-?waiting?on?<0x0000000787b48300>?(a?java.lang.Object)
????at?java.lang.Object.wait(java.base@13.0.1/Object.java:326)
????at?WaitDemo.lambda$main$0(WaitDemo.java:7)
????-?locked?<0x0000000787b48300>?(a?java.lang.Object)
????at?WaitDemo$$Lambda$14/0x0000000800b44840.run(Unknown?Source)
????at?java.lang.Thread.run(java.base@13.0.1/Thread.java:830)
```
死鎖
代碼參見 DeadLock.java:
```
public?class?DeadLockDemo?{
????public?static?void?main(String[]?args)?{
????????Object?object1?=?new?Object();
????????Object?object2?=?new?Object();
????????Thread?t1?=?new?Thread(()?->?{
????????????synchronized?(object1)?{
????????????????try?{
????????????????????Thread.sleep(200);
????????????????}?catch?(InterruptedException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????????synchronized?(object2)?{
????????????????}
????????????}
????????},?"deadlock-demo-1");
????????t1.start();
????????Thread?t2?=?new?Thread(()?->?{
????????????synchronized?(object2)?{
????????????????synchronized?(object1)?{
????????????????}
????????????}
????????},?"deadlock-demo-2");
????????t2.start();
????}
}
```
死鎖屬于比較嚴重的一種情況,jstack 會以明顯的信息進行提示。
```
Found?one?Java-level?deadlock:
=============================
"deadlock-demo-1":
??waiting?to?lock?monitor?0x00007fe5e406f500?(object?0x0000000787cecd78,?a?java.lang.Object),
??which?is?held?by?"deadlock-demo-2"
"deadlock-demo-2":
??waiting?to?lock?monitor?0x00007fe5e406d500?(object?0x0000000787cecd68,?a?java.lang.Object),
??which?is?held?by?"deadlock-demo-1"
Java?stack?information?for?the?threads?listed?above:
===================================================
"deadlock-demo-1":
????at?DeadLockDemo.lambda$main$0(DeadLockDemo.java:13)
????-?waiting?to?lock?<0x0000000787cecd78>?(a?java.lang.Object)
????-?locked?<0x0000000787cecd68>?(a?java.lang.Object)
????at?DeadLockDemo$$Lambda$14/0x0000000800b44c40.run(Unknown?Source)
????at?java.lang.Thread.run(java.base@13.0.1/Thread.java:830)
"deadlock-demo-2":
????at?DeadLockDemo.lambda$main$1(DeadLockDemo.java:21)
????-?waiting?to?lock?<0x0000000787cecd68>?(a?java.lang.Object)
????-?locked?<0x0000000787cecd78>?(a?java.lang.Object)
????at?DeadLockDemo$$Lambda$16/0x0000000800b45040.run(Unknown?Source)
????at?java.lang.Thread.run(java.base@13.0.1/Thread.java:830)
Found?1?deadlock
```
當然,關于線程的 dump,也有一些線上分析工具可以使用。下圖是 fastthread 的一個分析結果,但也需要你先了解這些情況發生的意義。

#### 小結
本課時主要介紹了一個處處有問題的報表系統,并逐步解決了它的 OOM 問題,同時定位到了拒絕服務的原因。
在研發資源不足的時候,我們簡單粗暴的進行了硬件升級,并切換到了更加優秀的 G1 垃圾回收器,還通過代碼手段進行了問題的根本解決:
* 縮減查詢的字段,減少常駐內存的數據;
* 去掉不必要的、命中率低的堆內緩存,改為分布式緩存;
* 從產品層面限制了單次請求對內存的無限制使用。
在這個過程中,使用 MAT 分析堆數據進行問題代碼定位,幫了大忙。代碼優化的手段是最有效的,改造完畢后,可以節省更多的硬件資源。事實上,使用了 G1 垃圾回收器之后,那些亂七八糟的調優參數越來越少用了。
接下來,我們使用 jstack 分析了一個出現頻率非常非常高的問題,主要是不同速度的接口在同一應用中的資源競爭問題,我們發現一些成熟的微服務框架,都會對這些資源進行限制和隔離。
最后,以 4 個簡單的示例,展示了 jstack 輸出內容的一些意義。代碼都在 git 倉庫里,你可以實際操作一下,希望對你有所幫助。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充