JVM 調(diào)優(yōu)分類
調(diào)優(yōu)是一個很大的概念吊档,簡單說就是把系統(tǒng)進行優(yōu)化,但是站在一個系統(tǒng)的角度,能夠干的事情太多了,我們一般把 JVM 調(diào)優(yōu)分成以下三類:
? JVM 預調(diào)優(yōu)
? 優(yōu)化 JVM 運行環(huán)境(慢遮晚、卡頓等)
? 解決 JVM 中的問題(OOM 等)
調(diào)優(yōu)中,現(xiàn)象最明顯的是 OOM拦止,因為有異常拋出县遣,當然它也只是作為調(diào)優(yōu)的一部分。預調(diào)優(yōu)和優(yōu)化運行環(huán)境估計很多人做的就是服務(wù)器重啟而已汹族。
JVM 預調(diào)優(yōu)
業(yè)務(wù)場景設(shè)定
調(diào)優(yōu)是要分場景的萧求,所以一定要明顯你調(diào)優(yōu)項目的場景設(shè)定,像現(xiàn)在大家都是微服務(wù)架構(gòu)了鞠抑,服務(wù)拆分出來以后更加適合做場景設(shè)定饭聚。比如這個服務(wù)就注重吞吐量忌警,這個服務(wù)注重用戶的體驗(用戶的響應(yīng)時間)等等搁拙。?
無監(jiān)控不優(yōu)化
這里的監(jiān)控指的是壓力測試,能夠看到結(jié)果法绵,有數(shù)據(jù)體現(xiàn)的箕速,不要用感覺去優(yōu)化,所有的東西一定要有量化的指標朋譬,比如吞吐量盐茎,響應(yīng)時間,服務(wù)器資源徙赢,網(wǎng)絡(luò)資源等等字柠。總之一句話狡赐,無監(jiān)控不優(yōu)化窑业。
處理步驟
計算內(nèi)存需求
計算內(nèi)存需求,內(nèi)存不是越大越好枕屉,對于一般系統(tǒng)來說常柄,內(nèi)存的需求是彈性的,內(nèi)存小,回收速度快也能承受西潘。所以內(nèi)存大小沒有固定的規(guī)范卷玉。虛擬機棧的大小在高并發(fā)情況下可以變小。
元空間(方法區(qū))保險起見還是設(shè)定一個最大的值(默認情況下元空間是沒有大小限制的)喷市,一般限定幾百 M 就夠用了相种,為什么說還限定元空間。
舉例子:一臺 8G 的內(nèi)存的服務(wù)器品姓,如果運行時還有其他的程序加上虛擬機棧加上元空間蚂子,占用超過 6 個 G 的話,那么我們設(shè)定堆是彈性的(max=4G)缭黔,那么其實堆空間拓展也超不過 2G食茎,所以這個時候限制元空間還是有必要的。
選定 CPU
對于系統(tǒng)來說馏谨, CPU 的性能是越高越好别渔,這個按照你的預算來定(CPU 的成本很高)。
尤其是現(xiàn)在服務(wù)器做了虛擬機化之后惧互,虛擬機的性能指標不能單看虛擬化后的參數(shù)指標哎媚,更應(yīng)該看實際物理機的情況。
選擇合適的垃圾回收器
對于吞吐量優(yōu)先的場景喊儡,就只有一種選擇拨与,就是使用 PS 組合(Parallel Scavenge+Parallel Old )。
對于響應(yīng)時間優(yōu)先的場景艾猜,在 JDK1.8 的話優(yōu)先 G1买喧,其次是 CMS 垃圾回收器。
設(shè)定新生代大小匆赃、分代年齡
吞吐量優(yōu)先的應(yīng)用:一般吞吐量優(yōu)先的應(yīng)用都有一個很大的新生代和一個較小的老年代.原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而老年代盡存放長期存活對象淤毛。
設(shè)定日志參數(shù)
-XX:+PrintGC 輸出 GC 日志
-XX:+PrintGCDetails 輸出 GC 的詳細日志
-XX:+PrintGCTimeStamps 輸出 GC 的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出 GC 的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行 GC 的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的輸出路徑
注意:一般記錄日志的是算柳,如果只有一個日志文件肯定不行低淡,有時候一個高并發(fā)項目一天產(chǎn)生的日志文件就上 T,其實記錄日志這個事情瞬项,應(yīng)該是運維干的事情蔗蹋。日志文件幫助我們分析問題。
優(yōu)化 JVM 運行環(huán)境(慢囱淋、卡頓等)
一般造成 JVM 卡或者慢的原因無非兩個部分猪杭,一個是 CPU 占用過高,一個是內(nèi)存占用過高绎橘。所以這個時候需要我們進行問題的排查胁孙,進行具體的故障分析唠倦。
解決 JVM 中的問題(OOM 等)
前面章節(jié)講過,見:內(nèi)存溢出(重點)
億級流量電商系統(tǒng) JVM 調(diào)優(yōu)
億級流量系統(tǒng)
億級流量系統(tǒng)涮较,其實就是每天點擊量在億級的系統(tǒng)稠鼻,根據(jù)淘寶的一個官方的數(shù)據(jù)分析。
每個用戶一次瀏覽點擊 20~40 次之間狂票,推測出每日活躍用戶(日活用戶)在 500 萬左右候齿。
同時結(jié)合淘寶的一個點擊數(shù)據(jù),可以發(fā)現(xiàn)闺属,能夠付費的也就是橙色的部分(cart)的用戶慌盯,比例只有 10%左右。
90%的用戶僅僅是瀏覽掂器,那么我們可以通過圖片緩存亚皂、Redis 緩存等技術(shù),我們可以把 90%的用戶解決掉国瓮。
10%的付費用戶灭必,大概算出來是每日成交 50 萬單左右。
GC 預估
如果是普通業(yè)務(wù)乃摹,一般處理時間比較平緩禁漓,大概在 3,4 個小時處理,算出來每秒只有幾十單孵睬,這個一般的應(yīng)用可以處理過來(不需要 JVM 預估調(diào)優(yōu))另外電商系統(tǒng)中有大促場景(秒殺播歼、限時搶購等),一般這種業(yè)務(wù)是幾種在幾分鐘掰读。我們算出來大約每秒 2000 單左右的數(shù)據(jù)秘狞,承受大促場景的使用 4 臺服務(wù)器(使用負載均衡)。每臺訂單服務(wù)器也就是大概 500 單/秒磷支。
我們測試發(fā)現(xiàn)谒撼,每個訂單處理過程中會占據(jù) 0.2MB 大小的空間(什么訂單信息食寡、優(yōu)惠券雾狈、支付信息等等),那么一臺服務(wù)器每秒產(chǎn)生 100M 的內(nèi)存空間抵皱,這些對象基本上都是朝生夕死善榛,也就是 1 秒后都會變成垃圾對象。
加入我們設(shè)置堆的空間最大值為 3 個 G呻畸,我們按照默認情況下的設(shè)置移盆,新生代 1/3 的堆空間,老年代 2/3 的堆空間伤为。Eden:S0:S1=8:1:1咒循,我們推測出据途,old 區(qū)=2G,Eden 區(qū)=800M,S0=S1=100M。
根據(jù)對象的分配原則(對象優(yōu)先在 Eden 區(qū)進行分配)叙甸,由此可得颖医,8 秒左右 Eden 區(qū)空間滿了。
每 8 秒觸發(fā)一個 MinorGC(新生代垃圾回收)裆蒸,這次 MinorGC 時熔萧,JVM 要 STW,但是這個時候有 100M 的對象是不能回收的(線程暫停僚祷,對象需要 1 秒后都會變成垃圾對象)佛致,那么就會有 100M 的對象在本次不能被回收(只有下次才能被回收掉)。
所以經(jīng)過本次垃圾回收后辙谜。本次存活的 100M 對象會進入 S0 區(qū)俺榆,但是由于另外一個 JVM 對象分配原則(如果在 Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代装哆,無須等到 MaxTenuringThreshold 中要求的年齡)肋演。
所以這樣的對象本質(zhì)上不會進去 Survivor 區(qū),而是進入老年代烂琴。
所以我們推算爹殊,大概每個 8 秒會有 100M 的對象進入老年代。大概 20*8=160 秒奸绷,也就是 2 分 40 秒左右 old 區(qū)就會滿掉梗夸,就會觸發(fā)一次 FullGC,一般來說,這次 FullGC 是可以避免的号醉,同時由于 FullGC 不單單回收老年代+新生代反症,還要回收元空間,這些 FullGC 的時間可能會比較長(老年代回收的朝生夕死的對象畔派,使用標記清除/標記整理算法決定了效率并不高,同時元空間也要回收一次铅碍,進一步加大 GC 時間)。
所以問題的根本就是做到如何避免沒有必要的 FullGC线椰。
GC 預估調(diào)優(yōu)
我們在項目中加入 VM 參數(shù):
-Xms3072M -Xmx3072M -Xmn2048M -XX:SurvivorRatio=7
-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M
-XX:MaxTenuringThreshold=2
-XX:ParallelGCThreads=8
-XX:+UseConcMarkSweepGC
1胞谈、首先看一下堆空間:old 區(qū)=1G,Eden 區(qū)=1.4G,S0=S1=300M
2憨愉、那么第一點烦绳,Eden 區(qū)大概需要 14 秒才能填滿,填滿之后配紫,100M 的存活對象會進入 S0 區(qū)(由于這個區(qū)域變大径密,不會觸發(fā)動態(tài)年齡判斷)
3、再過 14 秒躺孝,Eden 區(qū)享扔,填滿之后底桂,還是剩余 100M 的對象要進入 S1 區(qū)。但是由于原來的 100M 已經(jīng)是垃圾了(過了 14 秒了)惧眠,所以戚啥,S1 也只會有 Eden 區(qū)過來的 100M 對象,S0 的 100M 已經(jīng)別回收锉试,也不會觸發(fā)動態(tài)年齡判斷猫十。
4、反反復復呆盖,這樣就沒有對象會進入 old 區(qū)拖云,就不會觸發(fā) FullGC,同時我們的 MinorGC 的頻次也由之前的 8 秒變?yōu)?14 秒,雖然空間加大应又,但是換來的還是 GC 的總時間會減少宙项。
5、-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M 棧一般情況下很少用到 1M株扛。所以為了線程占用內(nèi)存更少尤筐,我們可以減少到 256K元空間一般啟動后就不會有太多的變化,我們可以設(shè)定為 128M洞就,節(jié)約內(nèi)存空間盆繁。
6、-XX:MaxTenuringThreshold=2 這個是分代年齡(年齡為 2 就可以進入老年代)旬蟋,因為我們基本上都使用的是 Spring 架構(gòu)油昂,Spring 中很多的 bean 是長期要存活的,沒有必要在 Survivor 區(qū)過渡太久倾贰,所以可以設(shè)定為 2冕碟,讓大部分的 Spring 的內(nèi)部的一些對象進入老年代。
7匆浙、-XX:ParallelGCThreads=8 線程數(shù)可以根據(jù)你的服務(wù)器資源情況來設(shè)定(要速度快的話可以設(shè)置大點安寺,根據(jù) CPU 的情況來定,一般設(shè)置成 CPU 的整數(shù)倍)首尼。
8挑庶、-XX:+UseConcMarkSweepGC 因為這個業(yè)務(wù)響應(yīng)時間優(yōu)先的,所以還是可以使用 CMS 垃圾回收器或者 G1 垃圾回收器饰恕。
JVM 調(diào)優(yōu)實戰(zhàn)
項目介紹
代碼介紹
在 Linux 服務(wù)跑起來: java -cp ref-jvm3.jar -XX:+PrintGC -Xms200M -Xmx200M ex13.FullGCProblem 挠羔。
CPU 占用過高排查實戰(zhàn)
1、先通過 top 命令找到消耗 cpu 很高的進程 id 假設(shè)是 2732
top 命令是我們在 Linux 下最常用的命令之一埋嵌,它可以實時顯示正在執(zhí)行進程的 CPU 使用率、內(nèi)存使用率以及系統(tǒng)負載等信息俱恶。其中上半部分顯示的是系統(tǒng)的統(tǒng)計信息雹嗦,下半部分顯示的是進程的使用率統(tǒng)計信息范舀。
2、執(zhí)行 top -p 2732 單獨監(jiān)控該進程
3了罪、在第 2 步的監(jiān)控界面輸入 H锭环,獲取當前進程下的所有線程信息
4、找到消耗 cpu 特別高的線程編號泊藕,假設(shè)是 2734(要等待一陣)
5辅辩、執(zhí)行 jstack 123456 對當前的進程做 dump,輸出所有的線程信息
6娃圆、 將第 4 步得到的線程編號 11354 轉(zhuǎn)成 16 進制是 0x7b
7玫锋、 根據(jù)第 6 步得到的 0x7b 在第 5 步的線程信息里面去找對應(yīng)線程內(nèi)容
8、 解讀線程信息讼呢,定位具體代碼位置
發(fā)現(xiàn)找是 VM 的線程占用過高撩鹿,我們發(fā)現(xiàn)我開啟的參數(shù)中,有垃圾回收的日志顯示悦屏,所以我們要換一個思路节沦,可能是我們的業(yè)務(wù)線程沒問題,而是垃圾回收的導致的础爬。
jstat(代碼中有打印 GC 參數(shù)甫贯,生產(chǎn)上可以使用這個 jstat –gc 來統(tǒng)計,達到類似的效果):
是用于監(jiān)視虛擬機各種運行狀態(tài)信息的命令行工具看蚜。它可以顯示本地或者遠程虛擬機進程中的類裝載获搏、內(nèi)存、垃圾收集失乾、JIT 編譯等運行數(shù)據(jù)常熙,在沒有 GUI圖形界面,只提供了純文本控制臺環(huán)境的服務(wù)器上碱茁,它將是運行期定位虛擬機性能問題的首選工具裸卫。
假設(shè)需要每 250 毫秒查詢一次進程 13616 垃圾收集狀況,一共查詢 10 次纽竣,那命令應(yīng)當是:jstat-gc 13616 250010墓贿。
使用這個大量的 FullGC 了,并且還拋出了 OUT Of Memory蜓氨。
S0C:第一個幸存區(qū)的大小
S1C:第二個幸存區(qū)的大小
S0U:第一個幸存區(qū)的使用大小
S1U:第二個幸存區(qū)的使用大小
EC:伊甸園區(qū)的大小
EU:伊甸園區(qū)的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法區(qū)大小
MU:方法區(qū)使用大小
CCSC:壓縮類空間大小
CCSU:壓縮類空間使用大小
YGC:年輕代垃圾回收次數(shù)
YGCT:年輕代垃圾回收消耗時間
FGC:老年代垃圾回收次數(shù)
FGCT:老年代垃圾回收消耗時間
GCT:垃圾回收消耗總時間
怎么辦聋袋?OOM 了. 我們可以看到,這個里面 CPU 占用過高是什么導致的穴吹?
是業(yè)務(wù)線程嗎幽勒?不是的,這個是 GC 線程占用過高導致的港令。JVM 在瘋狂的進行垃圾回收啥容,再回顧下之前的知識锈颗,JVM 中默認的垃圾回收器是多線程的(回顧下之前的知識),所以多線程在瘋狂回收咪惠,導致 CPU 占用過高击吱。
內(nèi)存占用過高內(nèi)存占用過高思路
用于生成堆轉(zhuǎn)儲快照(一般稱為 heapdump 或 dump 文件)。jmap 的作用并不僅僅是為了獲取 dump 文件遥昧,它還可以查詢 finalize 執(zhí)行隊列覆醇、Java 堆和永久代的詳細信息,如空間使用率炭臭、當前用的是哪種收集器等永脓。和 jinfo 命令一樣,jmap 有不少功能在Windows 平臺下都是受限的徽缚,除了生成 dump 文件的-dump 選項和用于查看每個類的實例憨奸;
空間占用統(tǒng)計的-histo 選項在所有操作系統(tǒng)都提供之外。
把 JVM 中的對象全部打印出來凿试, 但是這樣太多了排宰,那么我們選擇前 20 的對象展示出來,jmap –histo 1196 | head -20
定位問題的關(guān)鍵那婉,就是這條命令板甘。很多個 88 萬個對象。
問題總結(jié)(找到問題)
一般來說详炬,前面這幾行盐类,就可以看出,到底是哪些對象占用了內(nèi)存呛谜。
這些對象回收不掉嗎在跳?是的,這些對象回收不掉隐岛,這些對象回收不掉猫妙,導致了 FullGC,里面還有 OutOfMemory。
任務(wù)數(shù)多于線程數(shù)聚凹,那么任務(wù)會進入阻塞隊列割坠,就是一個隊列,你進去妒牙,排隊彼哼,有機會了,你就上來跑湘今。
但是敢朱,因為代碼中任務(wù)數(shù)一直多于線程數(shù),所以每 0.1S,就會有 50 個任務(wù)進入阻塞對象蔫饰,50 個任務(wù)底下有對象琅豆,至少對象送進去了愉豺,但是沒執(zhí)行篓吁。所以導致對象一直都在,同時還回收不了蚪拦。
為什么回收不了杖剪?因為Executor 是一個 GCroots。
所以堆中驰贷,就會有對象 80 萬個盛嘿,阻塞隊列中 80 萬個任務(wù),futureTask括袒。并且這些對象還回收不了次兆。
總結(jié)
在 JVM 出現(xiàn)性能問題的時候。(表現(xiàn)上是 CPU100%锹锰,內(nèi)存一直占用)
1芥炭、 如果 CPU 的 100%,要從兩個角度出發(fā)恃慧,一個有可能是業(yè)務(wù)線程瘋狂運行园蝠,比如說想很多死循環(huán)。還有一種可能性痢士,就是 GC 線程在瘋狂的回收彪薛,因為 JVM 中垃圾回收器主流也是多線程的,所以很容易導致 CPU 的 100%
2怠蹂、 在遇到內(nèi)存溢出的問題的時候善延,一般情況下我們要查看系統(tǒng)中哪些對象占用得比較多,我的是一個很簡單的代碼城侧,在實際的業(yè)務(wù)代碼中易遣,找到對應(yīng)的對象,分析對應(yīng)的類赞庶,找到為什么這些對象不能回收的原因训挡,就是我們前面講過的可達性分析算法,JVM 的內(nèi)存區(qū)域歧强,還有垃圾回收器的基礎(chǔ)澜薄。
當然,如果遇到更加復雜的情況摊册,你要掌握的理論基礎(chǔ)遠遠不止這些(JVM 很多理論都是排查問題的關(guān)鍵)肤京。
常見問題分析
超大對象
代碼中創(chuàng)建了很多大對象 , 且一直因為被引用不能被回收,這些大對象會進入老年代,導致內(nèi)存一直被占用忘分,很容易引發(fā) GC 甚至是 OOM棋枕。
超過預期訪問量
通常是上游系統(tǒng)請求流量飆升,常見于各類促銷/秒殺活動妒峦,可以結(jié)合業(yè)務(wù)流量指標排查是否有尖狀峰值重斑。
比如如果一個系統(tǒng)高峰期的內(nèi)存需求需要 2 個 G 的堆空間,但是堆空間設(shè)置比較小肯骇,導致內(nèi)存不夠窥浪,導致 JVM 發(fā)起頻繁的 GC 甚至 OOM。?
過多使用 Finalizer
過度使用終結(jié)器(Finalizer)笛丙,對象沒有立即被 GC漾脂,F(xiàn)inalizer 線程會和我們的主線程進行競爭,不過由于它的優(yōu)先級較低胚鸯,獲取到的 CPU 時間較少骨稿,因此它永遠也趕不上主線程的步伐,程序消耗了所有的可用資源姜钳,最后拋出 OutOfMemoryError 異常坦冠。?
內(nèi)存泄漏
大量對象引用沒有釋放,JVM 無法對其自動回收傲须。?
長生命周期的對象持有短生命周期對象的引用
例如將 ArrayList 設(shè)置為靜態(tài)變量蓝牲,則容器中的對象在程序結(jié)束之前將不能被釋放,從而造成內(nèi)存泄漏
連接未關(guān)閉
如數(shù)據(jù)庫連接泰讽、網(wǎng)絡(luò)連接和 IO 連接等例衍,只有連接被關(guān)閉后,垃圾回收器才會回收對應(yīng)的對象已卸。
變量作用域不合理
例如佛玄,1.一個變量的定義的作用范圍大于其使用范圍,2.如果沒有及時地把對象設(shè)置為 null累澡。
內(nèi)部類持有外部類
Java 的非靜態(tài)內(nèi)部類的這種創(chuàng)建方式梦抢,會隱式地持有外部類的引用,而且默認情況下這個引用是強引用愧哟,因此奥吩,如果內(nèi)部類的生命周期長于外部類的生命周期,程序很容易就產(chǎn)生內(nèi)存泄漏蕊梧;
如果內(nèi)部類的生命周期長于外部類的生命周期霞赫,程序很容易就產(chǎn)生內(nèi)存泄漏(垃圾回收器會回收掉外部類的實例,但由于內(nèi)部類持有外部類的引用肥矢,導致垃圾回收器不能正常工作)端衰;
解決方法:你可以在內(nèi)部類的內(nèi)部顯示持有一個外部類的軟引用(或弱引用),并通過構(gòu)造方法的方式傳遞進來,在內(nèi)部類的使用過程中旅东,先判斷一下外部類是否被回收灭抑。
Hash 值改變
在集合中,如果修改了對象中的那些參與計算哈希值的字段抵代,會導致無法從集合中單獨刪除當前對象腾节,造成內(nèi)存泄露(有代碼案例 Node 類)。
內(nèi)存泄漏經(jīng)典案例
代碼問題
代碼問題和內(nèi)存泄漏很大的關(guān)系主守,如果觀察一個系統(tǒng)禀倔,每次進行 FullGC 發(fā)現(xiàn)堆空間回收的比例比較小榄融,尤其是老年代参淫,同時對象越來越多,這個時候可以判斷是有可能發(fā)生內(nèi)存泄漏愧杯,內(nèi)存溢出不一定是代碼問題涎才。
內(nèi)存泄漏
程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間力九。?
內(nèi)存泄漏和內(nèi)存溢出辨析
內(nèi)存溢出:實實在在的內(nèi)存空間不足導致耍铜;
內(nèi)存泄漏:該釋放的對象沒有釋放,常見于使用容器保存元素的情況下跌前。
如何避免:內(nèi)存溢出:檢查代碼以及設(shè)置足夠的空間 棕兼;? ? ? 內(nèi)存泄漏:一定是代碼有問題
往往很多情況下,內(nèi)存溢出往往是內(nèi)存泄漏造成的抵乓。?
我們一般優(yōu)化的思路有一個重要的順序:
1. 程序優(yōu)化伴挚,效果通常非常大;?
2. 擴容灾炭,如果金錢的成本比較小茎芋,不要和自己過不去;
3. 參數(shù)調(diào)優(yōu)蜈出,在成本田弥、吞吐量、延遲之間找一個平衡點铡原。