深入理解JVM--03--垃圾收集器和內存分配策略

總結:

面試一般會問  java垃圾回收機制 GC算法  java內存分區(結構) java內存模型 類加載機制

GC,內存的分區,堆,類放在哪個區,什麼時候會內存溢出,GC root

CMS   G1收集器要能說出來, OOM   StackoverFlow ,  這還是要認真理解並適當強化記憶,有時書看了,但不總結的話,看了也白看,兩天之後就忘了,技術類的書籍一定要多總結,別偷懶,有時現在怕麻煩,將來更麻煩

1. 概述

GC的歷史比java久遠, Lisp語言就用到了GC

GC要解決的三個問題: 哪些內存需要回收?什麼時候回收?如何回收?

在java裏面,內存區域中的程序計數器、虛擬機棧、本地方法棧這3個區域隨着線程而生,線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧的操作,每個棧幀中分配多少內存基本是在類結構確定下來時就已知的。在這幾個區域不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟着回收了

Java堆和方法區則不同,一個接口中的多個實現類需要的內存可能不同,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的

(1)引用計數算法

          給對象添加引用計數器,有一個地方引用就加1, 失效了就減1,計數器爲0的對象不可能被再次使用

          缺點:無法解決對象之間循環引用的問題

(2)可達性分析算法

         java C# Lisp都是採用的這種

        概念:根(GC Roots)的對象作爲起始點,開始向下搜索,搜索所走過的路徑稱爲“引用鏈”,當一個對象到GC Roots沒                     有任何引用鏈相連時,則證明此對象是不可用的。

       在java中, 可以作爲GC Roots的對象

         1、棧(棧幀中的本地變量表)中引用的對象。

         2、方法區中的靜態成員。

         3、方法區中的常量引用的對象(全局變量)

         4、本地方法棧中JNI(一般說的Native方法)引用的對象。

再談引用:四大引用

強引用:Object o=new Object() 只要強引用存在,就不會被回收 

軟引用:SoftReference類來實現     軟引用關聯的對象,在將要發生內存溢出異常之前,纔會被GC。

弱引用:WeakReference     強度比soft弱,關聯的對象只能生存到下一次GC發生之前

虛引用:PhantomReference   最弱    關聯對象的唯一目的 就是被GC時 收到一個系統通知

3. 垃圾收集算法

1.標記--清除算法(Mark--Sweep)

標記-清除算法:最基礎的收集算法“標記--清除”(Mark-sweep)算法,算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,對象的標記過程是採用“可達性分析算法”來進行的。之所以說它是最基礎的收集算法,是因爲後續的收集算法都是基於這種思路並對其不足進行改進而來的。

    主要缺點:a、效率問題,標記和清除兩個過程的效率都不高。

                      b、空間問題,標記清除後會產生大量的不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續空間而不得不提前觸發另一次垃圾收集活動。

2.複製算法(新生代採用)

爲了解決效率問題,複製的收集算法出現了

他將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活的對象複製到另一塊上面,當然再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這個算法的代價是將內存縮小爲了原來的一半,未免太高了點。

現在的商業用的虛擬機都是採用這種算法,HotSpot默認的Eden : survivor =8:1

3. 標記-整理算法(老年代採用)

複製算法在對象存活率較高的時候進行較多的複製操作,效率將會變得更低

老年代一般不適用複製算法

標記過程仍然與“標記--清除”算法一樣,但後續步驟不是直接對回收對象進行清理,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的內存

4.分代收集算法:當前商業虛擬機的垃圾收集都採用“分代收集”(Generational Collection)算法,這種算法只是根據對象存活週期的不同將內存劃分爲幾塊。一般是把java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適合的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記--清理”或者“標記--整理”算法來進行回收。

(二)HotSpot的算法實現

1. Serial收集器(單線程收集)

JDK1.3新生代收集的唯一選擇

單線程的收集器:只使用一個CPU或一條收集線程進行垃圾收集時,它在工作時,必須暫停其他所有工作線程,直到它收集結束

2.ParNew收集器(Serial多線程版本的收集

他是serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲包括serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop the world、相當多的代碼、回收策略等都與Serial收集器完全一樣。

注意在談及垃圾收集器   幾款併發和並行的收集器時 根據上下文語句, 解釋兩個概念

&1.並行: 指多條垃圾收集線程並行工作,但此用戶仍處於等待狀態

&2. 併發: 指用戶線程和垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行在另外的CPU上

3.Parallel Scavenge收集器(吞吐量優先的收集器):是一個新生代收集器,他也是使用複製算法的收集器,又是並行的多線程收集器。看上去和ParNew都一樣,但他的特點是他的關注點與其他收集器不同,CMS等收集器的關注點是儘可能的縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

4.Serial Old收集器:它是Serial收集器的老年代版,它同樣是一個單線程收集器,使用“標記--整理”算法。這個收集器的意義在於給Client模式下的虛擬機使用。如果在Server模式下,那麼它主要有兩大用途:一種是在jdk1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作爲CMS收集器的後預案,在併發收集發生Concurrent Mode Failure時使用。

5。Parallel Old 收集器:是Parallel Scavenge收集器的老年代版,使用多線程與“標記--整理”算法。這個收集器在jdk1.6中才開始提供的,直到Parallel Old 收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加 Parallel Old收集器。

 

前面的收集器作爲了解的話,接下來的收集器比較重要

6. CMS收集器(Concurrent Mark Sweep)

CMS:是一種以獲取最短回收停頓時間爲目標的收集器基於標記--清除算法實現的

目前很大一部分的java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

過程分爲4步:初始標記   :  標記GC Roots能直接關聯的對象

                        併發標記   : 進行GC Roots Tracing的過程

                        重新標記 : 爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那部分標記記錄

                         併發清除 

           初始和重新標記需要 stop the world      兩個併發過程耗時較長         

  CMS收集器是低停頓的收集器,但是還有三大缺點

     1)對CPU資源敏感(併發設計都對CPU敏感)

    2)無法處理浮動垃圾(CMS在當次收集無法處理,只好留到下次GC處理的),可能出現Concurrent Mode failure失敗導致另一次full GC,老年代使用60%就激活,JDK1.6 時92%才激活

     3)收集結束時產生大量空間碎片(空間碎片過多 導致大對象分配困難 老年代還有很多空間但是沒有足夠的連續空間 不得不提前Full GC)

7.G1收集器

(1)最先進的收集器

G1是一款面向服務器端應用的垃圾收集器。與其他GC收集器相比,G1具備如下特點:

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

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

    c、空間整合:與CMS的“標記--清理”算法不同,G1從整體來看是基於“標記--整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的,但無論如何,這兩種算法都意味着G1運行期間不會產生內存空間碎片,收集後能提供規整的可用內存。這個特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前出發下一次GC。

    d、可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時java(RTSJ)的垃圾收集器的特性了。

  使用G1收集器時,java堆的內存佈局就與其他收集器有很大差別,它將真個java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留新生代與老年代的概念,但新生代與老年代不再試物理隔離的了,他們都是一部分Region(不需要連續)的集合。

    如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分爲一下幾個步驟:

    a、初始標記(Initial Marking)

    b、併發標記(Concurrent Marking)

    c、最終標記(Final Marking)

    d、篩選回收(Live Data Counting and Evacuation)

三. 理解GC日誌

33.125:[GC [DefNew:3324K->152K(3712K),0.0025925secs] 3324K->152K(11904K),0.0031680 secs]

  

100.667:[FullGC [Tenured:0K->210K(10240K),0.0149142secs] 4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs] <br>[Times:user=0.01 sys=0.00,real=0.02 secs]

  

    (1)最前面的數字“33.125:”和“100.667:”代表了GC發生的時間,這個數字的含義是從Java虛擬機啓動以來經過的秒數。

    (2)GC日誌開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的,例如下面這段新生代收集器ParNew的日誌也會出現“[Full GC”(這一般是因爲出現了分配擔保失敗之類的問題,所以才導致STW)。如果是調用System.gc()方法所觸發的收集,那麼在這裏將顯示“[Full GC(System)”。

[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]

    (3)接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名爲“Default New Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會變爲“[ParNew”,意爲“Parallel New Generation”。如果採用Parallel Scavenge收集器,那它配套的新生代稱爲“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。GC發生區域日誌與GC收集器對照列表如下:

GC日誌區域名 對應GC收集器名
[DefNew (Default New Generation) Serial收集器
[ParNew (Parallel New Generation) ParNew收集器
[PSYoungGen Parallel Scavenge收集器
[ParOldGen Parallel Old收集器

    (4)後面方括號內部的“3324K->152K(3712K)”含義是“GC前該內存區域已使用容量->GC後該內存區域已使用容量(該內存區域總容量)”。 
而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)”。

    (5)再往後,“0.0025925 secs”表示該內存區域GC所佔用的時間,單位是秒。

          有的收集器會給出更具體的時間數據,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,這裏面的user、sys和real與Linux的time命令所輸出的時間含義一致,分別代表用戶態消耗的CPU時間、內核態消耗的CPU事件和操作從開始到結束所經過的牆鍾時間(Wall Clock Time)。

            CPU時間與牆鍾時間的區別是,牆鍾時間包括各種非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間,所以讀者看到user或sys時間超過real時間是完全正常的。

四.內存分配和回收策略

1. 對象的內存分配:主要分配在堆內存的新生代的Eden區

2. 對象優先在Eden分配,

          當沒有足夠的空間進行分配時  JVM發起一次Minor GC (minor:較小的,少數的 未成年的)也就是輕量級GC       Eden : from: to=8:1:1

3. 大對象直接進入老年代

           大對象:就是需要大量連續內存空間的java對象  就是很長的字符串和數組byte[ ]就是典型的大對象

4. 長期存活的對象將進入老年代

          JVM給每個對象一個對象年齡age計數器,Eden--->Survivor 就是1歲,此後每次在Survior熬過一個Minor GC就增加1歲,  到15歲就晉升到老年代

5.空間分配擔保

       發生在Minor GC之前,JVM先檢查老年代最大可以使用的連續內存空間 > 新生代所有對象總空間,這將確保Minor GC安全,    如果小於 或者HandlePromotionFailure設置爲不允許冒險  改爲進行一次Full GC

-Xms   -Xmx   java heap

-Xmn              新生代內存

   

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章