初步瞭解JVM第三篇(堆和GC回收算法)

《初步瞭解JVM第一篇》《初步瞭解JVM第二篇》中,分別介紹了:html

  • 類加載器:負責加載*.class文件,將字節碼內容加載到內存中。其中類加載器的類型有以下:執行引擎:負責解釋命令,提交給操做系統執行。
    • 啓動類加載器(Bootstrap)
    • 擴展類加載器(Extension)
    • 應用程序類加載器(AppClassLoader)
    • 用戶自定義加載器(User-Defined) 
  • 執行引擎:負責解釋命令,提交給操做系統執行。
  • 本地接口:目的是爲了融合不一樣的編程語言提供給Java所用,可是企業中已經不多會用到了。
  • 本地方法棧:將本地接口的方法在本地方法棧中登記,在執行引擎執行的時候加載本地方法庫
  • PC寄存器:是線程私有的,記錄方法的執行順序,用以完成分支、循環、跳轉、異常處理、線程恢復等基礎功能。
  • 方法區:存放類的架構信息,ClassLoader加載的class文件內容存放在方法區中。
  • 棧:線程私有,用來管理Java程序的運行。

進行簡單的回顧後,接下來爲你們介紹Java中的堆。算法

堆(Heap)編程

你們可會分不清棧和堆,其實能夠簡單記住一句話:棧管運行,堆管存儲。堆是線程共享的,而棧是線程私有的。那麼什麼是堆呢?多線程

在一個JVM實例中,堆內存只存在一個。對內存的大小是能夠進行調節的,類加載器讀取了類文件以後須要把類、方法、常變量放到堆內存中,保存全部引用類型的真實信息,以便執行器執行。架構

首先拋給一個大的概念給你們先,爲你們介紹堆內存的三大部分(這裏咱們講的以JDK8的版本爲準,也就是將永久代變改成元空間):併發

  • 新生區:咱們new出來的對象的存放地址,而新生區又分爲三部分:
    • Eden(伊甸區)
    • Survivor 0 Space(倖存者0區)
    • Survivor 1 Space(倖存者1區)
  • 養老區:新生區的對象通過15次的GC回收(垃圾回收)以後存活下來的對象就放在這裏,養老區若是滿了也會進行GC回收,只不過發生的頻率小於新生區
  • 元空間:元空間咱們上一篇已經講過了,主要是用來存放類的結構信息,相似一個模板。

 

上以就是堆內存的大三部分:伊甸區、養老區、元空間。上圖是邏輯上的結構,可是在物理上只有新生區和養老區,並且咱們須要區分新生代和養老代用的是JVM的內存,可是元空間用的是系統內存。若是看得有點懵,沒關係,先來咱們來一個一個介紹,首先第一部分新生區。編程語言

新生區spa

新生區就是類的誕生、成長、消亡的區域。一個類在這裏產生、而後應用,最後被垃圾回收器回收,結束了的生命的過程釋放出內存。那麼咱們來簡單說一下,一個類被new出來以後從開始到消亡的一個過程:操作系統

  • 1)假設有一個程序是一直不斷在new對象,那麼new出來的對象首先就是存放在新生區的伊甸區,(注意:通常new的對象是放在新生區的伊甸區的,大的對象會特殊處理)。
  • 2)伊甸區的內存也是有限,程序一直在不斷的new對象,終於!!!在某一個時刻,伊甸園的空間快沒有地方能夠存放新的對象了。也就是達到伊甸區存放對象的閾值。這時候,注意!!!伊甸區就開始進行垃圾回收,也就是咱們常說的輕GC,將大部分再也不使用的對象Kill掉!!留下還在使用的對象。由於堆內存裏面的對象絕大多數都是臨時對象,因此一次垃圾回收會Kill掉90%以上的對象,能存活下來的數量很是少。
  • 3)存活下來的對象就從伊甸區移到了倖存者0區注意倖存者0區還有一個別名就作From。
  • 4)雖然垃圾回收會Kill掉大部分的對象,可是咱們仍是不能排除有個別現象存在伊甸區和倖存者0區再一次滿了的狀況,由於程序new的速度確定是比Kill的速度快的,終於又在某一時刻!!!伊甸區又達到了必定的閾值,再次進行垃圾回收,這時候就會將伊甸區和倖存者0區(注意:遷移的對象包括倖存者0區)存活下來的對象遷移到倖存者1區(倖存者1區的另一個別名爲To)。
  • 5)一直如此反覆,等到倖存者1區也滿了,就將存活的對象移到養老區進行養老,能到養老區的通常都一些長期使用的對象。那養老區怎麼肯定哪些纔是長期使用的對象呢?在新生區中,一個對象通過每次垃圾回收以後倖存下來的,都會進行計數,通過了15次垃圾回收以後依然存在的,就會進入到養老區。

(注意:講到這裏,是大部分對象消亡了,可是仍是有通過15次垃圾回收以後存活下來的對象進入了養老區)線程

養老區

在新生區中,咱們已經描述了一個類從開始到消亡或者進入養老區的過程,要麼就是被kill了,要麼就是進入了養老區。進入養老區以後就能夠舒舒服服的摸魚了嗎?你想得太簡單了,接下來看看,養老區又有怎麼樣的一番搏鬥呢:

  • 1)重新生區倖存下來的幸運兒來到了養老區養老,養老區就至關一個養老院,可是一個養老院也會滿員。這時候,沒辦法了,只能清出一部分老人,讓新的一批重新生區來的老人入住,這時候就發生了垃圾回收,也就是咱們說的重GC。
  • 2)雖然在養老區也會發生垃圾回收機制,可是仍是會有一天,這個養老院實在是騰不出空位了,即便是進行重GC也騰不出幾個空間,這時候沒辦法了!!!表明已經沒有內存了,玩不轉了,因此係統就會報錯,也就是咱們常看到的OOM(「OutOfMemoryError」):對內存溢出。
  • 3)因而乎,程序就異常中止了,全部對象都消亡了,這個就是程序中一個對象從開始到消亡的整個過程。

堆的內存大小分配:

 注:

  • From就是上面說的倖存者0區的別名
  • To就是上面說的倖存者1區的別名

這個比例咱們必定要記住,很是重要,這是在GC時選取何種算法的一個依據之一,新生代跟老年代是1:2,而新生代中的三個分區中分別是8:1:1。

看完了堆內存的結構,接下來咱們就要講講GC垃圾回收算法了。在上面咱們描述了一個對象從開始到結束的過程,中間會發生GC回收,其中:

  • 新生代:發生的GC叫作輕GC也叫MinorGC,所用的算法叫作複製算法。
  • 老年代:發生的GC叫作重GC也叫Full GC,所用的算法叫作標記清除算法和標記壓縮算法

  這裏過個眼熟,下面咱們在GC垃圾回收算法的時候會講到。

垃圾回收算法

在進行垃圾回收的時候,JVM須要根據不一樣的堆內存和結構去選取適合的算法來提升垃圾回收的效率,而垃圾回收算法主要有:

  • 引用計數法
  • 複製算法
  • 標記清除算法
  • 標記壓縮算法

1)引用計數算法

原理:給對象中每個對象分配一個引用計數器,每當有地方引用該對象時,引用計數器的值加一,當引用失效時,引用計數器的值減一,無論何時,只要引用計數器的值等於0了,說明該對象不可能再被使用了。

優勢:

  • 實現原理簡單,並且斷定效率很高。大部分狀況下都是一個不錯的算法。

缺點:

  • 每次對對象複製時均要維護引用計數器,且計數器自己也有必定的消耗。
  • 較難處理循環引用。

在JVM中通常不採用這種方式實現,因此就不展開來說了。

2)複製算法(Copying)——新生代使用

在新生代中的GC,用的主要算法就是複製算法,並且發生GC的過程當中From區和To區會發生一次交換(請記住這句話)。在堆的內存分配圖中JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(別名叫From和To)。默認比例爲8:1:1,通常狀況下,新建立的對象都會被分配到Eden區(一些大對象特殊處理),當Eden區進行了GC還存留下來的就會被移到Survivor區。對象在Survivor區每通過一輪GC存留下來年齡就會加1。直到它存活到了必定歲數的是時候就會被移到養老區。因爲新生區中的絕大部分對象都是臨時對象,不會存活過久,因此通過每一輪的GC以後存活下來的對像都很少,因此新生區所用的GC算法就是複製算法。

複製算法原理:

首先先給你們介紹一個名詞叫作根集合(GC Root):

  • 經過System Class Loader或者Boot Class Loader加載的class對象,經過自定義類加載器加載的class不必定是GC Root
  • 處於激活狀態的線程
  • 棧中的對象
  • JNI棧中的對象
  • JNI中的全局對象
  • 正在被用於同步的各類鎖對象
  • JVM自身持有的對象,好比系統類加載器等

有了上面的瞭解咱們就能夠來學學複製算法:

  • 複製算法從根集合(GC Root)開始,從From區中找到通過GC存活下來的對象(注意:雖說是From區,可是這裏的From區是包括了伊甸區和倖存者1區(別名From),因此你們不要認爲From區就是單單包括From區而已)。拷貝到To中;
  • 上面咱們說過From和To會發生一次交換就是發生在這裏,From將倖存下來的對象拷貝到To以後,這時From區就沒有對象,空出來了,而To如今不是空的,存放了From的倖存的對象(默認狀態是From有對象,To是空的)。這時候From和To就會發生身份的互換,下次內存分配從To開始。也就是說發生一次GC以後From就會變成To,To就會變成From(當誰是空的,誰就是To)
  • 一直這樣反覆GC,一直再一次發生GC的時候,From存活的對象拷貝到To時,To會被填滿,這時候就會把這些對象(知足年齡爲15的對象,這個值能夠經過-XX:MaxTenuringThreshold來設置,默認是15)移動到養老區。

  下面咱們用一張圖來描述一下複製算法發生的過程:

咱們一直都在反覆強調,Eden區的對象存活率是比較低的,因此通常就是拿兩塊10%的內存做爲空閒區(To)和活動區(From),拿80%的內存來存儲新建的對象。一但GC事後,就會將這10%的活動區和80%的Eden區存留下來的對象移到空閒區(To)中。而後以前的內存就獲得了釋放,依次類推。

複製算法的缺點:

  • 複製的時候須要耗費通常的內存,內存消耗大(可是效率的快的,並且新生區的存活效率低,並不須要複製太多的對象,因此新生區用這種算法效率是比咱們下面要講的算法效率高的)。
  • 若是對象的存活率很高,須要複製的對象太多,這時候效率就大大下降了。

複製算法的優勢:

  • 沒有標記和清除的過程,效率高。
  • 由於是直接對對象進行復制的,因此不會產生內存碎片。

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

老年代主要由標記清除算法和標記壓縮算法混合使用。

標記算法的步驟從名字其實就能夠看出來是怎麼回事了:

  • 標記須要清除的對象
  • 清除標記的對象

在複製算法中咱們就說了它的缺點是浪費空間,因此爲了解決這個問題,就不將對象進行復制了,由於複製一份須要同等大小的內存。標記清除算法採用標記的方式,將要清除的對象進行標記而後直接清除掉,這樣就就大大節省了空間了。同上,繼續來經過一張圖來理解:

上圖就是標記清除算法的過程,從過程當中能夠看出一些問題:

因爲回收的對象是進行標記後直接刪除的,因此就像上圖回收後所展現的同樣,內存空間是不連續的,也就是會有內存碎片的產生。第二個問題是複製算法是直接複製的,可是標記清除算法是須要掃描兩次,耗時嚴重。

標記清除算法的優勢:

  • 對須要回收的對象進行標記清除,不須要額外的空間。

標記清除算法的缺點:

  • 效率低,在進行GC時,須要中止整個程序。
  • 清理出來的內存空間是不連續的,存在內存碎片。因爲空間不連續,查找的效率也會下降

可是因爲養老區存活下來的對象會比新生區的對象多,因此用標記清除是比複製算法好的。

4)標記壓縮算法(Mark-Compact)

理解了標記清除算法後,其實這一個算法就比較簡單理解了。就是多了一步整理的階段,清除內存碎片使空間變得連續。過程以下圖:

標記壓縮算法的優勢:

  • 能夠看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。 
  • 標記/整理算法不只能夠彌補標記清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。

標記壓縮算法的缺點:

  • 雖然這個算法解決了上兩個算法的一些缺點,可是這個算法倒是耗時最長的。從效率來看是低於標記清除算法和複製算法的。

以上就是GC的四大算法,固然出了這四大算法還有標記清除壓縮算法(Mark-Sweep-Compact),這個也很好理解就是在整理階段再也不是GC一次就整理一次,而是每隔一段時間整理一次,減小移動對象的成本。

分代收集算法:

當有人問你哪一個算法是最好的時候,你的回答應該是:無,沒有最好的算法,只有最合適的算法。使用哪一個算法應該看GC發生在什麼地方:

  • 新生代:複製算法
    • 緣由:存活率低,須要複製的對象不多,所須要用到的空間不是不少。另一方面,新生代發生的頻率是很是高的,而複製算法的效率在新生代是最高的,因此新生代用複製算法是最合適的。
  • 老年代:標記清除和標記壓縮算法混合使用
    • 緣由:存在大量存活率高的對像,複製算法明顯變得不合適。通常是由標記清除或者是標記清除與標記整理的混合實現。
    • Mark階段的開銷與存活對像的數量成正比,這點上說來,對於老年代,標記清除或者標記整理有一些不符,但能夠經過多線程利用,對併發、並行的形式提升標記效率。
    • Sweep階段的開銷與所管理區域的大小成正相關,可是清除「就地處決」的特色,回收的過程沒有移動對象。使其相對其它有移動對像步驟的回收算法,仍然是效率最好的。可是須要解決內存碎片問題。
    • Compact階段的開銷與存活對像的數據成開比,如上一條所描述,對於大量對像的移動是很大開銷的,作爲老年代的第一選擇並不合適。
    • 基於上面的考慮,老年代通常是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器作爲補償措施:當內存回收不佳(碎片致使的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。

終於寫完了,以上即是本人對JVM的理解,若有不足歡迎提出,謝謝!!!

相關文章
相關標籤/搜索