1. 程式人生 > >關於jvm記憶體模型和gc回收

關於jvm記憶體模型和gc回收

最近對於這方面的知識進行了學習,寫出來做個總結。

JVM 記憶體區域

  • 程式計數器

    這是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器,指的是上次程式碼被執行的地方,執行緒私有

  • Java 虛擬機器棧

    它是 Java方法執行的記憶體模型,每一個方法被呼叫到執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程,執行緒私有

  • 本地方法棧

    跟虛擬機器棧類似,不過本地方法棧用於執行本地方法,執行緒私有

  • Java 堆

    該區域存在的唯一目的就是存放物件,幾乎應用中所有的物件例項都在這裡分配記憶體,所有執行緒共享

  • 方法區

    它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,所有執行緒共享

執行緒私有的區域隨著執行緒的結束就沒有了,沒有垃圾回收;gc操作的地方是在所有執行緒共享的區域。微笑

記憶體分代

為什麼要分代?

       堆記憶體是虛擬機器管理的記憶體中最大的一塊,也是垃圾回收最頻繁的一塊區域,我們程式所有的物件例項都存放在堆記憶體中。給堆記憶體分代是為了提高物件記憶體分配和垃圾回收的效率。試想一下,如果堆記憶體沒有區域劃分,所有的新建立的物件和生命週期很長的物件放在一起,隨著程式的執行,堆記憶體需要頻繁進行垃圾收集,而每次回收都要遍歷所有的物件,遍歷這些物件所花費的時間代價是巨大的,會嚴重影響我們的GC效率,這簡直太可怕了。

       有了記憶體分代,情況就不同了,新建立的物件會在新生代中分配記憶體,經過多次回收仍然存活下來的物件存放在老年代中,靜態屬性、類資訊等存放在永久代中,新生代中的物件存活時間短,只需要在新生代區域中頻繁進行GC,老年代中物件生命週期長,記憶體回收的頻率相對較低,不需要頻繁進行回收,永久代中回收效果太差,一般不進行垃圾回收,還可以根據不同年代的特點採用合適的垃圾收集演算法。分代收集大大提升了收集效率,這些都是記憶體分代帶來的好處。

       記憶體分代劃分

Java虛擬機器將堆記憶體劃分為新生代老年代永久代,永久代是HotSpot虛擬機器特有的概念,它採用永久代的方式來實現方法區,其他的虛擬機器實現沒有這一概念,而且HotSpot也有取消永久代的趨勢,在JDK 1.7中HotSpot已經開始了“去永久化”,把原本放在永久代的字串常量池移出。永久代主要存放常量、類資訊、靜態變數等資料,與垃圾回收關係不大,新生代和老年代是垃圾回收的主要區域。記憶體分代示意圖如下:

       

       新生代(Young)

       新生成的物件優先存放在新生代中,新生代物件朝生夕死,存活率很低,在新生代中,常規應用進行一次垃圾收集一般可以回收70% ~ 95% 的空間,回收效率很高。

       HotSpot將新生代劃分為三塊,一塊較大的Eden空間和兩塊較小的Survivor空間,預設比例為8:1:1。劃分的目的是因為HotSpot採用複製演算法來回收新生代,設定這個比例是為了充分利用記憶體空間,減少浪費。新生成的物件在Eden區分配(大物件除外,大物件直接進入老年代),當Eden區沒有足夠的空間進行分配時,虛擬機器將發起一次Minor GC。

       GC開始時,物件只會存在於Eden區和From Survivor區,To Survivor區是空的(作為保留區域)。GC進行時,Eden區中所有存活的物件都會被複制到To Survivor區,而在From Survivor區中,仍存活的物件會根據它們的年齡值決定去向,年齡值達到年齡閥值(預設為15,新生代中的物件每熬過一輪垃圾回收,年齡值就加1,GC分代年齡儲存在物件的header中)的物件會被移到老年代中,沒有達到閥值的物件會被複制到To Survivor區。接著清空Eden區和From Survivor區,新生代中存活的物件都在To Survivor區。接著, From Survivor區和To Survivor區會交換它們的角色,也就是新的To Survivor區就是上次GC清空的From Survivor區,新的From Survivor區就是上次GC的To Survivor區,總之,不管怎樣都會保證To Survivor區在一輪GC後是空的。GC時當To Survivor區沒有足夠的空間存放上一次新生代收集下來的存活物件時,需要依賴老年代進行分配擔保,將這些物件存放在老年代中。

       老年代(Old)

在新生代中經歷了多次(具體看虛擬機器配置的閥值)GC後仍然存活下來的物件會進入老年代中。老年代中的物件生命週期較長,存活率比較高,在老年代中進行GC的頻率相對而言較低,而且回收的速度也比較慢。

       永久代(Permanent)

永久代儲存類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,對這一區域而言,Java虛擬機器規範指出可以不進行垃圾收集,一般而言不會進行垃圾回收。

       Minor GC 和 Full GC的區別

新生代GC(Minor GC):Minor GC指發生在新生代的GC,因為新生代的Java物件大多都是朝生夕死,所以Minor GC非常頻繁,一般回收速度也比較快。當Eden空間不足以為物件分配記憶體時,會觸發Minor GC。

       老年代GC(Full GC/Major GC):Full GC指發生在老年代的GC,出現了Full GC一般會伴隨著至少一次的Minor GC(老年代的物件大部分是Minor GC過程中從新生代進入老年代),比如:分配擔保失敗。Full GC的速度一般會比Minor GC慢10倍以上。當老年代記憶體不足或者顯式呼叫System.gc()方法時,會觸發Full GC。

垃圾回收演算法

標記-清除演算法(Mark-Sweep)

這是最基礎的收集演算法,如它的名字一樣,演算法分為“標記”和“清除”兩個階段:

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

之所以說它是最基礎的收集演算法,是因為後續的收集演算法都是基於這種思路並對其缺點進行改進而得到的。

它的主要缺點有兩個:

1、效率問題,標記和清除過程的效率都不高;

2、空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致,當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

複製演算法(Copying)

為了解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

這樣使得每次都是對其中的一塊進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為原來的一半,未免太高了一點。

但是這種演算法的效率相當高,所以,現在的商業虛擬機器都採用這種收集演算法來回收新生代。為什麼新生代可以使用複製演算法呢?

IBM 有專門研究表明,新生代中的物件 98% 都是朝生夕死,所以就不需要按照1:1的比例來劃分記憶體空間。這裡鑑於此,新生代採用瞭如下的劃分策略。

現在把新生代再劃分為三部分,一塊較大的 Eden(伊甸園) 和兩塊較小的 Survivor(倖存者) 區域。

當回收時,將 Eden 和 Survivor 中還存活著的物件一次性地拷貝到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor的空間。HotSpot 虛擬機器預設Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的記憶體是會被“浪費”的。

這樣清理完成後,原來的 Survivor 就空了,並一直保持為空,直到下次 Minor GC 時,它再作為存活物件的盛放地。兩個 Survivor 就這樣輪流當做 GC 過程中新生代存活物件的中轉站。

但是,如果使用複製演算法的記憶體區域有大量的存活物件時,複製演算法就會變得捉襟見肘,這時需要更大的 Survivor 區用於盛放那些存活物件,甚至可能需要 1:1的比例。所以針對堆記憶體區域的老年代,就有了下面的演算法。

標記-整理演算法

標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。這種方法避免了碎片的產生,同時也不需要一塊額外的記憶體空間,對於老年代會比較合適。

但是相比複製演算法,雖然該算法佔用的記憶體空間少,但是耗費的垃圾回收時間會比複製演算法久,所以上面也說了

我們應該儘量避免或者減少 Full GC 的發生。

   這兩種演算法用精煉的語言描述就是

  • 複製演算法:用空間換時間

  • 標記-整理演算法:用時間換空間

一句話 魚與熊掌不可兼得,但是針對新生代和老年代,他們都是最佳的選擇。

總結

簡單梳理一下文中講到的一些知識點

  • 為了更好的管理堆記憶體,該區域分為新生代和老年代。
  • 新生代發生垃圾回收要比老年代頻繁。
  • 新生代發生的垃圾回收成為 Minor GC;老年代發生的 GC 成為 Full GC。
  • 新生代使用複製演算法進行垃圾回收;老年代使用標記-整理演算法
  • 為了更高效管理新生代的記憶體,按照複製演算法,結合 IBM 的研究論證,新生代分為三塊,一塊比較大的 Eden 區和兩塊比較小的 Survivor 區,比例為 8:1:1
  • 儘可能的避免或者減少垃圾回收