10w 級的并發場景,JVM 有哪些方面值得優化呢?
背景
以秒殺為例,大型電商系統會拆分多個微服務,比如訂單服務,商品服務,優惠服務,庫存服務,支付服務等等。
我們以下單為例,如下是簡約版下單流程
一般我們需要結合業務和已有的資源,再去評估 JVM 的 GC 頻率來作為參考:
- 首先結合業務流程分析,計算我們服務系統每秒產生的對象占用內存大小。
- 假設我們采用服務器規模 8C*16G,估算下新生代的空間,大概多久觸發 MinorGC。
- 為了避免頻繁的 Full GC,我們可以重新估算具體需要的機器配置和數量,給 JVM 設置多大內存。
我們先來分析下在 10W TPS 下單場景中,我們每秒產生的對象占用內存大小。
如何選擇垃圾回收器?
在選擇垃圾回收器的時候,我們需要考慮兩個指標:
- 吞吐量:CPU 在用戶應用程序運行的時間/(CPU 在用戶應用程序運行的時間+CPU 垃圾回收器運行的時間)。
- 響應時間:平均每次 GC 的耗時。
目前主流的垃圾回收器配置是新生代采用 ParNew,老年代采用 CMS 組合的方式,雖然在 JDK8 以后更加推薦使用 G1。一般來說,對于延遲敏感的推薦 CMS;在大內存、要求高吞吐推薦 G1。
在電商下單場景中,用戶對于些許的延遲可能會比較敏感,所以以下還是以 CMS 使用為例。
然后,我要如何設置 JVM 參數呢?
堆內存大小設置
對于 8G 內存,我們一般設置內存一半給 JVM,正常的 JVM 參數設置如下
-Xms8192M -Xmx8192M Xmn2048M -Xss1M -XX:MMetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
這樣設置,而在下單場景,大多對象都是短期存活的,這樣設置因為動態對象年齡判斷原則導致頻繁出發 Full GC。
所以可以調整新生代內存大小
-Xms8192M -Xmx8192M -Xmn4086M -Xss1M -XX:MMetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
這樣就降低了對象頻繁進入老年代的問題,實際上很多優化都是圍繞著如何減少 Full GC 去做的,就是盡可能把短期存活的對象留在 survivor 里,不要進入老年代,這樣就可以在 Minor GC 的時候回收掉這些對象,不會產生 Full GC,從而引發 STW,影響系統性能。
除了新生代大小外,還有什么可以優化呢?
常見 JVM 參數優化
- 新生代動態對象年齡:默認設置為 15。本例子中每次 Minor GC 間隔 20s 左右,而在下單場景中,一般對象幾秒內就會變成垃圾對象了,像這么長時間都還沒被回收的話,其實可以早點放到老年代,這樣也不會占用新生代的內存了,比如通過設置參數-XX:MaxTenuringThreshold=5,那么經歷過 5 次 Minor GC 后就會進入老年代。
- 針對大對象,有時我們會在本地緩存一些不常變化的配置,可結合實際業務評估這些對象大小比如會超過 2M,而且會一直存活保留,那么針對這些對象其實可以直接進入老年代,可以通過設置參數-XX:PretenureSizeThreshold=2M。
- 針對 CMS 的碎片整理,因為 CMS 基于標記-清除算法實現,會產生內存碎片,如果這些內存碎片長時間不清理的話,那么老年代的內存可用空間會降低,所以 CMS 也提供了兩個參數用于內存碎片的整理:
- -XX:+UseCMSCompactAtFullCollection,開啟內存碎片整理。
- -XX:CMSFullGCsBeforeCompaction,執行指定次數的 Full GC 后,進行一次內存整理壓縮整理的 Full GC。
- 元空間大小,Meta 區域的大小一定要指定,如果我們代碼類或引入動態生成類的技術超過元空間大小,那么會觸發 Full GC,可以通過 jstat 命令查看項目生成類的大小來評估具體設置值,一般設置 256M 夠了。
- JIT 即時編譯,-XX:ReservedCodeCacheSize。JIT 是 JVM 一個非常重要的特性,CodeCahce 存放的就是即時編譯器所生成的二進制代碼。
- JVM 逃逸分析,逃逸分析也是一種優化手段,JVM 如果分析確定一個對象不會逃逸于方法之外,那么這個對象會被分配在棧上,而不是在堆上,這樣一定程度上就可以減輕 GC 壓力。