[016]JVM如何分配內存及其回收策略01

背景

以前看了周志明的《深入理解Java虛擬機》,今天突然想談起它來。
所以今天這篇文章會說說 java的內存分配策略和垃圾收集器。

提出問題

java 中內存如何申請的,什么樣的數據存放在新生代,什么樣的數據存放在老生代,什么樣的數據存放在永久代。

數據的回收:這3個區域的數據是如何回收的呢?各使用什么樣的垃圾收集器。
今天的文章圍繞他們解決。

對象如何被轉移

這里有兩個概念:MinGc:新生代GC, Major GC:老生代GC。
新生代往老生代轉移的原則是:
1).在進行15次 MinGC還存活下來的就轉移到 Old GC。
2).如果對象太大,直接放到 Old區。
3).如果MinGC 過程中,Survivor放不下會被擔保到Old區。

當Eden區滿了就會觸發MinGC,當Old區滿了就會觸發Major GC。
為什么會有2個Survivor,是由于必須經過15次MinGC才會轉移到Old GC,起到轉騰的作用。
比如 第一次MinGC:Eden +Suv1 -> Surv2,第二次Min GC: Eden + Surv2 -> surv1。
這里虛擬機為每個對象定義了年齡計數器,當年齡大于閥值得時候就進入老年代。

大對象直接進入老年代,大對象是指需要大量連續內存空間的對象)。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代采用復制算法收集內存)。

堆內存如何被申請

虛擬機如何執行 Class1 c1 = new Class1()這條指令。
在執行這條指令的時候要做兩件事情:
1.檢查這個指令的參數是否能在常量池中定位到一個符號引用,并且檢查這個符號引用是否被加載、解析、初始化。如果沒有必須先執行類加載過程。
2.申請內存,內容申請需要看堆中內存是否連續,如果是連續通過移動指針頭(稱為指針膨脹)。如果不連續的話就通過“空閑列表”定位。 而內存是否連續跟垃圾回收算法是否帶有壓縮功能有關。

serial和parNew帶有壓縮功能,而CMS基于mark-sweep算法的收集器,通常采用空閑列表。
分配空間的過程中,還需要考慮一個問題是。如果多個線程同時修改指針會出現并發的問題。
虛擬機采用兩種辦法 a).CAS+失敗重試(CAS就是我們所說的樂觀鎖機制)。 b).內存分配的動作劃分到不同的線程空間中。
3.內存申請完之后就是初始化。
這里涉及到對象的內存布局,對象分為對象頭(如hashcode,對象類型指針)、實例數據、對象填充。

對象如何被定位呢?
我們知道對象的引用放在棧里,所以我們有兩種方式引用到堆里的內存。
1.棧里的reference 直接指向 堆里的對象。
2.棧里reference 指向堆里 對象句柄(對象句柄這個地址是固定的),對象類型數據指針是放在 對象句柄中的,所以當對象移動了只會修改 對象句柄中實例數據指針。

由于使用句柄的方式多了一次指針定位的開銷,所以比第一種會慢一些。

什么樣的數據會被回收?

簡單的說就是無用的數據應該被回收,什么是無用的呢?
怎么判定無用:市面上有
1).引用計數法(解決不了循環引用的場景 A與B互相引用)
2).可達性分析算法
這個算法就是通過 一系列稱為 "GC Roots"的對象作為起點,搜索鎖走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任可達路徑則會被回收。

那什么是GC Roots對象呢?
GC Roots對象包含如下幾點:a.虛擬機棧中引用的對象。 b.方法去中類靜態屬性引用的對象。 c.方法區中常量引用的對象。 d.本地方法棧中JNI引用的對象。

什么時候被回收

什么時候被回收是根據其引用的類型來決定的,java中的引用分為以下幾種。
1.強引用 2.軟引用 3.弱引用 3.虛引用
強引用就是類似于 Class1 c1 = new Class1(); 永遠不會被回收
軟引用用來描述有用但是非必須的,在內存溢出的會被回收。 java中通過SoftReference實現軟引用。
弱引用強度比軟引用更加的弱一些,再下一次垃圾回收時就會被回收。
虛引用,我們不會通過虛引用來獲取一個對象也不會對生存時間構成影響,它的作用就是在被回收掉后收到一個系統通知。

怎么被回收

垃圾回收算法

這里描述使用什么算法及其如何回收內存。
垃圾回收算法應該考慮以下幾點:
1).回收的效率和時間。
2).垃圾回收的時候是否影響正在運行的程序。

我們先不考慮分代的問題,垃圾回收就是想把沒有用的清楚掉把有用的留下來。對于無用的對象,我們可以標記清楚 - 標記清楚法。 對于有用的對象我們可以采用標記整理法 或者 復制法。

這里提一下復制法:新生代中包含(Eden + Survivor1 + Survivor2),每次會把Eden +Survivor中的存活對象移動到另一個 Survivor中,如果另一個Survivor空間不夠就需要老生代擔保。

而實際jvm中是分為新生代、老生代。對于新生代由于回收頻率高存活對象比較少,所以采用復制法。對于老生代由于對象存活率高沒有額外的區域進行擔保所以使用(標記清除、標記整理)法。

垃圾回收器

前面談到了收集算法,但是垃圾具體是通過收集器進行回收的。市面上最簡單的垃圾收集器是serial收集器,由于其簡單專心做垃圾收集適用于運行在client模式下的虛擬機(新生代也只有幾十到100多兆)。

a).parNew是Serial的多線程版本,其余行為包括垃圾收集算法(復制算法)都與Serial收集器一致。parNew是運行在server下的首選新生代收集器。

b).Cocurrent Mark-sweep,這個是虛擬機真正意義上第一款并發收集器,由于其使用Mark-sweep算法所以其適用于老生代。

c).Parallel Scavenge 提供兩個參數MaxGCPauseMillis,GCTimeRatio來設置虛擬機優化目標,該收集器注重的的是吞吐量,對于與用戶交互少但是有很多后臺程序比較適合。 -- 老生代還是新生代???

d).Serial old 收集器是Serial的老年代版本(使用標記-整理算法), Parralle Scavenge無法與CMS工作只能與Serial Old配合一起工作。這是parralle old收集器出現它采用的是標記-整理 算法

d).CMS收集器,為什么Cocurrent特性呢?仔細分析然來CMS收集器把收集過程分為4個子階段:
1).初始標記 -- 把GC Root對象找出來
2).并發標記 -- GC Roots tracing
3).重新標記 -- 把進行 并發標記 那一段時間,標記變動的那一部分重新標記,這個階段比初始標記時間長比并發標記時間短,但是必須停止用戶線程。
4).并發清除

初始標記、重新標記是需要”stop the world“,并發標記和并發清除這兩個階段還是可以垃圾回收線程與用戶線程一起執行。由于CMS是并發的執行收集工作,所以它是一個很耗資源的收集器,會使得用戶程度整體執行時間長(雖然瞬時停頓感少了)。

由于CMS收集器與用戶線程一起工作,所以不能像其他收集器比如par Old一樣等到老生代占到100%在啟用,一般等到60%就啟用CMS收集器,這里有一個參數配置-XXCMSInitiatingOccupancyFraction。平常我們可以提高這個參數來降低CMS啟動收集次數,但是如果比率過高,預留的空間不足用戶線程使用會出現“Cocurrent Mode Failure”,這樣的話性能反而會降低。

還有一個問題CMS容易產生碎片,如果需要進行碎片整理(無法并發)的話必須停止用戶線程。我們通過一個參數CMSFullGCBeforeCompaction來控制執行多少次無壓縮的收集然后執行一次帶壓縮的收集。

e).G1收集器
G1收集器在多核CPU等可以充分利用硬件環境,它是面向服務端的垃圾回收器。采用的是“標記-整理”的方式,所以沒有碎片的出現。
G1收集器的堆內存與其他收集器有很大的區別,它將整個堆劃分為很多region。G1能夠建立預測模型會跟蹤每個Region垃圾回收價值大?。ɑ厥账@得的空間大小和及回收所需時間的經驗值),然后選一個價值最高的region進行回收(這是Garbage-First 名稱的由來)。

堆被分成許多region也會帶來一個問題,如果一個region A被其他region B引用,當我回收A的時候我還要掃描整個堆,把引用A的regin掃描出來。所以G1采用空間換時間的辦法即提供一個rememerSet的數據結構來維護這些引用關系。

如何查看垃圾收集器日志

開啟GC日志:參數 - XX:+PrintGC(或者 - verbose:gc)開啟了簡單 GC 日志模式,
簡單GC日志不會打印具體使用的算法,比如下面日志:

[GC 246656K->243120K(376320K), 0.0929090 secs]
[Full GC 243120K->241951K(629760K), 1.5589690 secs]

第一行表示執行的MinGC 堆空間使用從246656K 到 243120K 堆的總大小為376320K,執行時間消耗0.0929090 secs。
第二行表示執行了FUll GC堆空間從243120K 到 241951K,執行時間消耗1.5589690 secs。
這里沒有描述執行GC的時間,數據有沒有從新生代轉到老生代,也沒有顯示具體使用的算法。

如果想要看詳細的GC日志,則-XX:PrintGCDetails,這時候會打印出詳細日志:
如下描述的是執行了一次新生代GC,新生代使用由142816K減少到10752K。堆的總容量大小由246648K減少到243136K。

[GC
    [PSYoungGen: 142816K->10752K(142848K)] 246648K->243136K(375296K), 0.0935090 secs
]
[Times: user=0.55 sys=0.10, real=0.09 secs]

對于FullGC 詳細日志如下:
打印了每一代的堆使用情況變化,其中PsYoungGen 9707 +ParOldGen 232244 = 堆的總使用大小 241951.

[Full GC
    [PSYoungGen: 10752K->9707K(142848K)]
    [ParOldGen: 232384K->232244K(485888K)] 243136K->241951K(628736K)
    [PSPermGen: 3162K->3161K(21504K)], 1.5265450 secs
]

如果想打印出時間參數可以使用- XX:+PrintGCTimeStamps 可以將時間和日期也加到 GC 日志中。
如果指定了 - XX:+PrintGCDateStamps,每一行就添加上了絕對的日期和時間。

2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0.0959470 secs]
2014-01-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0.1421720 secs]
2014-01-03T12:08:38.513-0100: [GC 246757K->243133K(375296K), 0.2761000 secs]

如果想輸出到文件 就加上 -Xloggc

寫完后的想法

對這塊知識,邊看書邊收集資料整理理解。
下一篇文章將通過代碼具體模擬觸發MinGC,及其通過GC 日志分析,查看JVM活動的細節。
這是我個人面對這個問題的邏輯推導不是粘貼別人的答案花費了我大半天的時間但是很值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容