JVM垃圾收集器和內存分配策略

更多文章訪問我的博客網站

http://www.caoyong.xin:8080/blogger

垃圾收集器和內存分配策略

今天來說一說JVM的垃圾收集機制,這個是Java語言的一大特點

目錄

  • 什麼是“垃圾”

  • 收集“垃圾”的方法

  • 用什麼機器收集“垃圾”

  • 內存分配策略


1:什麼是“垃圾”

    Java堆中存放的是對象實例,而Java堆也是JVM收集對象垃圾的場所,用什麼方法可以判斷對象是否是“垃圾”?

    引用計數算法: 在對象中添加一個計數器,每當對象被引用的什麼,計數器就會加1,當引用失效的時候計數器就會減1,如果計數器爲0,則表示該對象不可能在被使用,標記爲“垃圾”。

    可達性分析算法:有一個“GC Roots“的對象作爲起始點,這些起始點會向下搜索,搜索的路徑稱爲”引用鏈“。當一個對象和”GC Roots“之間沒有任何引用鏈的時候,則判斷該對象不在被使用,標記爲“垃圾”。obj5,obj6與GC Roots之間沒有任何引用鏈,所以視爲“垃圾”。

967d3e9ef151a80b9b2c778877ac4060.png

什麼樣的對象可以當做GC Roots對象?

  1.     虛擬機棧中引用的對象

  2.    方法區類靜態屬性引用的對象

  3.    方法區常量引用對象

  4.    本地方法棧中,Native方法的引用。


即使經過可達性分析算法過後,對象沒有被任何引用鏈鏈接,也不能判斷該對象一定是“垃圾”,它還是有自救的機會,一個對象要被真正宣佈是“垃圾”,要經過兩次標記,如果對象經過可達性分析過後,沒有被任何引用鏈鏈接,則視爲第一次標記並進行一次篩選,篩選的依據是,是否覆蓋finalize()方法,如果finalize()方法沒有被虛擬機調用,加載過(也就是第一次調用該方法),那麼在該方法中,只用重新與引用鏈相連,那麼該對象就可以救活。否則,該對象被視爲第二次標記,判斷死亡,視爲“垃圾”。



2:收集“垃圾”的方法(垃圾收集算法)

    2.1:標記-清除算法(Mark-Sweep)

            20130f84142c0bd7eb6cb4df6ca4fe92.png


標記-清除算法,是一種最基礎的收集算法。該算法分爲兩個階段。

標記:也就是上面提到的,被標記爲“垃圾”的對象

清除:清除掉那些被標記的對象。


不足:

  一個是效率問題,標記和清除的過程效率都不是很高。

    第二個問題就是,會產生大量的內存碎片,浪費內存空間




2.2:複製算法

1447ca391f7abc9a1324321ae790e13f.jpg

    爲了解決效率問題,複製算法出現。

  算法思路:將內存平分爲二,每次只是用其中一塊,當着一塊內存使用完時,將該塊中的存活對象複製到另一塊新的內存中,然後再將該內存清除乾淨。


不足:這種算法是奢侈的,將內存一分爲二,且只是用內存的一般,這種代價很高。

 

 


2.3:標記-整理算法

1048273-20170914225354703-1260247534.png

標記-整理算法

    標記:和標記-清除算法一樣,標記那些可回收的對象

    整理:所有的存活對象都向一端移動,然後直接清理掉端邊界以外的內存。



2.4:分代收集算法

    現在商業的虛擬機大部分都是採用這種“分代收集”的算法,根據對象的存活週期,將內存劃分爲對應的幾個部分,一般把Java堆分爲新生代和老年代,不同代中實現不同的收集算法

    新生代:在新生代中Java對象大多都是“朝生夕死”,只有少量存活,所以在新生代中使用複製算法。

    老年代:由於老年代中對象的存活率較高,所以一般使用,標記-清除或者標記-整理算法。


3:HotSpot的算法實現

    前面提到了,標記垃圾的算法和收集垃圾的算法,而在HotSpot虛擬機中,在實現這些算法的時候,對算法的執行效率有着嚴格的要求。

1.枚舉根節點

  在可達性分析中,可以作爲GC Roots的節點有很多,但是現在很多應用僅僅方法區就有上百MB,如果逐個檢查的話,效率就會變得不可接受。

  而且,可達性分析必須在一個一致性的快照中進行-即整個分析期間,系統就像凍結了一樣。否則如果一邊分析,系統一邊動態表化,得到的結果就沒有準確性。這就導致了系統GC時必須停頓所有的Java執行線程。(sun將這件事成爲“Stop  The Word”)

  目前主流Java虛擬機使用的都是準確式GC,所以當執行系統都停頓下來之後,並不需要一個不漏的檢查完所有執行上下文和全局的引用位置,虛擬機應該有辦法直接知道哪些地方存放着對象引用。在HotSpot實現中,使用一組稱爲 OopMap 的數據結構來達到這個目的。OopMap會在類加載完成的時候,記錄對象內什麼偏移量上是什麼類型的數據,在JTI編譯過程中,也會在特定的位置記錄下棧和寄存器哪些位置是引用。這樣,在GC掃描的時候就可以直接得到這些信息了。

2.安全點

  如果OopMap內容變化的指令非常多,HotSpot並不會爲每條指令都產生OopMap,只是在特定的位置記錄了這些信息,這些位置成爲“安全點”(SafePoint)。程序執行時只有在達到安全點的時候才停頓開始GC。一般具有較長運行時間的指令才能被選爲安全點,如方法調用、循環跳轉、異常跳轉等。

  接下來要考慮的便是,如何在GC時保證所有的線程都“跑”到安全點上停頓下來。這裏有兩種方案: 搶先式中斷 (Preemptive Suspension) 和主動式中斷 (Voluntary Suspension)。

  搶先式中斷會把所有線程中斷,如果某個線程不在安全點上,就恢復線程讓它跑到安全點上。幾乎沒有虛擬機採用這種方式。

  主動式中斷思想是需要中斷線程時,不直接對線程操作,而是設置一個GC標誌,各個線程會輪詢這個標誌並在需要時自己中斷掛起。這樣,輪詢標誌的地方和安全點是重合的。

3.安全區域

  安全點機制保證程序執行時,在不太長的時間內就會遇到可進入GC的安全點,但是,程序“不執行”的時候呢,程序不執行就是沒有分配CPU時間,這時線程無法響應JVM的中斷請求,JVM顯然不太可能的等待線程重新被分配CPU時間。

  安全區域是指一段代碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。

  在線程執行到安全區域代碼時,首先標識自己進入安全區域,當這段時間裏JVM發起GC,不用管標識爲安全區域的線程了。在線程要離開安全區域時,要檢查系統是否已經完成了根節點枚舉,如果完成,線程繼續執行,否則等待直到收到可以安全離開安全區域的信號爲止。


4:用什麼機器收集垃圾(垃圾收集器)

    前面提到了垃圾收集算法是內存回收的理論。那麼垃圾收集器就是具體實現。隨着時間的發展Java虛擬機出現了很多的收集器,如圖

    3436608979.jpg


上面是新生代,下面是老年代。連線表示不同收集器可以一起使用。比如Serial和CMSyiji以及Serial Old


4.1:Serial收集器

     Serial收集器是最基本,發展最悠久的收集器,這是一個單線程的收集器。它在進行垃圾收集的時候,會停止所有其他線程,直到它收集結束。

                                                                                    Serial/Serial Old

220554_JpwZ_2431292.png

優點:在與其它單線程收集器相比,簡單而高效,是Client模式下的默認新生代收集器

缺點:由於存在“Stop The Word”的情況,就相當於,應用運行一個小時,要停止幾分鐘,這樣響應較敏感的應用,就不怎麼合適了。


4.2:ParNew收集器

    ParNew收集器是Serial收集器的多線程版。

1f3644c78239b5ab9452821716d59b59.jpg

ParNew收集器與Serial收集器並沒有太多的改進,但是他確實在Server模式下的首選新生代收集器,還有個與性能無關的因素,那就是出來Serial,目前只有ParNew可以和CMS(後面會提到)搭配使用。


接下來會提到併發和並行,下面來了解一下

併發:指用戶線程和垃圾收集線程同時運行,用戶線程在執行,而垃圾收集線程在另一個cup上執行。

並行:多條垃圾收集線程並行工作,而這個時候用戶線程處於等待狀態。


4.3:Parallel Scavenge收集器

 Parallel Scavenge收集器是新生代的收集器,採用複製算法收集,有又是並行的多線程收集器。

 Parallel Scavenge收集器的特點在於,它和其他收集器的關注點不同,像CMS等收集器的關注點是縮短垃圾收集時所造成用戶線程停頓的時間。而Parallel Scavenge收集器收集器的目標是達到一個可控的吞吐量。吞吐量=運行代碼時間/(運行代碼時間+垃圾收集時間)。


停頓時間越短就越合適需要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量可以提高cup的利用率。儘快完成程序。主要適合後臺運行計算不需要太多交互的程序。


4.4:Serial Old收集器

 Serial Old收集器是Serial的年老版的收集器,在老年代中它是一個單線程的收集器,使用標記-整理算法。主要給Client模式下的虛擬機使用。

                                                                                Serial/Serial Old

220554_JpwZ_2431292.png


4.5:Parallel Old收集器

        Parallel Old收集器收集器是Parallel Scavenge的年老版,使用多線程和標記-整理算法

                                                                                  Parallel Scavenge/Paeallel Old

989581411537ed3ef5b4e9f0c6d0b399.jpg

    

     Parallel Old收集器 的出現“吞吐量優先”的收集器有了合適的應用組合。在注重吞吐量和cup資源敏感的場所,ParallelScavenge/Paeallel Old  
      組合優先考慮。


4.6:CMS收集器。

    CMS(Concurrent Mark Sweep)的收集器,是一種以最短回收停頓時間爲目標的收集器。對於互聯網網站或者B/S(瀏覽器和服務器)系統的服務端上,對響應速度有着嚴格的要求,希望停頓時間最短,CMS收集器就比較合適。

ce046db50193a06dc555af566263054b.jpgc70d17dbccd6bd9a1a5fd970a9e8bde4.jpg


    CMS收集器是基於一種標記-清除的算法,相對於前面幾種,它的實現過程較爲複雜。分四個步驟

  • 初始標記

  • 併發標記

  • 重新標記

  • 併發標記

     其中初始標記和重新標記這兩個步驟仍然需要“Stop The Word”(應用線程停頓),初始標記標記的是GC Roots能直接關聯的對象。速度很快。

    併發標記就是進行GC Roots Tracing的過程,而重新標記則是爲了修正在併發標記階段因用戶線程繼續運作而導致產生變動的那一部分對象的標記記錄。這個階段要比初始標記時間長一點,但是遠沒有併發標記的時間長。

    由於整個過程耗時最長的在併發標記階段和併發清除階段,而這兩個過程收集線程都可以和用戶應用線程併發工作,所以總體來說CMS收集器是和用戶線程併發工作的。


CMS雖然很優秀,但是也是有一些不足的。

1:CMS由於在併發標記和併發清除階段都是和應用線程同時進行,那麼這樣也就導致了,垃圾收集清理過程會佔用一部分cup資源,導致應用程序變慢,吞吐量下降。爲了應付這種情況,虛擬機提供了一種“增量式併發收集器”。在併發標標記和併發清除的時候,讓GC現場和應用線程交替運行,一減少GC佔用CPU資源。

2:CMS收集器無法處理浮動垃圾,所謂浮動垃圾,就是在併發清除的過程中,由於應用線程還在工作,這段時間也會產生“垃圾”。這這一部分垃圾,是這次垃圾收集清除過程無法清除的,只能留到下次清除過程。

3:由於CMS採用的是標記—清除算法,所以會產生大量的磁盤碎片。


4.7:G1收集器

G1收集器收集器是現在收集器的最新成果。是一款面向服務器端的垃圾收集器。未來可以替換CMS收集器,G1收集器有一下特點


1、並行於併發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行。


2、分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間,熬過多次GC的舊對象以獲取更好的收集效果。


3、空間整合:與CMS的“標記--清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的。


4、可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,


G1收集器的執行過程

  1. 初始標記

  2. 併發標記

  3. 最終標記

  4. 篩選回收

timg.jpg


雖然G1收集器是最新的也是最先進的收集器,但是由於纔開發不久,所以還沒有大規模應用和測試。所以CMS還是最好的選擇。


5:內存分配策略

Java技術體系中所提倡的是自動化內存管理,也就是解決兩個問題對象的內存分配以及內存的回收,回收前面已經講過了,現在講講對象的內存分配。


對於對象的分配也就是在Java堆上分配,Java對象的分配大部分在新生代的Eden區上面(關於Java分區,前一篇文章做了詳細講解)。但是也有一部分對象會被分在老年代。

首先還是講一下新生代和老年代

新生代GC:(Minor GC)是指發生在新生代的垃圾收集動作。Java對象大多都有朝生夕滅的特性,所以在Minor GC非常頻繁,回收速度也比較快。

老年代GC:(Major GC/Full GC是指發生在老年代的GC 經常會伴隨一次Minor GC 但也不是絕對。一般Major G迴避Minor GC慢10倍。


5.1:對象優先分在新生代的Eden區

5.2:大對象直接進入老年代。所謂大對象就是大量連續佔用內存。比如較長的字符串和數組。

5.3:長期存活的對象進入老年代

5.4:動態對象年齡判斷:如果在Survivor空間(新生代中的一種空間)中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象將會直接進入老年代
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章