《深入理解Java虛擬機》學習整理——內存篇

HostSpot虛擬機運行時內存

程序計數器——當前線程執行字節碼的行號指示器,如果執行Native方法,則計數器值為空,是唯一一個在Java虛擬機規范沒有規定OOM情況的區域。

Java虛擬機棧——Java方法執行的內存模型,每個方法以棧幀為單位,棧幀存儲方法的局部變量表、動態鏈接、方法出口。每個方法調用到執行完畢對應一個棧幀在Java虛擬機棧入棧到出棧。局部變量表除了基本類型外還存有對象引用類型,對象引用類型指向一個代表對象的句柄或與其對象相關的位置。

本地方法棧——在Sun HotSpot中本地方法棧與虛擬機棧合二為一,區別是Java虛擬機棧為執行Java方法提供服務,本地方法棧為執行Native方法提供服務。

Java堆——所有線程共享的一塊區域,用于存放對象實例,也是運行時內存中占用空間最大的一塊,也是垃圾回收的主要區域。Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。從內存回收的角度來看,由于現在收集器基本都采用分代收集算法,所以Java堆中還可以細分為:新生代和老年代;再細致一點的有Eden空間、From Survivor空間、To Survivor空間等。

方法區——存儲加載的類信息(版本號、字段、方法、接口)、常量池(如運行時常量池)、靜態變量、即時編譯器編譯后的代碼。

直接內存——不屬于Java虛擬機定義的范疇,Native方法創建的對象儲存在此區域,與本機總內存相關。




Java對象的創建

虛擬機遇到一條new指令,首先去方法區檢查指令的參數(類的類型)能否在常量池(方法區)定位到類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成后便可完全確定,為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。

內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭)。接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。



Java對象的內存布局

在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

HotSpot虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“Mark Word”。


對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,HotSpot虛擬機通過這個指針來確定這個對象是哪個類的實例。并不是所有的虛擬機實現都必須在對象數據上保留類型

指針。如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中卻無法確定數組的大小。

接下來的實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

第三部分對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

Java對象的訪問定位

目前主流的訪問方式有使用句柄和直接指針兩種。

如果使用句柄訪問的話,那么Java堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。


如果使用直接指針訪問,那么Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。


這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要修改。

使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項非??捎^的執行成本。就本書討論的主要虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的范圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。

如何判斷對象需要回收?

引用計數算法(不使用)

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

但是,至少主流的Java虛擬機里面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。如果兩個對象相互引用,除此之外再無任何引用執行這兩個對象將導致無法回收。

可達性分析算法(HotSpot虛擬機使用)

這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。


在Java語言中,可作為GC Roots的對象包括下面幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象。

方法區中類靜態屬性引用的對象。

方法區中常量引用的對象。

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

Java中的引用類型

在JDK 1.2以前,Java中的引用的定義很傳統:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表著一個引用。

在JDK 1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

強引用——類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用——在系統將要發生內存溢出異常之前,將會把弱引用對象進回收范圍之中進行第二次回收。

弱引用——當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。因此弱引用無法存活到下次GC之后。

虛引用——一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

finalize()方法

即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。

任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行。

需要特別說明的是,上面關于對象死亡時finalize()方法的描述可能帶有悲情的藝術色彩,筆者并不鼓勵大家使用這種方法來拯救對象。相反,筆者建議大家盡量避免使用它,因為它不是C/C++中的析構函數,而是Java剛誕生時為了使C/C++程序員更容易接受它所做出的一個妥協。它的運行代價高昂,不確定性大,無法保證各個對象的調用順序。有些教材中描述它適合做“關閉外部資源”之類的工作,這完全是對這個方法用途的一種自我安慰。

finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,所以筆

者建議大家完全可以忘掉Java語言中有這個方法的存在。

垃圾回收算法

標記-清除算法

如同它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。

它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

標記—清除算法的執行過程如圖所示。


復制算法

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


現在的商業虛擬機都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor[1]。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle romotion)。

標記-整理算法

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

分代收集算法

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

HotSpot的算法實現

枚舉根節點

在HotSpot的實現中,是使用一組稱為OopMap的數據結構來得知哪些地方存放著對象引用,在類加載完成的時候,HotSpot就把對象內什么偏移量上是什么類型的數據計算出來,在JIT編譯過程中,也

會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。

安全點

實際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄了這些信息,這些位置稱為安全點(Safepoint),即程序執行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致于讓GC等待時間太長,也不能過于頻繁以致于過分增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的——因為每條指令執行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執行”的最明顯特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令才會產生Safepoint。

對于Sefepoint,另一個需要考慮的問題是如何在GC發生時讓所有線程(這里不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension),其中搶先式中斷不需要線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。現在幾乎沒有虛擬機實現采用搶先式中斷來暫停線程從而響應GC事件。

而主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

安全區域

使用Safepoint似乎已經完美地解決了如何進入GC的問題,但實際情況卻并不一定。Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程序“不執行”的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處于Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。對于這種情況,就需要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴展了的Safepoint。

在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間里JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號為止。

垃圾收集器

如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。Java虛擬機規范中對垃圾收集器應該如何實現并沒有任何規定,因此不同的廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大差別。


如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機所處的區域,則表示它是屬于新生代收集器還是老年代收集器。

Serial收集器

Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛擬機新生代收集的唯一選擇。

這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。


實際上到現在為止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它也有著優于其他收集器的地方:簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的代碼。


ParNew收集器除了多線程收集之外,其他與Serial收集器相比并沒有太多創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。

ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。當然,隨著可以使用的CPU的數量的增加,它對于GC時系統資源的有效利用還是很有好處的。它默認開啟的收集線程數與CPU的數量相同,

并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。

并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行于另一個CPU上。

Parallel Scavenge收集器

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在于給Client模式下的虛擬機使用。如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用[1],另一種用途就是作為CMS收集器的后備預案,在并發收集發生Concurrent Mode Failure時使用。


Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK 1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(還記得上面說過Parallel Scavenge收集器無法與CMS收集器配合工作嗎?)。由于老年代Serial Old收集器在服務端應用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由于單線程的老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重

視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

CMS收集器是基于“標記—清除”算法實現的,它的運作過程相對于前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:

初始標記(CMS initial mark)

并發標記(CMS concurrent mark)

重新標記(CMS remark)

并發清除(CMS concurrent sweep)


其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,并發標記階段就是進行GC RootsTracing的過程,而重新標記階段則是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。

由于整個過程中耗時最長的并發標記和并發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。

CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:并發收集、低停頓,Sun公司的一些官方文檔中也稱之為并發低停頓收集器(Concurrent Low Pause Collector)。但是CMS還遠達不到完美的程度,它有以下3個明顯的缺點:

CMS收集器對CPU資源非常敏感。其實,面向并發設計的程序都對CPU資源比較敏感。在并發階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啟動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,并發回收時垃圾收集線程不少于25%的CPU資源,并且隨著CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了50%,其實也讓人無法接受。

CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由于CMS并發清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程序運作使用。

CMS是一款基于“標記—清除”算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩余,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次FullGC。

G1收集器

G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,早在JDK 1.7剛剛確立項目目標,Sun公司給出的JDK 1.7 RoadMap里面,它就被視為JDK 1.7中HotSpot虛擬機的一個重要進化特征。從JDK 6u14中開始就有Early Access版本的G1收集器供開發人員實驗、試用,由此開始G1收集器的“Experimental”狀態持續了數年時間,直至JDK 7u4,Sun公司才認為它達到足夠成熟的商用程度,移除了“Experimental”的標識。

G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK 1.5中發布的CMS收集器。與其他GC收集器相比,G1具備如下特點:

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

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

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

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

內存分配與回收策略

對象優先在Eden分配(新生代)

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

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

老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

大對象直接進入老年代

所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(筆者列出的例子中的byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來說就是一個壞消息(替Java虛擬機抱怨一句,比遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,寫程序的時候應當避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們(新生代復制算法導致空間整理)。

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

既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這點,虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中。

動態對象年齡判定

為了能更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。

下面解釋一下“冒險”是冒了什么風險,前面提到過,新生代使用復制收集算法,但為了內存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC后仍然存活的情況(最極端的情況就是內存回收后新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

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

推薦閱讀更多精彩內容