自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

億級流量系統(tǒng)如何玩轉(zhuǎn) JVM

云計算 虛擬化
相信你看到這里 , 應該對高并發(fā)系統(tǒng)中 對象如何吃 JVM 內(nèi)存 頻繁 遇到 gc 如何解決 已經(jīng)有所了解了 。

 [[340168]]

本文轉(zhuǎn)載自微信公眾號「Shooter茶杯」,作者Shooter。轉(zhuǎn)載本文請聯(lián)系Shooter茶杯公眾號。  

抱歉很久沒寫新文章了 , 這段時間一直在學習擴大自己的知識盲區(qū) , 工作上也挺忙的 , 拖更了好久

答應了朋友要出個 JVM 系列 , 應該會有幾篇文章 , 我會努力在保證質(zhì)量的前提下進行輸出~

So 進入今天的主題

前言

有被 JVM 相關(guān)問題刁難過嗎?

上個月朋友去面某東說被 JVM 難哭了

面試官上來就是素質(zhì)三連:

有沒有 高并發(fā)項目經(jīng)驗、頻繁 gc 怎么解決、有沒有搞過 JVM 調(diào)優(yōu)

我那個朋友公司做的是 to b 方向 , 系統(tǒng)流量不是很大 , 加上才工作 2 年直接被問懵逼

回來就問我高并發(fā)系統(tǒng)怎么玩 , 為了避免重復勞動 , 遂有此文~

一、億級流量系統(tǒng)回顧

接下來做個回顧:

OTA 平臺 4億 用戶

高峰期 百萬 訂單

高峰期 12 小時 1.8億 訪問量

每小時的流量是:1.8億 / 12 = 1250w

每分的流量是:1250w / 60 = 20.8w

每秒的流量是:20.8w / 60 = 3472

2 個集群 32 臺 8C/16G 的機器

一次核心接口查詢平均占用 5mb 內(nèi)存

每秒鐘 JVM 會有 550mb 的新生代堆內(nèi)存空間被占用

二、系統(tǒng)的 JVM 參數(shù)

基于G1垃圾收集器

這里我截取了這個服務生產(chǎn)環(huán)境的 JVM 參數(shù):

  1. -Xmx12288m 初始堆大小.
  2. -Xms12288m 最大堆大小
  3. -Xss256k 每個線程的棧內(nèi)存大小
  4. -XX:MetaspaceSize=256m 元空間初始大小
  5. -XX:MaxMetaspaceSize=1g 元空間最大大小
  6. -XX:MaxGCPauseMillis=200 每次YGC / MixedGC 的最多停頓時間 (期望最長停頓時間)
  7. -XX:+UseG1GC java8 指定使用G1垃圾回收器
  8. -XX:-OmitStackTraceInFastThrow 對異常做的一個優(yōu)化,拋出異常非???,但是看不到異常的堆棧信息(僅供參考)
  9. -XX:MinHeapFreeRatio=30 GC后java堆中空閑量占的最小比例,小于該值,則堆內(nèi)存會增加
  10. -XX:MaxHeapFreeRatio=50 GC后java堆中空閑量占的最大比例,大于該值,則堆內(nèi)存會減少
  11. -XX:CICompilerCount=4 設置的相對較大可以一定程度提升JIT編譯的速度,默認為2
  12. -XX:SoftRefLRUPolicyMSPerMB=0 任何軟引用對象在下一次 GC 都盡快釋放掉,給內(nèi)存釋放空間。
  13. -XX:+PrintGC 輸出GC日志
  14. -XX:+PrintGCDetails 輸出GC的詳細日志
  15. -XX:+PrintGCDateStamps 輸出GC的時間戳(以基準時間的形式)
  16. -XX:+UseGCLogFileRotation 開或關(guān)閉GC日志滾動記錄功能
  17. -XX:NumberOfGCLogFiles=5 設置滾動日志文件的個數(shù)
  18. -XX:GCLogFileSize=32M 設置滾動日志文件的大小,當前寫日志文件大小超過該參數(shù)值時,日志將寫入下一個文件
  19. -XX:+HeapDumpOnOutOfMemoryError JVM會在遇到OutOfMemoryError時拍攝一個堆轉(zhuǎn)儲快照,并將其保存在一個文件中

注意

-XX:SoftRefLRUPolicyMSPerMB=0 這個參數(shù)在某些情況下會造成元空間 OOM ,一般最好給個 2000 / 5000,

0 是經(jīng)過調(diào)優(yōu)確認不會引起這個問題才用。

為什么會造成 OOM 我會在以后的文章會中提到。

三、高并發(fā)下 JVM 是怎么玩的?

堆空間怎么分配內(nèi)存?

雖然給堆空間分配了 12G 的內(nèi)存,但新生代并不是一開始就把這 12G 一下就占滿了,老年代還得占一部分。

也不是一開始就將新老生代按個比例分配好空間,新生代一開始只會分配 5% 的堆內(nèi)存空間,然后慢慢的增大,

這個是可以通過 -XX:G1NewSizePercent 來設置新生代初始占比的,其實維持這個默認值就可以了

同樣老年代也是,并不是以開始就分配幾個G ;因為 G1 是基于 Region 的邏輯來分區(qū)的。

到底多久會觸發(fā)一次新生代的 YoungGC(ygc)?

有人說:新生代的 Eden 區(qū)空間不夠用了就會觸發(fā) ygc 那到底 Eden區(qū)使用多少了才是內(nèi)存不夠呢?

有一個參數(shù) -XX:G1MaxNewSizePercent 默認值:60% ,限定了新生代最多占用堆內(nèi)存 60% 的空間,

那就是是 12G * 60% = 7.2G,然后 新生代又有 Eden 和 兩個 Survivor 組成 默認比例是: 8:1:1,

7.2G * 0.8 = 5.76G , 是 Eden 區(qū)快到 5.7G 就觸發(fā) ygc 么?

并不是,G1 有個很重要的參數(shù) -XX:MaxGCPauseMillis 這個參數(shù)的默認值是 200

意味著每次 進行垃圾回收,最長的停頓時間不超過 200ms。這也是為什么 G1 號稱它造成的 STW 是停頓可控的。

做個大膽的假設: 200ms G1可以回收 300個Region 區(qū)域!

因為 G1 是在邏輯上區(qū)分 老年代和新生代的,整個堆被分成了 2048 個 Region 區(qū)域,12G 的堆內(nèi)存平均每個 Region 的大小是 6MB

但 Region 的大小必須是 2的 N次冪,所以每個 Region 的大小會是 8mb

之前算出來了這個系統(tǒng)每秒鐘往新生代輸送的對象大小是 550mb ,550mb / 8mb = 68 ,平均每秒會有 68 個 Region 被占滿,

回收 300 個 Region 需要 200ms , 300 / 68 = 4.5ms ,

大概 4.5ms 就會進行一次 ygc ,一分鐘就會進行 13 次 ygc ,每次 ygc 200ms

這樣分析就會發(fā)現(xiàn) G1 的垃圾回收其實是很動態(tài),很靈活的,它會根據(jù)你對 GC 的預期停頓時間來進行回收。

G1 哪些對象會進入老年代?

  1. 一個對象在年輕代里躲過15次垃圾回收,年齡太大了,壽終正寢,進入老年代
  2. 大對象直接送到老年代 參數(shù) XX:PretenureSizeThreshold 來控制多大的對象才算大。XX:PretenureSizeThreshold=100000000 單位為btye
  3. 動態(tài)年齡判定規(guī)則,如果一旦發(fā)現(xiàn)某次新生代 GC 過后,存活對象超過了 Survivor 50%
  4. 一次 ygc 過后存活對象太多了,導致 Survivor 區(qū)域放不下了,這批對象會進入老年代

這個接口的耗時一般在 200ms 左右,但在高并發(fā)情況下,內(nèi)存資源這么吃緊,CPU 和 線程資源都會有很高的負載,這時候就很有可能出現(xiàn)一些性能抖動的情況

相應的表現(xiàn)就是接口的響應時間延長,甚至會出現(xiàn)超時,在頻繁的 fgc 情況下:

  1. 一些對象在 Survivor區(qū) 經(jīng)過 15 次 ygc 后,就會晉升到老年代
  2. 很多接口的響應時間都延長,導致觸發(fā)動態(tài)年齡判斷規(guī)則,就會有一大批對象晉升到老年代,

看起來這么大的內(nèi)存,Survivor區(qū) 也足夠大,這個晉升規(guī)則也比較嚴格,但是高并發(fā)的場景下,上面這個流程只要反復的來幾次

老年代的對象就會越來越多

什么是 Mixed GC (混合回收)?

因為 G1 是基于 Region 的,并沒有嚴格的區(qū)分老年代新生代,

G1有一個參數(shù),XX:InitiatingHeapOccupancyPercent ,它的默認值是 45% ,意思就是說,如果 老年代 占據(jù)了堆內(nèi)存的 45% 的 Region 的時候,此時就會嘗試觸發(fā)一個新生代+老年代一起回收的混合回收。

什么時候發(fā)生 Full GC(fgc) ?

異常情況

  1. 大對象太多,對象都跑老年代去了,老年代內(nèi)存吃緊會觸發(fā) fgc ,如果fgc 內(nèi)存還是不夠使用,那再申請內(nèi)存的時候就會拋出 OOM 異常,然后再 fgc 如此往復循環(huán),系統(tǒng)并不會直接掛掉,表現(xiàn)是系統(tǒng)假死,非??D,用戶體驗極差。
  2. 元空間、直接內(nèi)存這些區(qū)域快滿了都會觸發(fā) fgc

后續(xù) 堆空間、元空間、直接內(nèi)存(堆外內(nèi)存) OOM 都會有真實的生產(chǎn)環(huán)境案例 敬請期待

正常情況

  1. fgc 都知道是一個很耗時的操作 , G1 正常的工作狀態(tài)是沒有 Full GC 概念的,老年代垃圾的收集任務全靠 Mixed GC 來處理。
  2. 不過在進行 Mixed 回收的時候,無論是年輕代還是老年代都基于復制算法進行回收,都要把各個 Region 的存活對象拷貝到別的 Region 里去,
  3. 此時萬一出現(xiàn)拷貝的過程中發(fā)現(xiàn)沒有空閑 Region 可以承載自己的存活對象了,就會觸發(fā)一次失敗。一旦失敗,立馬就會切換為停止系統(tǒng)程序,切換到 G1 之外的 Serial Old GC 來收集整個堆(包括 Young、Old、Metaspace )這才是真正的 Full GC(Full GC不在G1的控制范圍內(nèi))
  4. 進入這種狀態(tài)的G1就跟使用參數(shù) -XX:+UseSerialGC 的 Full GC 一樣(背后的核心邏輯是一樣的)。然后采用單線程進行標記、清理和壓縮整理,空閑出來一批 Region ,使用單線程的進行 gc 這個過程是極慢極慢的。
  5. 這也是 JVM 調(diào)優(yōu)的關(guān)鍵所在,務必不要讓你的系統(tǒng)觸發(fā) Full GC !

補充

-XX:MaxGCPauseMillis = 200 是一個默認值,停頓 200ms 也不算久,但一個高并發(fā)系統(tǒng)如果要求低延遲,快速響應

這個值就要再調(diào)低一點了,但是仍然不建議去把這個值改小,

很多時候設置的 200ms, 實際上也只有 20 - 80ms ,這是我觀察過不下 30 個生產(chǎn)環(huán)境的 GC 得出來的結(jié)論。

跟做性能測試的大佬也討論過這個的原因:G1 是一個 動態(tài)、靈活、自主、性能還不錯 的垃圾收集器

如果設置太小 ,可能導致每次 Mixed GC or ygc 只能回收很小一部分 Region ,最終可能無法跟上程序分配內(nèi)存的速度

從而觸發(fā) Full GC 所以很多系統(tǒng)并沒有去把這個值改成 50 或是 100

如果設置太大 ,那么可能 G1 會允許你不停的在新生代理分配新的對象,然后積累了很多對象,再一次性回收幾百個 Region

此時可能一次 GC 停頓時間就會達到幾百毫秒,但是 GC 的頻率很低。

比如說 30 分鐘才觸發(fā)一次新生代 GC,但是每次停頓 500ms ,毫無疑問, 500ms 對于一個高并發(fā)的系統(tǒng)來說實在是太久了

四、JVM 調(diào)優(yōu)該怎么做?

主要優(yōu)化在新生代

新生代gc如何優(yōu)化?

對于G1而言,我們首先應該給整個JVM的堆區(qū)域足夠的內(nèi)存,其次就是給新生代足夠的內(nèi)存,保證:

  • 不要讓對象經(jīng)歷 15 次垃圾回收從而進入老年代
  • 不要讓 Survivor 太小,從而觸發(fā)動態(tài)年齡判斷,也要保證每次 ygc 后 Survivor 都能夠放下存活的對象

之前我們算過,這個系統(tǒng)每分鐘會有 550mb 的對象會進入新生代 , 4.5s 就會來一次 ygc ,

一分鐘會有 13 次左右的 gc , 每次 gc 大概在 200ms 以內(nèi)。

PS : G1 回收只有初始標記和重新標記的階段是 stw,其他階段都是并發(fā)的,

gc 200ms , 真正 stw 的時間可能只是 幾十 ~ 一百ms

不過!每分鐘 13次 的 ygc 頻率,每次接近 200ms 左右耗時 gc 效率實在太低了

新生代優(yōu)化

因為有 記憶集 (RSet) 的存在,在 G1 回收 Region 效率不變的情況下 , 優(yōu)化的點就來了

擴大每個 Region 的大小 , 也就是擴大堆內(nèi)存的大小 , 簡而言之就是升級機器的內(nèi)存 或者是 集群進行擴容增加服務器的數(shù)量

目前這個業(yè)務系統(tǒng)只有 32 臺機器 8C 16G的機器 , 給堆空間的大小只有 12G , 對億級的流量還是不太能抗住 ,

目前階段性的分析后 , 性能瓶頸不在 CPU , 我們只需要升級內(nèi)存即可

升級到 8C 32G 給堆 24~26G 的空間 , 元空間給 1G 則機器數(shù)量不變 升級到 16C 64G 給堆 58~60G 的空間 , 元空間給 1G 還可以下幾臺機器

為什么會發(fā)生 Mixed gc ?

對于 Mixed gc 的觸發(fā),大家都知道是老年代在堆內(nèi)存里占比超過 45% 就會觸發(fā)

再回顧一下:年輕代的對象進入老年代的幾個條件:

  1. 新生代 gc 過后存活對象太多沒法放入 Survivor 區(qū)域
  2. 對象年齡太大
  3. 動態(tài)年齡判定規(guī)則

其中尤其關(guān)鍵的是新生代 gc 過后存活對象過多無法放入 Survivor 區(qū)域 , 以及動態(tài)年齡判定規(guī)則這兩個條件尤其可能 讓很多對象快速進入老年代

一旦老年代頻繁達到占用堆內(nèi)存 45% 的閾值 , 那么就會頻繁觸發(fā) Mixed gc

那我們的目標就是 :

盡量避免對象過快進入老年代 , 盡量避免頻繁觸發(fā) mixed gc , 就可以做到根本上優(yōu)化 mixed gc 了

Mixed gc 優(yōu)化思路

Mixed gc 優(yōu)化的核心還是 -XX:MaxGCPauseMills 這個參數(shù)

大家可以想一下 , 假設 -XX:MaxGCPauseMills 參數(shù)設置的值很大 , 導致系統(tǒng)運行很久

新生代可能都占用了堆內(nèi)存的 70% 了 , 此時才觸發(fā)新生代 gc

那么存活下來的對象可能就會很多 , 此時就會導致 Survivor 區(qū)域放不下那么多的對象 , 就會進入老年代中

或者是新生代 gc 過后 , 存活下來的對象過多 , 導致進入 Survivor 區(qū)域后觸發(fā)了動態(tài)年齡判定規(guī)則

達到了 Survivor 區(qū)域的 50% , 也會快速導致一些對象進入老年代

所以這里核心還是在于調(diào)節(jié) -XX:MaxGCPauseMills 這個參數(shù)的值 , 在保證新生代 gc 別太頻繁的同時 , 還得考慮每次 gc 過后的存活對象有多少

避免存活對象太多快速進入老年代,頻繁觸發(fā) Mixed gc

五、實際有效的調(diào)優(yōu)參數(shù)

1.-XX:MaxGCPauseMillis: 根據(jù)系統(tǒng)可以接受的響應時長和指標 觀察 JVM 的回收時間來進行修改 單位:ms

太小跟不上分配內(nèi)存的速度 , 太大 gc 的時間太長。

2.-XX:ParallelGCThreads: 在 stw 階段工作的 GC 線程數(shù) , 可以根據(jù)當前機器 CPU 核數(shù)來設置 , 建議核心數(shù) -1

-XX:ConcGCThreads: 在非 stw 階段工作的 GC 線程數(shù) , 會影響系統(tǒng)的吞吐量 , 畢竟是要跟用戶線程搶 CPU 資源

系統(tǒng)如果是計算密集型的建議是 CPU 核數(shù)的 1/4 ~ 1/3 , iO 密集型建議是 1/2

3.-XX:G1ReservePercent: G1為分配擔保預留的空間比例 也就是老年代留多少空間給 新生代來晉升 , 默認是 10%

如果晉升失敗會觸發(fā)單線程的 old gc 非??植?, 建議高并發(fā)系統(tǒng)加大機器內(nèi)存 提高這個參數(shù)的比例

4.-XX:MaxMetaspaceSize: 元空間最大大小 , 在高并發(fā)且機器內(nèi)存夠的情況 建議增大元空間的大小

稍微大的點系統(tǒng)都會有很多依賴的組件,這些組件底層都有可能會用到一些反射 或者 字節(jié)碼框架 , 會生成一些你看不懂類名的類

一旦第三方框架出現(xiàn)問題 , 你的系統(tǒng)很有可能也會受影響

調(diào)大元空間 , 有監(jiān)控系統(tǒng)的設置報警機制 , 給自己系統(tǒng)爭取一些緩沖時間也是有必要的

5.-XX:TraceClassLoading -XX:TraceClassUnloading

追蹤類加載和類卸載的情況 , 可以在 Tomcat 的 catalina.out 日志文件中

打印出來JVM中加載了哪些類,卸載了哪些類

6.-XX:SoftRefLRUPolicyMSPerMB: JVM 可以忍受多久 軟引用不被回收

如果是 0 則每次都會把軟引用回收掉釋放內(nèi)存

有一個情況是反射在 15 次后會動態(tài)生成一些軟引用類來提高反射的效率 , 當 ygc 的時候把這些軟應用給回收了

但是它們的類加載器或者一些奇怪名字的類還在元空間 , 那下次要用這個反射對象的時候又得重新創(chuàng)建

就造成了元空間慢慢無限增大從而觸發(fā) OOM , 建議這個參數(shù)設置 2000 - 5000 單位是: ms

7.-XX:+DisableExplicitGC: 關(guān)閉顯示的調(diào)用 System.gc() , System.gc() 是觸發(fā)類似 full gc 的操作

開啟 or 關(guān)閉 有兩個情況

關(guān)閉: 防止 team 里有剛?cè)肼毜男√觳艑懲暌粋€業(yè)務邏輯就給你來一個 System.gc() 來優(yōu)化內(nèi)存 (別問 問那個小天才就是我)

開啟: 項目里面有 Nio 相關(guān)的操作會用到直接內(nèi)存 , 在 Java 中是 DirecByteBuffer 對象來申請的

在某些不合理的情況下導致控制這塊區(qū)域的 DirecByteBuffer 會晉升到老年代

Nio 在申請堆外內(nèi)存空間不足的時候會手動調(diào)用 System.gc() 去回收 DirecByteBuffer 堆外內(nèi)存

有用到 Nio 的系統(tǒng)把這個參數(shù)關(guān)掉是有一定概率發(fā)生 Direct buffer memory 的

關(guān)閉還是打開取決于你自己的系統(tǒng) , 以及能不能做到 code review 不讓程序員自己去顯示的調(diào)用 System.gc()

8.-XX:G1MixedGCCountTarget: 設置垃圾回收混合回收階段,最多可以拆成幾次回收

G1 的垃圾回收是分為 初始標記、并發(fā)標記、最終標記、混合回收 這幾個階段的

其中混合回收是可以并發(fā)的反復回收多次 , 這樣的好處是避免單次停頓回收 stw 時間太長

停止系統(tǒng)一會兒 , 回收掉一些 Region , 再讓系統(tǒng)運行一會兒 , 然后再次停止系統(tǒng)一會兒 , 再次回收掉一些 Region

這樣可以盡可能讓系統(tǒng)不要停頓時間過長 , 可以在多次回收的間隙 , 也運行一下

在一定程度上可以防止部分接口相應超時

六、小結(jié)

相信你看到這里 , 應該對高并發(fā)系統(tǒng)中 對象如何吃 JVM 內(nèi)存 頻繁 遇到 gc 如何解決 已經(jīng)有所了解了 。

盡管有效的解決辦法仍然是加機器 , 但是加多少臺機器 , 怎么加機器 , JVM 參數(shù)要如何設置都有所了解了。

 

責任編輯:武曉燕 來源: Shooter茶杯
相關(guān)推薦

2020-01-17 11:00:23

流量系統(tǒng)架構(gòu)

2021-10-14 09:51:17

架構(gòu)運維技術(shù)

2021-03-02 07:54:18

流量網(wǎng)關(guān)設計

2016-11-23 12:55:09

京東活動系統(tǒng)流量

2018-10-23 09:22:06

2021-12-03 10:47:28

WOT技術(shù)峰會技術(shù)

2025-03-04 08:52:21

2021-10-12 10:00:25

架構(gòu)運維技術(shù)

2024-05-27 08:32:45

2020-10-27 07:29:43

架構(gòu)系統(tǒng)流量

2020-12-09 08:12:30

系統(tǒng)架構(gòu)

2021-06-28 10:09:59

架構(gòu)網(wǎng)關(guān)技術(shù)

2024-11-20 19:56:36

2024-10-28 08:01:11

2017-03-24 17:17:35

限流節(jié)流系統(tǒng)

2021-02-24 16:17:18

架構(gòu)運維技術(shù)

2019-09-25 09:50:29

高可用微服務系統(tǒng)

2020-03-18 16:15:21

億級搜索數(shù)據(jù)

2025-02-26 00:28:01

2024-08-16 14:01:00

點贊
收藏

51CTO技術(shù)棧公眾號