深入理解jvm記憶體模型以及gc原理

民生Fintech發表於2019-10-11

整體架構

Jvm = 類載入器 + 執行引擎 + 執行時資料區域


類載入器

作用

類載入器是將編譯好的class檔案載入到記憶體中,並進行驗證、初始化等步驟,形成能被jvm直接使用的型別。

載入過程

可分解為5個步驟:載入–>連線–>初始化–>使用–>解除安裝。
深入理解jvm記憶體模型以及gc原理
  • 載入:把class檔案以二進位制位元組流的形式儲存到方法區中,並在堆中建立對應的class物件。
  • 連線:連線過程又分為3步,驗證、準備、解析。
① 驗證:驗證檔案格式、後設資料、位元組碼是否符合規範。
② 準備:為成員變數分配記憶體並初始化值。
③ 解析:解析是虛擬機器將常量池的符號引用替換為直接引用的過程。
  • 初始化:初始化過程主要包括執行構造方法,初始化靜態變數、靜態塊。

執行引擎

其作用是將class位元組碼轉變成機器能識別的碼,然後在jvm中建立方法棧去執行方法。


執行時資料區

執行時資料區包含方法區、堆、虛擬機器棧、本地方法棧、程式計數器。如下圖:

深入理解jvm記憶體模型以及gc原理

方法區

方法區同堆一樣,是所有執行緒共享的記憶體區域,為了區分堆,又被稱為非堆。用於儲存已被虛擬機器載入的類資訊、常量、靜態變數,如static修飾的變數載入類的時候就被載入到方法區中。


 堆

簡單的說就是物件的儲存區,它是被所有執行緒共享的一塊區域堆是java虛擬機器管理記憶體最大的一塊記憶體區域,因為堆存放的物件是執行緒共享的,所以多執行緒的時候也需要同步機制。堆回收演算法使用的複製演算法效率高沒有碎片利用率低分為三個區 eden、S0、 S1 按照8:1:1的預設值。

 程式計數器

我們知道對於一個處理器,在一個確定的時刻都會執行一條執行緒中的指令,一條執行緒中有多個指令,為了執行緒切換可以恢復到正確執行位置,每個執行緒都需有獨立的一個程式計數器,不同執行緒之間的程式計數器互不影響,獨立儲存。

虛擬機器棧

虛擬機器棧指的是java方法執行的記憶體概念模型,每個方法執行時都會在棧記憶體裡面建立一個棧幀,棧幀用來儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫到執行完成,都會對應一個棧幀在虛擬機器中入棧到出棧的過程。

本地方法棧

本地方法棧與虛擬機器棧功能相似,是為虛擬機器使用的Native方法服務。有的虛擬機器可能會把這兩個棧合二為一。

程式計數器

我們知道對於一個處理器,在一個確定的時刻都會執行一條執行緒中的指令,一條執行緒中有多個指令,為了執行緒切換可以恢復到正確執行位置,每個執行緒都需有獨立的一個程式計數器,不同執行緒之間的程式計數器互不影響,獨立儲存。


GC

目前比較常用的gc回收是年代回收法,JVM將堆分成了二個大區新生代、老年代和持久代。新生代和老年代的記憶體區域是在堆上也是gc回收的主要區域,預設情況新生代與老年代比例為1:2,該值可以通過引數–XX:NewRatio 設定。持久代是在方法區,持久代存放一些一般不需要被回收的物件,持久代一般情況不會觸發GC。

新生代

新生代又分為Eden和Survivor區,而Survivor由S0和S1組成。,新生代預設分配是eden:S0:S1為8:1:1,該值可以通過引數–XX:SurvivorRatio 來設定。新生代採用的是複製回收演算法,當第一次產生物件是在eden區分配空間,當eden區空間滿了時候,會在S0區域分配空間,當S0空間滿了時候會觸發Minor GC,這時候會把eden區和S0區存活物件複製出來放在S1區,然後直接清空eden區和S0區,新生代就是這麼反覆的進行垃圾回收。

老年代

老年代用於存放經過多次Minor GC之後依然存活的物件,在年代回收法物件有個年齡的概念,在新生代每進行一次Minor GC仍然存活的物件年齡都會加1,當物件年齡達到一定的值就會進入老年代區域, 預設的值是15 ,可以通過引數-XX:MaxTenuringThreshold 來設定。還有一種情況是當物件特別大時候不需要達到設定值會直接進入老年代。老年代由於物件比較穩定所以老年代採用標記整理演算法進行Full GC,此演算法會減少記憶體碎片帶來的效率損耗,下面會重點介紹一下本演算法。

垃圾收集演算法

①.(標記-清除)演算法
這是最基礎的演算法,標記-清除演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,標記完成後統一回收所有被標記的物件。這種演算法的缺點是會產生記憶體碎片而且效率也不高。下圖是此演算法的執行過程。
深入理解jvm記憶體模型以及gc原理


②.(複製)演算法
為了解決(標記-清除)演算法的缺陷,複製演算法就被提了出來。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,這種演算法雖然實現簡單,執行高效且不容易產生記憶體碎片,但是卻對記憶體空間的使用做出了高昂的代價,因為能夠使用的記憶體縮減到原來的一半。 很顯然,複製演算法的效率跟存活物件的數目多少有很大的關係,如果存活物件很多,那麼複製演算法的效率將會大大降低。我們的新生代GC演算法採用的是這種演算法。複製演算法執行過程如下圖:
深入理解jvm記憶體模型以及gc原理

③.(標記-整理)演算法
因為複製演算法效率低,清除演算法會產生記憶體碎片,所以又產生了了(標記-整理)演算法。該演算法標記階段和(標記-清除)一樣,但是在完成標記之後,它不是直接清理可回收物件,而是將存活物件都向一端移動,然後回收被標記的物件,此演算法的好處是效率高,同時不會產生記憶體碎片。標記-整理演算法執行過程如下圖:

深入理解jvm記憶體模型以及gc原理


記憶體洩漏

在我們平時寫程式碼時候很容易發生gc沒有及時回收的情況,這時就會發生記憶體洩漏情況,下面介紹一個記憶體洩漏的例子。


Public class test{
Public static Map<int, Object> map = new HashMap<int, Object>();
Public void insert(){
for (int i=1;i<100; 
i++){ Object o=new Object()
map.put(i,o);
}
}
}複製程式碼


在這個例子中,由於map是靜態的,所以gc不會回收,當執行insert方法時候,進入for迴圈,宣告o物件,然後放進map裡面,執行完此方法o物件原本可以被gc回收,但是由於map是靜態的所以不會被回收,這樣就會導致記憶體洩漏,所以我們在寫程式碼時候一定要謹慎使用常量和靜態變數這型別的變數,可能在不經意間造成記憶體洩漏情況。

總結


jvm以及gc都是我們寫程式碼和設計程式時經常能涉及到的知識,深入的學習一下jvm和gc能提高對jvm調優的能力,也可以讓自己寫出更優雅的程式碼。提高自己的java水平。


Thanks!



作者簡介

程東旭,民生科技有限公司,使用者體驗技術部Firefly移動金融開發平臺Java開發工程師。

深入理解jvm記憶體模型以及gc原理


相關文章