這是面試專題系列第五篇JVM篇。這一篇可能稍微比較長,沒有耐心的同窗建議直接拖到最後。java
說說JVM的內存佈局?
Java虛擬機主要包含幾個區域:web
堆:堆Java虛擬機中最大的一塊內存,是線程共享的內存區域,基本上全部的對象實例數組都是在堆上分配空間。堆區細分爲Yound區年輕代和Old區老年代,其中年輕代又分爲Eden、S0、S1 3個部分,他們默認的比例是8:1:1的大小。面試
棧:棧是線程私有的內存區域,每一個方法執行的時候都會在棧建立一個棧幀,方法的調用過程就對應着棧的入棧和出棧的過程。每一個棧幀的結構又包含局部變量表、操做數棧、動態鏈接、方法返回地址。算法
局部變量表用於存儲方法參數和局部變量。當第一個方法被調用的時候,他的參數會被傳遞至從0開始的連續的局部變量表中。數組
操做數棧用於一些字節碼指令從局部變量表中傳遞至操做數棧,也用來準備方法調用的參數以及接收方法返回結果。安全
動態鏈接用於將符號引用表示的方法轉換爲實際方法的直接引用。微信
元數據:在Java1.7以前,包含方法區的概念,常量池就存在於方法區(永久代)中,而方法區自己是一個邏輯上的概念,在1.7以後則是把常量池移到了堆內,1.8以後移出了永久代的概念(方法區的概念仍然保留),實現方式則是如今的元數據。它包含類的元信息和運行時常量池。網絡
Class文件就是類和接口的定義信息。多線程
運行時常量池就是類和接口的常量池運行時的表現形式。併發
本地方法棧:主要用於執行本地native方法的區域
程序計數器:也是線程私有的區域,用於記錄當前線程下虛擬機正在執行的字節碼的指令地址
知道new一個對象的過程嗎?
當虛擬機碰見new關鍵字時候,實現判斷當前類是否已經加載,若是類沒有加載,首先執行類的加載機制,加載完成後再爲對象分配空間、初始化等。
-
首先校驗當前類是否被加載,若是沒有加載,執行類加載機制 -
加載:就是從字節碼加載成二進制流的過程 -
驗證:固然加載完成以後,固然須要校驗Class文件是否符合虛擬機規範,跟咱們接口請求同樣,第一件事情固然是先作個參數校驗了 -
準備:爲靜態變量、常量賦默認值 -
解析:把常量池中符號引用(以符號描述引用的目標)替換爲直接引用(指向目標的指針或者句柄等)的過程 -
初始化:執行static代碼塊(cinit)進行初始化,若是存在父類,先對父類進行初始化
Ps:靜態代碼塊是絕對線程安全的,只能隱式被java虛擬機在類加載過程當中初始化調用!(此處該有問題static代碼塊線程安全嗎?)
當類加載完成以後,緊接着就是對象分配內存空間和初始化的過程
-
首先爲對象分配合適大小的內存空間 -
接着爲實例變量賦默認值 -
設置對象的頭信息,對象hash碼、GC分代年齡、元數據信息等 -
執行構造函數(init)初始化
知道雙親委派模型嗎?
類加載器自頂向下分爲:
-
Bootstrap ClassLoader啓動類加載器:默認會去加載JAVA_HOME/lib目錄下的jar -
Extention ClassLoader擴展類加載器:默認去加載JAVA_HOME/lib/ext目錄下的jar -
Application ClassLoader應用程序類加載器:好比咱們的web應用,會加載web程序中ClassPath下的類 -
User ClassLoader用戶自定義類加載器:由用戶本身定義
當咱們在加載類的時候,首先都會向上詢問本身的父加載器是否已經加載,若是沒有則依次向上詢問,若是沒有加載,則從上到下依次嘗試是否能加載當前類,直到加載成功。
說說有哪些垃圾回收算法?
標記-清除
統一標記出須要回收的對象,標記完成以後統一回收全部被標記的對象,而因爲標記的過程須要遍歷全部的GC ROOT,清除的過程也要遍歷堆中全部的對象,因此標記-清除算法的效率低下,同時也帶來了內存碎片的問題。
複製算法
爲了解決性能的問題,複製算法應運而生,它將內存分爲大小相等的兩塊區域,每次使用其中的一塊,當一塊內存使用完以後,將還存活的對象拷貝到另一塊內存區域中,而後把當前內存清空,這樣性能和內存碎片的問題得以解決。可是同時帶來了另一個問題,可以使用的內存空間縮小了一半!
所以,誕生了咱們如今的常見的年輕代+老年代的內存結構:Eden+S0+S1組成,由於根據IBM的研究顯示,98%的對象都是朝生夕死,因此實際上存活的對象並非不少,徹底不須要用到一半內存浪費,因此默認的比例是8:1:1。
這樣,在使用的時候只使用Eden區和S0S1中的一個,每次都把存活的對象拷貝另一個未使用的Survivor區,同時清空Eden和使用的Survivor,這樣下來內存的浪費就只有10%了。
若是最後未使用的Survivor放不下存活的對象,這些對象就進入Old老年代了。
PS:因此有一些初級點的問題會問你爲何要分爲Eden區和2個Survior區?有什麼做用?就是爲了節省內存和解決內存碎片的問題,這些算法都是爲了解決問題而產生的,若是理解緣由你就不須要死記硬背了
標記-整理
針對老年代再用複製算法顯然不合適,由於進入老年代的對象都存活率比較高了,這時候再頻繁的複製對性能影響就比較大,並且也不會再有另外的空間進行兜底。因此針對老年代的特色,經過標記-整理算法,標記出全部的存活對象,讓全部存活的對象都向一端移動,而後清理掉邊界之外的內存空間。
那麼什麼是GC ROOT?有哪些GC ROOT?
上面提到的標記的算法,怎麼標記一個對象是否存活?簡單的經過引用計數法,給對象設置一個引用計數器,每當有一個地方引用他,就給計數器+1,反之則計數器-1,可是這個簡單的算法沒法解決循環引用的問題。
Java經過可達性分析算法來達到標記存活對象的目的,定義一系列的GC ROOT爲起點,從起點開始向下開始搜索,搜索走過的路徑稱爲引用鏈,當一個對象到GC ROOT沒有任何引用鏈相連的話,則對象能夠斷定是能夠被回收的。
而能夠做爲GC ROOT的對象包括:
-
棧中引用的對象
-
靜態變量、常量引用的對象
-
本地方法棧native方法引用的對象
垃圾回收器瞭解嗎?年輕代和老年代都有哪些垃圾回收器?
年輕代的垃圾收集器包含有Serial、ParNew、Parallell,老年代則包括Serial Old老年代版本、CMS、Parallel Old老年代版本和JDK11中的船新的G1收集器。
Serial:單線程版本收集器,進行垃圾回收的時候會STW(Stop The World),也就是進行垃圾回收的時候其餘的工做線程都必須暫停
ParNew:Serial的多線程版本,用於和CMS配合使用
Parallel Scavenge:能夠並行收集的多線程垃圾收集器
Serial Old:Serial的老年代版本,也是單線程
Parallel Old:Parallel Scavenge的老年代版本
CMS(Concurrent Mark Sweep):CMS收集器是以獲取最短停頓時間爲目標的收集器,相對於其餘的收集器STW的時間更短暫,能夠並行收集是他的特色,同時他基於標記-清除算法,整個GC的過程分爲4步。
-
初始標記:標記GC ROOT能關聯到的對象,須要STW -
併發標記:從GCRoots的直接關聯對象開始遍歷整個對象圖的過程,不須要STW -
從新標記:爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生改變的標記,須要STW -
併發清除:清理刪除掉標記階段判斷的已經死亡的對象,不須要STW
從整個過程來看,併發標記和併發清除的耗時最長,可是不須要中止用戶線程,而初始標記和從新標記的耗時較短,可是須要中止用戶線程,整體而言,整個過程形成的停頓時間較短,大部分時候是能夠和用戶線程一塊兒工做的。
G1(Garbage First):G1收集器是JDK9的默認垃圾收集器,並且再也不區分年輕代和老年代進行回收。
G1的原理了解嗎?
G1做爲JDK9以後的服務端默認收集器,且再也不區分年輕代和老年代進行垃圾回收,他把內存劃分爲多個Region,每一個Region的大小能夠經過-XX:G1HeapRegionSize設置,大小爲1~32M,對於大對象的存儲則衍生出Humongous的概念,超過Region大小一半的對象會被認爲是大對象,而超過整個Region大小的對象被認爲是超級大對象,將會被存儲在連續的N個Humongous Region中,G1在進行回收的時候會在後臺維護一個優先級列表,每次根據用戶設定容許的收集停頓時間優先回收收益最大的Region。
G1的回收過程分爲如下四個步驟:
-
初始標記:標記GC ROOT能關聯到的對象,須要STW -
併發標記:從GCRoots的直接關聯對象開始遍歷整個對象圖的過程,掃描完成後還會從新處理併發標記過程當中產生變更的對象 -
最終標記:短暫暫停用戶線程,再處理一次,須要STW -
篩選回收:更新Region的統計數據,對每一個Region的回收價值和成本排序,根據用戶設置的停頓時間制定回收計劃。再把須要回收的Region中存活對象複製到空的Region,同時清理舊的Region。須要STW
總的來講除了併發標記以外,其餘幾個過程也仍是須要短暫的STW,G1的目標是在停頓和延遲可控的狀況下儘量提升吞吐量。
何時會觸發YGC和FGC?對象何時會進入老年代?
當一個新的對象來申請內存空間的時候,若是Eden區沒法知足內存分配需求,則觸發YGC,使用中的Survivor區和Eden區存活對象送到未使用的Survivor區,若是YGC以後仍是沒有足夠空間,則直接進入老年代分配,若是老年代也沒法分配空間,觸發FGC,FGC以後仍是放不下則報出OOM異常。
YGC以後,存活的對象將會被複制到未使用的Survivor區,若是S區放不下,則直接晉升至老年代。而對於那些一直在Survivor區來回複製的對象,經過-XX:MaxTenuringThreshold配置交換閾值,默認15次,若是超過次數一樣進入老年代。
此外,還有一種動態年齡的判斷機制,不須要等到MaxTenuringThreshold就能晉升老年代。若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代。
頻繁FullGC怎麼排查?
這種問題最好的辦法就是結合有具體的例子舉例分析,若是沒有就說通常的分析步驟。發生FGC有多是內存分配不合理,好比Eden區過小,致使對象頻繁進入老年代,這時候經過啓動參數配置就能看出來,另外有可能就是存在內存泄露,能夠經過如下的步驟進行排查:
-
jstat -gcutil或者查看gc.log日誌,查看內存回收狀況
S0 S1 分別表明兩個Survivor區佔比
E表明Eden區佔比,圖中能夠看到使用78%
O表明老年代,M表明元空間,YGC發生54次,YGCT表明YGC累計耗時,GCT表明GC累計耗時。
[GC [FGC 開頭表明垃圾回收的類型
PSYoungGen: 6130K->6130K(9216K)] 12274K->14330K(19456K), 0.0034895 secs表明YGC先後內存使用狀況
Times: user=0.02 sys=0.00, real=0.00 secs,user表示用戶態消耗的CPU時間,sys表示內核態消耗的CPU時間,real表示各類牆時鐘的等待時間
這兩張圖只是舉例並無關聯關係,好比你從圖裏面看能到是否進行FGC,FGC的時間花費多長,GC後老年代,年輕代內存是否有減小,獲得一些初步的狀況來作出判斷。
-
dump出內存文件在具體分析,好比經過jmap命令jmap -dump:format=b,file=dumpfile pid,導出以後再經過 Eclipse Memory Analyzer等工具進行分析,定位到代碼,修復
這裏還會可能存在一個提問的點,好比CPU飆高,同時FGC怎麼辦?辦法比較相似
-
找到當前進程的pid,top -p pid -H 查看資源佔用,找到線程 -
printf 「%x\n」 pid,把線程pid轉爲16進制,好比0x32d -
jstack pid|grep -A 10 0x32d查看線程的堆棧日誌,還找不到問題繼續 -
dump出內存文件用MAT等工具進行分析,定位到代碼,修復
JVM調優有什麼經驗嗎?
要明白一點,全部的調優的目的都是爲了用更小的硬件成本達到更高的吞吐,JVM的調優也是同樣,經過對垃圾收集器和內存分配的調優達到性能的最佳。
簡單的參數含義
首先,須要知道幾個主要的參數含義。
-
-Xms設置初始堆的大小,-Xmx設置最大堆的大小 -
-XX:NewSize年輕代大小,-XX:MaxNewSize年輕代最大值,-Xmn則是至關於同時配置-XX:NewSize和-XX:MaxNewSize爲同樣的值 -
-XX:NewRatio設置年輕代和年老代的比值,若是爲3,表示年輕代與老年代比值爲1:3,默認值爲2 -
-XX:SurvivorRatio年輕代和兩個Survivor的比值,默認8,表明比值爲8:1:1 -
-XX:PretenureSizeThreshold 當建立的對象超過指定大小時,直接把對象分配在老年代。 -
-XX:MaxTenuringThreshold設定對象在Survivor複製的最大年齡閾值,超過閾值轉移到老年代 -
-XX:MaxDirectMemorySize當Direct ByteBuffer分配的堆外內存到達指定大小後,即觸發Full GC
調優
-
爲了打印日誌方便排查問題最好開啓GC日誌,開啓GC日誌對性能影響微乎其微,可是能幫助咱們快速排查定位問題。-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log -
通常設置-Xms=-Xmx,這樣能夠得到固定大小的堆內存,減小GC的次數和耗時,可使得堆相對穩定 -
-XX:+HeapDumpOnOutOfMemoryError讓JVM在發生內存溢出的時候自動生成內存快照,方便排查問題 -
-Xmn設置新生代的大小,過小會增長YGC,太大會減少老年代大小,通常設置爲整個堆的1/4到1/3 -
設置-XX:+DisableExplicitGC禁止系統System.gc(),防止手動誤觸發FGC形成問題
往期精選
另外,我輸出了 六本
PDF,已免費提供下載,以下所示
本文分享自微信公衆號 - Java建設者(javajianshe)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。