1. 程式人生 > 程式設計 >JVM 記憶體佈局與GC演演算法

JVM 記憶體佈局與GC演演算法

1 JVM記憶體佈局圖

整體上來看,JVM的記憶體分為堆區非堆區,而非堆區又包括了方法區、JVM棧、本地方法棧、程式計數器等。

2 JVM執行時資料區劃分

2.1 JVM堆

其主要作用是用於為幾乎所有的物件例項陣列例項的例項化提供記憶體空間。說通俗點,所有采用new關鍵字產生的物件的空間都在此分配。 如:Map<String,String> map = new HashMap<>(); 這個map所引用的物件即位於JVM的堆中。 JVM heap的特點是:

  1. 記憶體不一定連續分配,只要邏輯上是連續的即可;
  2. 記憶體的大小可以通過JVM的引數來控制:如 -Xms = 1024M -Xmx = 2048M; 表示JVM Heap的初始大小為1GB,最大可自動伸縮到2GB;
  3. 平時所說的Java記憶體管理即指的是記憶體管理器對這部分記憶體的管理(建立/回收);
  4. 所有執行緒共享;

2.2 JVM棧

JVM棧是伴隨著執行緒的產生而產生的,屬於執行緒私有區域,生命週期和執行緒保持一致,所以,JVM的記憶體管理器對這部分記憶體無需管理; 從圖1.1中可以看出,棧中有包含了多個棧幀(frame),frame 伴隨的方法的呼叫而產生,方法呼叫完畢,frame也就出棧,從JVM棧中被移除。frame主要儲存方法的區域性變數、運算元棧、動態連結、方法出口(reternAddress或者拋Exception)。 虛擬機器器規範中,對此記憶體區域規定了兩種異常情況:

  1. 當執行緒請求的棧的深度大於JVM所允許的最大深度,則丟擲StackOverflowError
  2. 當棧的記憶體是可動態擴充套件的時候,如果擴充套件時發現堆記憶體不足時,會丟擲OutofMemoryError

2.3 PC(程式計數器)

簡稱PC(Program Counter Register),為執行緒私有,如果執行的是一個Java方法,那麼此時PC裡儲存的是當前Java位元組碼的指令地址;如果說執行的是Native方法,那麼PC的內容則為空。

2.4 方法區

主要用於儲存已經由JVM載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。在Hotspot JVM中,設計者使用“永久代”來實現方法區,這樣帶來的好處是,JVM可以不用再特別的寫程式碼來管理方法區的記憶體,而可以像管理 JVM 堆一樣來管理。帶來的弊端在於,這很容易造成記憶體溢位的問題。我們知道,JVM的永久代使用 --XX:MAXPermSize來設定永久代的記憶體上限。 值得一提的是,執行時常量池也屬於方法區的一部分,所以,它的大小是受到方法區的限制的。執行期間也可以將新的常量放入池中,運用的比較多的就是String的intern()方法。 注意:

  • 在JDK6.0及之前版本,字串常量池是放在Perm Gen區(也就是方法區)中;
  • 在JDK7.0版本,字串常量池被移到了中了。

2.5 本地方法棧

與JVM棧非常類似,只不過,本地方法棧是為Java的Native method方法呼叫的時候服務。JVM規範並未定義如何實現該區域,但是,在Hotspot JVM中,本地方法棧和JVM棧合二為一,所以,本地方法棧同樣會丟擲StackOverflowErrorOutofMemoryError

3 GC(Garbage Collection)回收機制

3.1 基本概念

垃圾回收(Garbage Collection)是Java虛擬機器器(JVM)垃圾回收器提供的一種用於在空閒時間不定時回收無任何物件引用的物件佔據的記憶體空間的一種機制。

  • 引用:如果Reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。(引用都有哪些?對垃圾回收又有什麼影響?)
  • 垃圾無任何物件引用的物件(怎麼通過演演算法找到這些物件呢?)。
  • 回收:清理“垃圾”佔用的記憶體空間而非物件本身(怎麼通過演演算法實現回收呢?)。
  • 發生地點:一般發生在堆記憶體中,因為大部分的物件都儲存在堆記憶體中(堆記憶體為了配合垃圾回收有什麼不同區域劃分,各區域有什麼不同?)。
  • 發生時間:程式空閒時間不定時回收(回收的執行機制是什麼?是否可以通過顯示呼叫函式的方式來確定的進行回收過程?)

3.2 判斷 “垃圾” 的方法

垃圾收集器會對堆進行回收前,判斷物件中哪些是“存活”,哪些是“死亡”。

  1. 引用計數演演算法

    每當一個地方引用它時,計數器+1;引用失效時,計數器-1;計數值=0 —— 不可能再被引用。 缺點:物件之間相互矛盾迴圈引用的問題。

  2. 可達性分析演演算法

    把一系列“GC Roots”作為起始點,從節點向下搜尋,路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連,即不可達時,則證明此物件時不可用的。

3.3 垃圾回收演演算法

  1. 標記清除演演算法

    標記-清除(Mark-Sweep)演演算法是現代垃圾回收演演算法的思想基礎。標記-清除演演算法將垃圾回收分為兩個階段:標記階段清除階段。一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始的可達物件。,未被標記的物件就是未被引用的垃圾物件(好多資料說標記出要回收的物件,其實明白大概意思就可以了)。然後,在清除階段,清除所有未被標記的物件

  2. 標記整理演演算法

    標記整理演演算法類似與標記清除演演算法,不過它標記完物件後,不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體

  3. 複製演演算法

    複製演演算法可以解決效率問題,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊記憶體用完了,就將還存活著的物件複製到另一塊上面,然後再把已經使用過的記憶體空間一次清理掉,這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可(還可使用TLAB進行高效分配記憶體)。

3.4 垃圾回收流程

在JVM的記憶體空間中把堆空間分為年老代年輕代。將大量(據說是90%以上)建立了沒多久就會消亡的物件儲存在年輕代,而年老代中存放生命週期長久的例項物件。年輕代中又被分為Eden區(聖經中的伊甸園)、和兩個Survivor區。新的物件分配是首先放在Eden區,Survivor區作為Eden區和Old區的緩衝,在Survivor區的物件經歷若干次收集仍然存活的,就會被轉移到年老區。

  1. 新建的物件,大部分儲存在Eden區中。
  2. 當Eden記憶體不夠,就進行Minor GC釋放掉不活躍物件;然後將部分活躍物件複製到Survivor區中(如Survivor1),同時清空Eden區。
  3. 當Eden區再次滿了,將Survivor1中不能清空的物件存放到另一個Survivor中(如Survivor2),同時將Eden區中的不能清空的物件,複製到Survivor1,同時清空Eden區。
  4. 重複多次(預設15次):Survivor中沒有被清理的物件就會複製到老年區(Old)
  5. 當Old達到一定比例,則會觸發Major GC釋放老年代。
  6. 當Old區滿了,則觸發一個一次完整的垃圾回收(Full GC)。
  7. 如果記憶體還是不夠,JVM會丟擲記憶體不足,發生oom,記憶體洩漏。