1. 程式人生 > >【jvm】-從原理到實踐深入剖析jvm調優(小白也適用)

【jvm】-從原理到實踐深入剖析jvm調優(小白也適用)

1.why?

為什麼要進行Jvm調優?因為jdk預設的jvm引數並不能很好的滿足每個專案的實際效能需求,因為不同的專案本身佔用記憶體cpu資源就不一樣,加上伺服器配置的多種多樣,jvm提供的初始引數很難達到定製的效果,在專案生產環境中,除了對程式碼,sql,web容器等優化以外,對Jvm的優化也同樣重要,而且在一些情況下會出現記憶體溢位報錯,不論你怎麼修改程式碼,還是無法解決,這時候就得藉助於Jvm調優引數了,另外如果你想漲薪的話Jvm也是必須必會的一項技能,jvm的調優本身不算特別難,但想調優調的足夠"優",調優次數儘量少,那就是一項經驗活了,沒有豐富的經驗是很難搞定的,所以一般會jvm調優的程式猿也會被打上"高階"程式猿的標籤.

2.what?

什麼是jvm調優? jvm調優主要是對Jvm啟動時的引數進行調整,使java程式在執行時有較高的吞吐量,和較低的暫停時間,使cpu和記憶體的使用效率最大化,避免浪費資源.

3.How To?

如何優化Jvm呢?這是比較難的,首先得弄明白jvm的記憶體模型:

堆區:由Young區和Old區構成,Young區包含Survivor區和Eden區

其中Survivor區由兩塊相同大小的區S0,S1構成,一般新建立的物件會存在於young區中的Eden中,當需要進行gc垃圾回收時,jvm會把S0中正在執行的物件複製到S1中,然後清理S0中的物件,清理完成後釋放S0中的記憶體,伴隨著一次次GC,最終Young區中未被回收的物件會慢慢成長並轉移到Old區.這裡援引網上一段經典的例子來幫助理解物件在JVM堆記憶體中的生命週期::

我是一個普通的java物件,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“To”區,自從去了Survivor區,我就開始漂泊了,因為Survivor的兩個區總是交換名字,所以我總是搬家,搬到To Survivor居住,搬來搬去,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了年老代那邊,年老代裡,人很多,並且年齡都挺大的,我在這裡也認識了很多人。在年老代裡,我生活了20年(每次GC加一歲),然後被回收。

非堆區:由Metaspace(元空間,jdk8出現,在此之前是PermGen,也就是永久代),CCS(壓縮類空間,是Metaspace的一部分,主要用於存放堆中物件指向自己class的指標,該空間預設不存在,只有在64位系統中開啟短指標時才會出現,為了節省空間可以將64位長指標壓縮為32位短指標),CodeCache(存放一些 jar native程式碼等,預設不存在,被使用時才存在).

除此之外,Metaspace還用於存放:

然後需要弄明白Jvm的垃圾回收演算法:

目前主要採用三色標記演算法,這裡僅拋磚引玉簡單介紹一下,感興趣進一步深入瞭解的同學可以上網瞭解一下.

GC演算法會從GC Root開始,標記所有目前所有可達的物件。

Root根節點主要包含下述幾類:

從根節點開始(在這裡僅顯示了兩個根節點),所有的有引用關係的物件均被標記為存活物件(箭頭表示引用)。從根節點起,不可達物件均為垃圾物件。在標記操作完成後,系統回收所有不可達物件。

 回收後→

最後,你還需要弄明白jvm常見的幾種垃圾回收器:

1.serial collector 序列垃圾回收器,單執行緒的垃圾回收器,適用於單核cpu的渣渣伺服器,效能效率較差,不多說了...

2.parallel collector 並行垃圾回收器,多執行緒的垃圾回收器,效能效率較高,垃圾回收時暫停提供服務,也是預設開啟的垃圾回收器,jdk9除外,適合在要求吞吐量較高,且垃圾回收暫停時間較短的場景下,其實也就是大多數場景都是適用的.

3.cms collector(concurrent-mark-sweep)併發垃圾回收器,多執行緒併發的垃圾回收器,垃圾回收時不暫停提供服務,適合那些要求響應時間優先的服務,比如證券交易這種,不允許有延遲,越快越好...

4.G1 collector 從jdk7開始出現,既可以回收Young區垃圾,又可以回收Old區垃圾,在jdk9中完全替代其他gc回收器.

下述情況推薦使用G1GC回收器:

除此之外還有一些其他的GC回收器,至於如何選擇,要具體根據你伺服器的配置和JDK版本以及專案需求走,如果糾結於選擇哪種垃圾回收器的話,你可以參考oracle官方給出的建議:

有了上面這些知識儲備之後,接下來開始優化時你才能得心應手.

下面是時候表演真正的技術了:

Jvm調優一般由以下幾個步驟:

1.檢視當前所用的GC回收器,並根據自己需求選擇使用合適的GC回收器:

java -XX:+PrintCommandLineFlags -version

如下圖所示,可以開出來當前使用的GC回收器為ParallelGC.

如果你不想用ParallelGC,你可以通過啟動jar包時新增引數改變GC回收器型別:

使用SerialGC新增引數:       -XX:+UseSerialGC

使用ParallelGC新增引數:     -XX:+UseParallelGC 

使用CMSGC新增引數:          -XX:+UseConcMarkSweepGC

使用G1GC新增引數:           -XX:+UseG1GC
 

 

如圖,在啟動java應用時新增該引數:

2.開啟GC日誌,將GC日誌匯出到指定資料夾下,以便之後利用工具分析和參考.

-Xloggc:/root/outp/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps

#上面的/root/outp是我自己指定的路徑,可以靈活指定,命名也是可以隨便取的,只要字尾為.log即可

啟動後可以在root/outp資料夾下看到gc.log

用winscp或sz命令將其下載到你本地電腦.

3.藉助工具進行分析,這裡主要介紹2款分析GC日誌的工具,一款是gceasy線上分析,一款是gcviewer,關於這兩個工具我專門寫了一篇文章,感興趣的可以參考:URL

4.根據分析建議和結果進行調優:

這裡我自己新建了個EXCEL對比10次調整各種引數後的資料表現,並最終得出一個適合生產環境的jvm啟動引數:

使用的分析工具是gcviewer:

也可以用gceasy去分析,方便強大:http://gceasy.io

分析完會給出調優建議,如下圖紅框中所示:

工具分析表明,Metaspace分配的太小了,建議調大,然後你可以看看當前已用的Metaspace空間大小,圖上顯示是47.9MB,你可以適當調大兩倍(並非越大越好),比如我調整為128MB,再次啟動專案並分析日誌,發現吞吐量,GC次數都有明顯改善.

GC調優其實是沒有最佳實踐的,只有相對最佳的實踐,需要根據需求去調整,比如我們公司現在用的伺服器是4核16G記憶體的,在生產和測試環境中發現啟動12個左右的springboot專案記憶體就撐爆了,但cpu空閒率卻很高,因此我調優的目的主要就是降低記憶體佔用率,犧牲點cpu使用率無所謂...在這樣一種目的下,我可以分配給每個應用相對較少的記憶體,然後增加GC次數(會犧牲CPU使用率),這樣就能滿足我目前這種需求了. 當然在CPU和記憶體都允許的前提下,最好還是追求最高的吞吐量和最低的暫停時間為主.

下面提供一下jvm調優主要使用的引數,以jdk1.8為例,在Jdk1.8之前還可以優化PermGen(永久代),如果你覺得太麻煩,我建議你直接升級JDK1.9及以上,在JDK1.9中僅使用G1垃圾回收器,廢棄了其它垃圾回收器.

具體的可以檢視官網的建議:

jdk1.8 下,常用的優化引數和優化建議參考:

-Xms 
初始堆大小,預設為實體記憶體的1/64(<1GB),測試時也可以跟-Xmx保持一樣大,這樣可以避免每次垃圾回收完成後JVM重新分配記憶體
-Xmx 
最大堆大小,預設(MaxHeapFreeRatio引數可以調整)空餘堆記憶體大於70%時,JVM會減少堆直到 -Xms的最小限制,建議不要超過系統總可用記憶體的1/2
新生代 
-XX:NewSize 
新生代空間大小初始值
-XX:MaxNewSize 
新生代空間大小最大值
-Xmn 
新生代空間大小,此處的大小是(eden+2 survivor space) 
指定後可以不用指定上面兩條引數
-XX:YoungGenerationSizeIncrement=30
指定YoungGC的增長率,預設為20%

當然,需要調優的引數其實非常多的,官網上對每個調優引數都有介紹,感興趣深入瞭解的自行去官網學習,或者用到的時候去看下就行,對於大多數情況來說,jvm的調優其實調整-Xms,-Xmx,-Xmn -XX:MetaspaceSize=128M (根據具體情況調整) 這幾個引數的值就夠用了,甚至都不需要調整這麼多引數,jvm在垃圾回收這方面已經趨於成熟,一般公司用用預設的或者微調即可滿足需求.

最後,說一下最為簡單粗暴的方式,如果你使用的是jdk1.8及以上版本,又不想花時間精力在Jvm調優上,又想得到比較好的效能表現,不妨將你專案的垃圾回收器換成GC1,只需要指定合理的暫停時間,(太苛刻的話會增加GC次數,降低cpu效率)剩下的交給Jvm自己去做就是了,官方建議:

java -jar -XX:+UseG1GC -XX:MaxGCPauseMillis=200 xxx.jar