本課時我們主要分享一個實踐案例,即大型項目如何進行性能瓶頸調優,這也是對前面所學的知識進行總結。
性能調優是一個比較大且比較模糊的話題。在大型項目中,既有分布式的交互式調優問題,也有純粹的單機調優問題。由于我們的課程主要講解 JVM 相關的知識點,重點關注 JVM 的調優、故障或者性能瓶頸方面的問題排查,所以對于分布式應用中的影響因素,這里不過多介紹。
優化層次
下面是我總結的一張關于優化層次的圖,箭頭表示優化時需考慮的路徑,但也不總是這樣。當一個系統出現問題的時候,研發一般不會想要立刻優化 JVM,或者優化操作系統,會嘗試從最高層次上進行問題的解決:解決最主要的瓶頸點。

**數據庫優化**: 數據庫是最容易成為瓶頸的組件,研發會從 SQL 優化或者數據庫本身去提高它的性能。如果瓶頸依然存在,則會考慮分庫分表將數據打散,如果這樣也沒能解決問題,則可能會選擇緩存組件進行優化。這個過程與本課時相關的知識點,可以使用 jstack 獲取阻塞的執行棧,進行輔助分析。
**集群最優**:存儲節點的問題解決后,計算節點也有可能發生問題。一個集群系統如果獲得了水平擴容的能力,就會給下層的優化提供非常大的時間空間,這也是彈性擴容的魅力所在。我接觸過一個服務,由最初的 3 個節點,擴容到最后的 200 多個節點,但由于人力問題,服務又沒有什么新的需求,下層的優化就一直被擱置著。
**硬件升級**:水平擴容不總是有效的,原因在于單節點的計算量比較集中,或者 JVM 對內存的使用超出了宿主機的承載范圍。在動手進行代碼優化之前,我們會對節點的硬件配置進行升級。升級容易,降級難,降級需要依賴代碼和調優層面的優化。
**代碼優化**:出于成本的考慮,上面的這些問題,研發團隊并不總是坐視不管。代碼優化是提高性能最有效的方式,但需要收集一些數據,這個過程可能是服務治理,也有可能是代碼流程優化。我在第 21 課時介紹的 JavaAgent 技術,會無侵入的收集一些 profile 信息,供我們進行決策。像 Sonar 這種質量監控工具,也可以在此過程中幫助到我們。
**并行優化**:并行優化的對象是這樣一種接口,它占用的資源不多,計算量也不大,就是速度太慢。所以我們通常使用 ContDownLatch 對需要獲取的數據進行并行處理,效果非常不錯,比如在 200ms 內返回對 50 個耗時 100ms 的下層接口的調用。
**JVM 優化**:雖然對 JVM 進行優化,有時候會獲得巨大的性能提升,但在 JVM 不發生問題時,我們一般不會想到它。原因就在于,相較于上面 5 層所達到的效果來說,它的優化效果有限。但在代碼優化、并行優化、JVM 優化的過程中,JVM 的知識卻起到了關鍵性的作用,是一些根本性的影響因素。
**操作系統優化**:操作系統優化是解決問題的殺手锏,比如像 HugePage、Luma、“CPU 親和性”這種比較底層的優化。但就計算節點來說,對操作系統進行優化并不是很常見。運維在背后會做一些諸如文件句柄的調整、網絡參數的修改,這對于我們來說就已經夠用了。
雖然本課程是針對比較底層的 JVM,但我還是想談一下一個研發對技術體系的整體演進方向。
首先,掌握了比較底層、基礎的東西后,在了解一些比較高層的設計時,就能花更少的時間,這方面的知識有:操作系統、網絡、多線程、編譯原理,以及一門感興趣的開發語言。對 Java 體系來說,毫無疑問就是 Java 語言和 JVM。
其次,知識體系還要看實用性,比如你熟知編譯原理,雖然 JIT 很容易入門,但如果不做相關的開發,這并沒有什么實際作用。
最后,現代分布式系統在技術上總是一個權衡的結果(比如 CAP)。在分析一些知識點和面試題的時候,也要看一下哪些是權衡的結果,哪些務必是準確的。整體上達到次優,局部上達到最優,就是我們要追尋的結果。
代碼優化、JVM 的調優,以及單機的故障排查,就是一種局部上的尋優過程,也是一個合格的程序員必須要掌握的技能。
#### JVM 調優
由于 JVM 一直處在變化之中,所以一些參數的配置并不總是有效的,有時候你加入一個參數,“感覺上”運行速度加快了,但通過`-XX:+PrintFlagsFinal` 來查看,卻發現這個參數默認就是這樣,比如第 10 課時提到的 UseAdaptiveSizePolicy。所以,在不同的 JVM 版本上,不同的垃圾回收器上,要先看一下這個參數默認是什么,不要輕信他人的建議。
```
java?-XX:+PrintFlagsFinal?-XX:+UseG1GC??2>&1?|?grep?UseAdaptiveSizePolicy
```
* [ ] 內存區域大小
首先要調整的,就是各個分區的大小,不過這也要分垃圾回收器,我們來看一些全局參數及含義。
* -XX:+UseG1GC:用于指定 JVM 使用的垃圾回收器為 G1,盡量不要靠默認值去保證,要顯式的指定一個。
* -Xmx:設置堆的最大值,一般為操作系統的 2/3 大小。
* -Xms:設置堆的初始值,一般設置成和 Xmx 一樣的大小來避免動態擴容。
* -Xmn:表示年輕代的大小,默認新生代占堆大小的 1/3。高并發、對象快消亡場景可適當加大這個區域,對半,或者更多,都是可以的。但是在 G1 下,就不用再設置這個值了,它會自動調整。
* -XX:MaxMetaspaceSize:用于限制元空間的大小,一般 256M 足夠了,這一般和初始大小 -XX:MetaspaceSize 設置成一樣的。
* -XX:MaxDirectMemorySize:用于設置直接內存的最大值,限制通過 DirectByteBuffer 申請的內存。
* -XX:ReservedCodeCacheSize:用于設置 JIT 編譯后的代碼存放區大小,如果觀察到這個值有限制,可以適當調大,一般夠用即可。
* -Xss:用于設置棧的大小,默認為 1M,已經足夠用了。
* [ ] 內存調優
* -XX:+AlwaysPreTouch:表示在啟動時就把參數里指定的內存全部初始化,啟動時間會慢一些,但運行速度會增加。
* -XX:SurvivorRatio:默認值為 8,表示伊甸區和幸存區的比例。
* -XX:MaxTenuringThreshold:這個值在 CMS 下默認為 6,G1 下默認為 15,這個值和我們前面提到的對象提升有關,改動效果會比較明顯。對象的年齡分布可以使用 -XX:+PrintTenuringDistribution 打印,如果后面幾代的大小總是差不多,證明過了某個年齡后的對象總能晉升到老生代,就可以把晉升閾值設小。
* PretenureSizeThreshold:表示超過一定大小的對象,將直接在老年代分配,不過這個參數用的不是很多。
其他容量的相關參數可以參考其他課時,但不建議隨便更改。
#### 垃圾回收器優化
接下來看一下主要的垃圾回收器。
**CMS 垃圾回收器**
* -XX:+UseCMSInitiatingOccupancyOnly:這個參數需要加上 -XX:CMSInitiatingOccupancyFraction,注意后者需要和前者一塊配合才能完成工作,它們指定了 MajorGC 的發生時機。
* -XX:ExplicitGCInvokesConcurrent:當代碼里顯示調用了 System.gc(),實際上是想讓回收器進行 FullGC,如果發生這種情況,則使用這個參數開始并行 FullGC,建議加上這個參數。
* -XX:CMSFullGCsBeforeCompaction:這個參數的默認值為 0,代表每次 FullGC 都對老生代進行碎片整理壓縮,建議保持默認。
* -XX:CMSScavengeBeforeRemark:表示開啟或關閉在 CMS 重新標記階段之前的清除(YGC)嘗試,它可以降低 remark 時間,建議加上。
* -XX:+ParallelRefProcEnabled:可以用來并行處理 Reference,以加快處理速度,縮短耗時,具體用法見第 15 課時。
* [ ] G1 垃圾回收器
* -XX:MaxGCPauseMillis:用于設置目標停頓時間,G1 會盡力達成。
* -XX:G1HeapRegionSize:用于設置小堆區大小,這個值為 2 的次冪,不要太大,也不要太小,如果實在不知道如何設置,建議保持默認。
* -XX:InitiatingHeapOccupancyPercent:表示當整個堆內存使用達到一定比例(默認是 45%),并發標記階段 就會被啟動。
* -XX:ConcGCThreads:表示并發垃圾收集器使用的線程數量,默認值隨 JVM 運行的平臺不同而變動,不建議修改。
* [ ] 其他參數優化
* -XX:AutoBoxCacheMax:用于加大 IntegerCache,具體原因可參考第 20 課時。
* -Djava.security.egd=file:/dev/./urandom:這個參數使用 urandom 隨機生成器,在進行隨機數獲取時,速度會更快。
* -XX:-OmitStackTraceInFastThrow:用于減少異常棧的輸出,并進行合并。雖然會對調試有一定的困擾,但能在發生異常時顯著增加性能。
* [ ] 存疑優化
* -XX:-UseBiasedLocking:用于取消偏向鎖(第 19 課時),理論上在高并發下會增加效率,這個需要實際進行觀察,在無法判斷的情況下,不需要配置。
* JIT 參數:這是我們在第 22 課時多次提到的 JIT 編譯參數,這部分最好不要亂改,會產生意想不到的問題。
* [ ] GC 日志
這部分我們在第 9 課時進行了詳細的介紹,在此不再重復。
下面來看一個在 G1 垃圾回收器運行的 JVM 啟動命令。
```
java \
? ?-XX:+UseG1GC \
? ?-XX:MaxGCPauseMillis=100 \
? ?-XX:InitiatingHeapOccupancyPercent=45 \
? ?-XX:G1HeapRegionSize=16m \
? ?-XX:+ParallelRefProcEnabled \
? ?-XX:MaxTenuringThreshold=3 \
? ?-XX:+AlwaysPreTouch \
? ?-Xmx5440M \
? ?-Xms5440M \
? ?-XX:MaxMetaspaceSize=256M \
? ?-XX:MetaspaceSize=256M \
? ?-XX:MaxDirectMemorySize=100M \
? ?-XX:ReservedCodeCacheSize=268435456 \
? ?-XX:-OmitStackTraceInFastThrow \
? ?-Djava.security.egd=file:/dev/./urandom \
? ?-verbose:gc \
? ?-XX:+PrintGCDetails \
? ?-XX:+PrintGCDateStamps \
? ?-XX:+PrintGCApplicationStoppedTime \
? ?-XX:+PrintGCApplicationConcurrentTime ?\
? ?-XX:+PrintTenuringDistribution \
? ?-XX:+PrintClassHistogramBeforeFullGC \
? ?-XX:+PrintClassHistogramAfterFullGC \
? ?-Xloggc:/tmp/logs/gc_%p.log \
? ?-XX:+HeapDumpOnOutOfMemoryError \
? ?-XX:HeapDumpPath=/tmp/logs \
? ?-XX:ErrorFile=/tmp/logs/hs_error_pid%p.log \
? ?-Djava.rmi.server.hostname=127.0.0.1 \
? ?-Dcom.sun.management.jmxremote \
? ?-Dcom.sun.management.jmxremote.port=14000 \
? ?-Dcom.sun.management.jmxremote.ssl=false \
? ?-Dcom.sun.management.jmxremote.authenticate=false \
? ?-javaagent:/opt/test.jar \
? ?MainRun
```
* [ ] 故障排查
有需求才需要優化,不要為了優化而優化。一般來說,上面提到的這些 JVM 參數,基本能夠保證我們的應用安全,如果想要更進一步、更專業的性能提升,就沒有什么通用的法則了。
打印詳細的 GCLog,能夠幫助我們了解到底是在哪一步驟發生了問題,然后才能對癥下藥。使用 gceasy.io 這樣的線上工具,能夠方便的分析到結果,但一些偏門的 JVM 參數修改,還是需要進行詳細的驗證。
一次或者多次模擬性的壓力測試是必要的,能夠讓我們提前發現這些優化點。
我們花了非常大的篇幅,來講解 JVM 中故障排查的問題,這也是和我們工作中聯系最緊密的話題。
JVM 故障會涉及到內存問題和計算問題,其中內存問題占多數。除了程序計數器,JVM 內存里劃分每一個區域,都有溢出的可能,最常見的就是堆溢出。使用 jmap 可以 dump 一份內存,然后使用 MAT 工具進行具體原因的分析。
對堆外內存的排查需要較高的技術水平,我們在第 13 課時進行了詳細的講解。當你發現進程占用的內存資源比使用 Xmx 設置得要多,那么不要忘了這一環。
使用 jstack 可以獲取 JVM 的執行棧,并且能夠看到線程的一些阻塞狀態,這部分可以使用 arthas 進行瞬時態的獲取,定位到瞬時故障。另外,一個完善的監控系統能夠幫我們快速定位問題,包括操作系統的監控、JVM 的監控等。
代碼、JVM 優化和故障排查是一個持續優化的過程,只有更優、沒有最優。如何在有限的項目時間內,最高效的完成工作,才是我們所需要的。
#### 小結
本課時對前面的課程內容做了個簡單的總結,從 7 個層面的優化出發,簡要的談了一下可能的優化過程,然后詳細地介紹了一些常見的優化參數。
JVM 的優化效果是有限的,但它是理論的基礎,代碼優化和參數優化都需要它的指導。同時,有非常多的工具能夠幫我們定位到問題。
偏門的優化參數可能有效,但不總是有效。實際上,從 CMS 到 G1,再到 ZGC,關于 GC 優化的配置參數也越來越少,但協助排查問題的工具卻越來越多。在大多數場景下,JVM 已經能夠達到開箱即用的高性能效果,這也是一個虛擬機所追求的最終目標。
- 前言
- 開篇詞
- 基礎原理
- 第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 面試題補充