1 此文目的
本文不準備從盤古開天地開始講述JVM的種種,相關的文章網上太多了,大多也無非轉來轉去,連圖都差不多。筆者只整理個提綱挈領的學習路線指南,并對自己學習過程中遇到的坑和容易混淆和忽視的地方作個總結。
2 JVM內存模型
2.1 內存模型
內存區域劃分有多個維度,相同區域在不同維度的名稱并不一樣。如下圖所示
2.2 方法區和永久代
這兩個概念,很多時候都被當做是同一個概念。實際上,“方法區”是java虛擬機規范中對存放類信息,字段,方法,常量,靜態變量,接口和常量池的內存區域的定義,而“永久代”則是HotSpot VM在1.8版本以前對于方法區的具體實現。由于java虛擬機規范并沒有對方法區的具體實現作限制,所以HotSpot VM和JRocket VM對于方法區的實現都是不一樣的,JRocket中就沒有永久代的概念。而在1.8及1.8以后的版本中,HotSpot VM用"元空間"--metaspace來代替永久代,實現方法區。 這個變化帶來的就是VM參數的變化,所有的PermGen都被替換成了MetaSpace。并且metaSpace不再使用堆內存,而是使用系統內存。但是該發生的OOM一樣會發生。原因也基本都是加載到內存中的 class 數量太多或者體積太大。
3.GC
3.1 GC算法
GC算法和GC收集器也是兩個維度的概念。 GC算法包括清除算法(也叫標記清除算法),復制算法,標記-整理算法。 不同垃圾收集器針對不同的內存區域,采用不同的GC算法。 具體介紹,網上相關資料很多,可以參考這篇文章:blog.csdn.net/xiaoping091…
3.2 垃圾收集器
垃圾收集器經歷了從串行收集器到并行收集器,再到并發收集器的進化過程。這三者的區別如下圖所示
不同版本默認使用的垃圾收集器以及支持開發者定制的垃圾收集器都是不一樣的 jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.9 默認垃圾收集器G1 與此同時,通過設置JVM參數也可以自己選擇垃圾收集器。如要開啟G1垃圾回收器,可以用-XX:+UseG1GC,支持G1垃圾回收器的JDK最低版本為JDK 7u4。在用戶自己選擇垃圾收集器的時候,要注意JDK版本的問題。 筆者用表格的形式列出了新生代和老年代的GC收集器的常見搭配方案:
3.3 Full GC觸發條件
頻繁FullGC導致的stop the world的現象,會大大影響系統的穩定性。盡管一代又一代的垃圾收集器的優化,使得stop the world的時間越來越短,但是在大型應用中,還是避之不及。 出發FullGC的情況有以下幾種:
- System.gc()方法的調用
- 老年代不足
- 方法區不足
- concurrent mode failure concurrent mode failure是在執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足造成的(有時候“空間 不足”是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC)。
- promotion failed minor gc時年輕代的存活區空間不足而晉升老年代,老年代又空間不足而觸發full gc
- 統計得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間 當準備要觸發一次young GC時,如果發現統計數據說之前young GC的平均晉升大小比目前old gen剩余的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC)。
3.3.1 OOM的類型
通常情況下,JVM的GC機制能保證應用的正常運行,導致系統頻繁FullGC的原因百分之九十都是內存溢出(OOM)。OOM分為以下幾類:
- Java.lang.OutOfMemeoryError:Java heap space 堆空間的內存溢出,可能的原因是某個可達性分析認為不能被回收的對象隨著時間推移變得越來越大,例如某個static類型的map對象,被不停地塞入鍵值對,也可能是大循環或者死循環不斷創建對象,而對象分配內存的速度超過了GC清理內存的速度。
- Java.lang.OutOfMemeoryError:GC overhead limit exceeded 這種OOM異常是Hotspot VM 1.6定義的一個策略,通過統計GC時間來預測是否要OOM了,提前拋出異常,防止OOM發生。Sun 官方對此的定義是:“并行/并發回收器在GC回收時間過長時會拋出OutOfMemroyError。過長的定義是,超過98%的時間用來做GC并且回收了不到2%的堆內存。用來避免內存過小造成應用不能正常工作。” 那么為什么會出現這種GC效率低下的現象呢?通常是因為老年代內存占有過多導致的頻繁GC,這種情況下,可以增加-verbose:gc -XX:+PrintGCDetails來分析具體原因,也可以加-XX:+HeapDumpOnOutOfMemoryError,這樣OOM時會自動做Heap Dump,第二種方法適用于所有OOM異常的排查。
- Java.lang.OutOfMemoryError: PermGen space(JAVA8引入了Metaspace區域)方法區內存溢出,通常是因為加載的類過多,可以先排除程序問題導致的重復類加載,或者加大方法區內存的分配
- Java.lang.OutOfMemoryError: unable to create new native thread 產生這種異常的原因是由于系統在不停地創建大量的線程,且不進行釋放。
4. JVM調優
4.1 調優參數
正確設置JVM參數,可以盡可能多地避免系統資源浪費,盡可能詳細地掌握系統運行情況,并且對可能出現的問題防患于未然。
Xms:堆初始空間
Xmx:堆最大空間
Xmn:年輕代大小
XX:MaxNewSize 新生代最大空間 建議設置為整個堆的1/3到1/4
XX:NewSize
XX:MaxTenuringThreshold survivor中到老年代中的年齡閾值
Xss:每個線程的棧大小
java -XX:+PrintCommandLineFlags -version 得到JDK建議的內存分配大小
tomcat設置catalina.sh:
export JAVA_OPTS="-server –Xms1024m -Xmx1024m -XX:+UseParallelOldGC -verbose:gc -Xloggc:../logs/gc.log
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps"
-XX:+PrintCommandLineFlagsjvm參數可查看默認設置收集器類型
-XX:+PrintGCDetails亦可通過打印的GC日志的新生代、老年代名稱判斷
4.2 JVM監控
-
1.本機環境下,推薦一款idea上的插件VisualVM Launcher,實際就是聯動了JDK開發包中自帶的jvisualvm.exe監控軟件。也可以設置遠程監控。具體使用方法,可以參考這篇文章https://blog.csdn.net/wngpenghao/article/details/82884874IDEA Java性能分析插件VisualVM Launcher 配置(JAVA VisualVM 與Jconsole配置相同)
-
2.Linux的相關命令: jstat命令可以對jvm從各維度進行統計,詳細使用參考jstat命令查看jvm的GC情況
-
3.VM參數設置時,指定打印出gc日志 -Xloggc:../logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 詳細的參數設置以及gc日志該如何閱讀,可以參考java之GC日志該怎么看
4.3 JVM異常排查
- 保存dump 當使用監控軟件或者命令查看發現JVM異常時,應第一時間保存下dump現場。 命令是jmap -dump:format=b,file=文件名[pid] pid是服務進程
如果是使用jvisualvm就更方便了,直接點擊如圖所示的按鈕即可:
4.4 實戰例子
由于實際工作中,能接觸到JVM機會的機會并不多,所以筆者整理了一些經典實例
Metaspace溢出排查過程
分享一次 Java 內存泄漏的排查
一次生產的 JVM 優化案例
JVM成長之路,記錄一次內存溢出導致頻繁FGC的問題排查及解決
非常詳細的jvm調優實例,性能瓶頸定位