學完了這篇JVM,面試官真拿我沒辦法了!

曠世奇才927發表於2021-05-31

在我們面試中經常會遇到面試官問一些有關JVM的問題,下面我大概從執行時資料域、類載入機制、類載入器、垃圾收集器、垃圾收集演算法、JVM堆記憶體模型、JVM記憶體結構、JVM調優等幾個方面來講一下JVM。

一、執行時資料區域

在執行Java程式的時候,JAVA虛擬機器會將自己所管理的記憶體劃分為若干個不同的資料區域,每個區域分工不同,這些區域統稱為“執行時資料區域”。下面來根據一張圖來看一下這幾個區域。
file
1、程式計數器

1>較小的記憶體空間。

2>當前執行緒位元組碼的行號指示器。

3>改變計數器的值來選取下一條需要執行的位元組碼指令。

4>一個處理器只會執行一條執行緒中的指令,為了執行緒切換後能恢復到正確的執行位置,每個執行緒一個計數器。

2、Java虛擬機器棧

1>執行緒私有。

2>生命週期與執行緒相同。

3>每個方法執行都會建立棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。

3、本地方法棧

1>執行緒私有。

2>使用到的本地(Native)方法服務。

4、Java堆

1>記憶體中最大的一塊。

2>執行緒共享。

3>堆滿了丟擲OutOfMemoryError異常。

5、方法區

1>執行緒共享。

2>用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、及時編譯器編譯後的程式碼快取等資料。

6、執行時常量池(次區域在方法區內部)

1>執行時常量池是方法區的一部分。

2>存放各種字面量與符號引用。

二、垃圾收集演算法

1、標記-清除演算法

首先標記出所有需要回收的物件,在標記完成後,統一回收掉所有被標記的物件。如圖。

file

2、標記-複製演算法

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活的物件複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。如圖。

file

3、標記-整理演算法

標記-整理演算法就是標記-清除演算法後再將存活物件整理到一起,從而騰出來更大的連續空間。如圖。

file

三、垃圾收集器

1、Serial收集器

file

Serial收集器是最基礎、歷史最悠久的收集器,這個收集器是一個單執行緒工作的收集器。

2、ParNew收集器

file

ParNew收集器實質上是Serial收集器的多執行緒並行版本,可以同時使用多條執行緒進行垃圾收集。

3、Parallel Scavenge 收集器

Parallel Scavenge收集器是一款新生代收集器,它是基於標記-複製演算法實現的收集器。Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量,所謂吞吐量就是處理器用於執行使用者程式碼的時間與處理器總消耗時間的比值,即。

file

4、Serial Old收集器

file

Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器。

5、Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支援多執行緒並行收集,基於標記-整理演算法實現。

file

6、CMS收集器

file

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,它的運作過程分為四個步驟,包括:

1> 初始標記

2> 併發標記

3> 重新標記

4> 併發清除

初始標記:初始標記需要stw,初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快。

併發標記:併發標記階段就是從GC Roots的直接關聯物件開始遍歷整個物件圖的過程,這個過程耗時較長但是不需要停頓使用者執行緒,可以與垃圾收集執行緒一起併發執行。

重新標記:重新標記階段則是為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。

併發清除:這個階段清理刪除掉標記階段判斷的已經死亡的物件,由於不需要移動存活物件,所以這個階段也是可以與使用者執行緒同時併發的。

6、Garbage First 收集器(簡稱G1收集器)

G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。如圖。

file

G1收集器的運作過程大致可以劃分為以下四個步驟:

初始標記:僅僅只是標記一下GC Roots能直接關聯到的物件,

併發標記:從GC Root開始對堆中物件進行可達性分析,遞迴掃描整個堆裡的物件圖,找出要回收的物件,這階段耗時較長,但可與使用者程式併發執行。

最終標記:對使用者執行緒做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。

篩選回收:負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據使用者所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活物件複製到空的Region中,再清理掉整個舊Region的全部空間。這裡的操作設計存活物件的移動,是必須暫停使用者執行緒,由多條收集器執行緒並行完成的。

file

四、類載入機制

當我們執行一個類的時候,我們會通過java.exe來呼叫jvm.dll檔案,建立一個類載入器,然後再載入我們的類,我們的類在載入的過程中會經歷載入、驗證、準備、解析、初始化。

file

載入:此階段將類的二進位制位元組流載入到記憶體中。

驗證:此階段將驗證檔案的格式、後設資料驗證、位元組碼驗證、符號應用驗證,保證這些程式碼執行後不會危害虛擬機器自身的安全。

準備:準備階段是為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始預設值。

解析:解析階段是Java虛擬機器將常量池內的符號引用替換為直接引用的過程,符號引用是用符號來描述所引用的目標,而直接引用是用地址來描述所應用的目標。

初始化:由於之前準備階段是給靜態變數賦值預設值,而初始化階段是給靜態變數賦值他的真實的值。

五、類載入器

1、啟動類載入器:這個類載入器負責載入存放在<JAVA_HOME>\lib目錄中的系統的jar。

2、擴充套件類載入器:這個類載入器負責載入存放在<JAVA_HOME>\lib\ext目錄中的擴充套件類jar。

3、應用程式類載入器:這個類載入器負責載入我們自己寫的類。

六、雙親委派模型

file

前面我們說到每個類會有一個對應的類載入器去載入這個類,而不同的類載入器所載入的類的型別不同,當我們要載入一個類的時候首先會去應用程式類載入器載入過的集合裡(注:這裡是載入過的集合裡)檢視有沒有載入這個類,如果沒有就去擴充套件類載入器載入過的集合裡檢視有沒有載入過這個類,如果沒有就再向上去找引導類載入器載入過的集合裡看有沒有載入過這個類,如果也沒有就會從引導類載入器要載入的核心類中尋找有沒有要載入的類,如果沒有就向下尋找擴充套件類載入器中要載入的擴充套件類中有沒有,如果也沒有就去應用程式載入器中尋找。

好處:

1、不會重複去載入一個類,假如應用程式類載入器載入過Student類,那麼下載再載入這個類的時候只需要判斷應用程式類載入過的集合裡有沒有載入過這個類,如果載入過就不用再次載入了。

2、防止惡意修改核心類庫,比如我們自己寫一個String類,我們去執行這個類,系統會在引導類載入器中載入Java的核心類庫中的String類,而不會載入我們自定義個的String類,這就防止我們私自篡改核心類庫。

七、JVM堆記憶體模型

file

在JVM中我們的堆記憶體模型大概為圖所示,我們新生成的物件會放到eden區,當我們eden區域放滿了,我們會進行一個輕GC,會把eden區域和S0區域的存活物件放到S1區域,然後將eden區域和S0區域清空,然後新生成的物件接著放到eden區域,當eden區域再次滿了,會將eden區域和S1區域的存活物件放到S0區域,然後將eden區域和S1區域清空,如此迴圈往復,沒進行一次迴圈會將沒有被清除的物件年齡+1,如果存活的物件年齡到達15(這個數值可以調整),就會將此物件放到老年代,或者是survivor區域滿了也會將一些物件放到老年代,如果當老年代滿了就會進行一次重GC。

八、JVM調優

所謂JVM調優主要就是減少堆記憶體中重GC的次數,這樣我們就根據物件什麼情況會進入老年代來進行分析,首先是存活15代(這個數值可以調整),這種情況一般就是一些常量等,這些一般不會有再大的優化空間,或者一些程式碼的問題造成迴圈呼叫,這種情況通過修改調整程式碼即可。其次是當survivor區域滿了的話就會向老年代放入一些物件,這個時候我們就需要根據業務情況來調整堆記憶體的大小以及年輕代和老年代的比例,一般我們例如一些電商系統,一些訂單物件進入記憶體中就是朝生夕是,訂單從建立到持久化資料庫中就不在記憶體中使用了,所以一些正常的業務產生的物件會在輕GC的時候被收回,但是有一些大物件可能放不下年輕代中所以就會放入老年代,這種情況我們就要調大堆記憶體中年輕代的大小,使得一些大物件可以放入到年輕代中,並隨著輕GC被垃圾收集器收回。

相關文章