掃描下方海報二維碼,試聽課程:
面試
(課程詳細大綱,請參見文末)數組
============================================緩存
本文來源於專欄:《從零開始帶你成爲JVM實戰高手》
併發
是做者救火隊隊長開放的試讀文章jvm
案例背景引入分佈式
特殊的電商大促場景高併發
抗住大促的瞬時壓力須要幾臺機器?性能
大促高峯期訂單系統的內存使用模型估算優化
內存到底該如何分配?操作系統
新生代垃圾回收優化之一:Survivor空間夠不夠
新生代對象躲過多少次垃圾回收後進入老年代?
多大的對象直接進入老年代?
別忘了指定垃圾回收器
今日思考題
按照慣例,咱們接下來會用案例驅動來帶着你們分析到底該如何在特定場景下,預估系統的內存使用模型。
而後合理優化新生代、老年代、Eden和Survivor各個區域的內存大小。
接着再儘可能優化參數避免新生代的對象進入老年代,儘可能讓對象留在新生代裏被回收掉。
咱們這裏的背景是電商系統,電商系統其實通常會拆分爲不少的子系統獨立部署
好比商品系統、訂單系統、促銷系統、庫存系統、倉儲系統、會員系統,等等
咱們這裏就以比較核心的訂單系統做爲例子來講明。
(提示:食用本案例以前,請務必充分理解專欄以前兩週的文章!)
咱們的案例背景是每日上億請求量的電商系統,那麼你們能夠來推算一下每日上億請求量的電商系統,他會每日有多少活躍用戶?
通常按每一個用戶平均訪問20次來計算,那麼上億請求量,大體須要有500萬日活用戶。
那麼繼續來推算一下,這500萬的日活用戶都是會進來進行大量的瀏覽,那麼多少人會下訂單?
這裏能夠按照10%的付費轉化率來計算,天天大概有50萬人會下訂單,那麼大體就是天天會有50萬訂單。
這50萬訂單算他集中在天天4小時的高峯期內,那麼其實平均下來每秒鐘大概也就幾十個訂單,你們是否是以爲根本沒啥可說的?
由於幾十個訂單的壓力下,根本就不須要對JVM多關注,基本上就是每秒鐘佔用一些新生代內存,隔好久新生代纔會滿。而後一次Minor GC後垃圾對象清理掉,內存就空出來了,幾乎無壓力。
可是若是你要是考慮到特殊的電商大促場景,就不會這麼想了
由於不少中小型的電商平臺,確實平時系統壓力其實沒那麼大,也沒太大的高併發,每秒幾千併發壓力就算是高峯壓力了。
可是若是遇到一些大促場景,好比雙11什麼的,狀況就不一樣了。
假設在相似雙11的節日裏,零點的時候,不少人等着大促開始就要剁手購物,這個時候,可能在大促開始的短短10分鐘內,瞬間就會有50萬訂單。
那麼此時每秒就會有接近1000的下單請求,咱們就針對這種大促場景來對訂單系統的內存使用模型分析一下。
那麼要抗住大促期間的瞬時下單壓力,訂單系統須要部署幾臺機器呢?
基本上能夠按3臺來算,就是每臺機器每秒須要抗300個下單請求。這個也是很是合理的,並且須要假設訂單系統部署的就是最普通的標配4核8G機器。
從機器自己的CPU資源和內存資源角度,抗住每秒300個下單請求是沒問題的。
可是問題就在於須要對JVM有限的內存資源進行合理的分配和優化,包括對垃圾回收進行合理的優化,讓JVM的GC次數儘量最少,並且儘可能避免Full GC,這樣能夠儘量減小JVM的GC對高峯期的系統新更難的影響。
背景已經所有說完了,接下來我們就得來預估訂單系統的內存使用模型了.
基本上能夠按照每秒鐘處理300個下單請求來估算,其實不管是訂單處理性能仍是併發狀況,都跟生產很接近
由於處理下單請求是比較耗時的,涉及不少接口的調用,基本上每秒處理100~300個下單請求是差很少的。
那麼每一個訂單我們就按1kb的大小來估算,單單是300個訂單就會有300kb的內存開銷
而後算上訂單對象連帶的訂單條目對象、庫存、促銷、優惠券等等一系列的其餘業務對象,通常須要對單個對象開銷放大10倍~20倍。
此外,除了下單以外,這個訂單系統還會有不少訂單相關的其餘操做,好比訂單查詢之類的,因此連帶算起來,能夠往大了估算,再擴大10倍的量。
那麼每秒鐘會有大概300kb * 20 * 10 = 60mb的內存開銷。
可是一秒事後,能夠認爲這60mb的對象就是垃圾了,由於300個訂單處理完了,全部相關對象都失去了引用,能夠回收的狀態。
你們看下圖:
假設咱們有4核8G的機器,那麼給JVM的內存通常會到4G,剩下幾個G會留點空餘給操做系統之類的來使用
不要想着把機器內存一會兒都耗盡,其中堆內存咱們能夠給3G,新生代咱們能夠給到1.5G,老年代也是1.5G。
而後每一個線程的Java虛擬機棧有1M,那麼JVM裏若是有幾百個線程大概會有幾百M
而後再給永久代256M內存,基本上這4G內存就差很少了。
同時還要記得設置一些必要的參數,好比說打開「-XX:HandlePromotionFailure」選項(不熟悉這個參數的,能夠回頭複習一下專欄以前的文章)
JVM參數以下所示:
「-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:HandlePromotionFailure」
可是「-XX:HandlePromotionFailure」參數在JDK 1.6之後就被廢棄了,因此如今通常都不會在生產環境裏設置這個參數了。
在JDK 1.6之後,只要判斷「老年代可用空間」> 「新生代對象總和」,或者「老年代可用空間」> 「歷次Minor GC升入老年代對象的平均大小」
上述兩個條件知足一個,就能夠直接進行Minor GC,不須要提早觸發Full GC了。
因此實際上,若是你們用的是JDK 1.7或者JDK 1.8,那麼JVM參數就保持以下便可,後面也都再也不加入這個參數了:
「-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M」
此時JVM內存入下圖所示。
接着就很明確了,訂單系統的系統程序在大促期間不停的運行,每秒處理300個訂單,都會佔據新生代60MB的內存空間
可是1秒事後這60MB對象都會變成垃圾,那麼新生代1.5G的內存空間大概須要25秒就會佔滿,以下圖。
25秒事後就會要進行Minor GC了,此時由於有「-XX:HandlePromotionFailure」選項,因此你能夠認爲須要進行的檢查,主要就是比較 「老年代可用空間大小」和「歷次Minor GC後進入老年代對象的平均大小」,剛開始確定這個檢查是能夠經過的。
因此Minor GC直接運行,一會兒能夠回收掉99%的新生代對象,由於除了最近一秒的訂單請求還在處理,大部分訂單早就處理完了,因此此時可能存活對象就100MB左右。
可是這裏問題來了,若是「-XX:SurvivorRatio」參數默認值爲8,那麼此時新生代裏Eden區大概佔據了1.2GB內存,每一個Survivor區是150MB的內存,以下圖。
因此Eden區1.2GB滿了就要進行Minor GC了,所以大概只須要20秒,就會把Eden區塞滿,就要進行Minor GC了。
而後GC後存活對象在100MB左右,會放入S1區域內。以下圖。
而後再次運行20秒,把Eden區佔滿,再次垃圾回收Eden和S1中的對象,存活對象可能仍是在100MB左右會進入S2區,以下圖。
此時JVM參數以下:
「-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8」
首先在進行JVM優化的時候,第一個要考慮的問題,就是你經過估算,你的新生代的Survivor區到底夠不夠?
按照上述邏輯,首先每次新生代垃圾回收在100MB左右,有可能會突破150MB,那麼豈不是常常會出現Minor GC事後的對象沒法放入Survivor中?而後豈不是頻繁會讓對象進入老年代?
還有,即便Minor GC後的對象少於150MB,可是即便是100MB的對象進入Survivor區,由於這是一批同齡對象,直接超過了Survivor區空間的50%,此時也可能會致使對象進入老年代。
(關於jvm的垃圾回收規則,若是不太清楚,請參加專欄以前的文章)
因此其實按照咱們這個模型來講,Survivor區域是明顯不足的。
這裏其實建議的是調整新生代和老年代的大小,由於這種普通業務系統,明顯大部分對象都是短生存週期的,根本不該該頻繁進入老年代,也不必給老年代維持過大的內存空間,首先得先讓對象儘可能留在新生代裏。
因此此時能夠考慮把新生代調整爲2G,老年代爲1G,那麼此時Eden爲1.6G,每一個Survivor爲200MB,以下圖。
這個時候,Survivor區域變大,就大大下降了新生代GC事後存活對象在Survivor裏放不下的問題,或者是同齡對象超過Survivor 50%的問題。
這樣就大大下降了新生代對象進入老年代的機率。
此時JVM的參數以下:
「-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8」
其實對任何系統,首先相似上文的內存使用模型預估以及合理的分配內存,儘可能讓每次Minor GC後的對象都留在Survivor裏,不要進入老年代,這是你首先要進行優化的一個地方。
你們都知道,除了Minor GC後對象沒法放入Survivor會致使一批對象進入老年代以外,還有就是有些對象連續躲過15次垃圾回收後會自動升入老年代。
其實按照上述內存運行模型,基本上20多秒觸發一次Minor GC,那麼若是按照「-XX:MaxTenuringThreshold」參數的默認值15次來講,你要是連續躲過15次GC,就是一個對象在新生代停留超過了幾分鐘了,此時他進入老年代也是應該的。
有些博客會說,應該提升這個參數,好比增長到20次,或者30次,其實那種說法根本是不對的
由於你對這個參數考慮必須結合系統的運行模型來講,若是躲過15次GC都幾分鐘了,一個對象幾分鐘都不能被回收,說明確定是系統裏相似用@Service、@Controller之類的註解標註的那種須要長期存活的核心業務邏輯組件。
那麼他就應該進入老年代,況且這種對象通常不多,一個系統累計起來最多也就幾十MB而已。
因此你說你提升「-XX:MaxTenuringThreshold」參數的值,有啥用呢?讓這些對象在新生代裏多停留幾分鐘?
所以考慮問題,必定不要人云亦云,要結合運行原理,本身推演和思考,不一樣的業務系統還都是不同的。
其實這個參數甚至你均可以下降他的值,好比下降到5次,也就是說一個對象若是躲過5次Minor GC,在新生代裏停留超過1分鐘了,儘快就讓他進入老年代,別在新生代裏佔着內存了。
總之,對於這個參數務必是結合你的系統具體運行的模型來考慮。
要記住,JVM沒有萬能的最佳參數,可是有一套通用的分析和優化的方法。
此時JVM參數以下:
「-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5」
另外有一個邏輯是說,大對象能夠直接進入老年代 ,由於大對象說明是要長期存活和使用的
好比在JVM裏可能會緩存一些數據,這個通常能夠結合本身系統中到底有沒有建立大對象來決定。
可是通常來講,給他設置個1MB足以,由於通常不多有超過1MB的大對象。若是有,多是你提早分配了一個大數組、大List之類的東西用來放緩存的數據。
此時JVM參數以下:
「-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M」
同時你們別忘了要指定垃圾回收器,新生代使用ParNew,老年代使用CMS,以下JVM參數 :
「-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC」
ParNew垃圾回收器的核心參數,其實就是配套的新生代內存大小、Eden和Survivor的比例
只要你設置合理,避免Minor GC後對象放不下Survivor進入老年代,或者是動態年齡斷定以後進入老年代,給新生代裏的Survivor充足的空間,那麼Minor GC通常就沒什麼問題。
而後根據你的系統運行模型,合理設置「-XX:MaxTenuringThreshold」,讓那些長期存活的對象,抓緊儘快進入老年代,別在新生代裏一直待着。
這樣基本上一個初步的優化好的JVM參數就結合你的業務出來了。明天咱們繼續結合案例來分析 老年代的垃圾回收和參數優化方式。
你們看完這個案例,能夠直接去看看本身生產系統的JVM參數了,看看你的新生代、老年代、Eden和Survivor的大小
而後去估算一下你的系統運行模型:
每秒佔用多少內存?
多長時間觸發一次Minor GC?
通常Minor GC後有多少存活對象?
Survivor能放的下嗎?
會不會頻繁由於Survivor放不下致使對象進入老年代?
會不會因動態年齡判斷規則進入老年代?
請你們把本身的思考發至討論區進行互動討論,在明天的文章中,我也會進行相應的點評。
END
《21天互聯網Java進階面試訓練營(分佈式篇)》詳細目錄,掃描圖片末尾的二維碼,試聽課程