Java GC系列
Java的內存分配與回收全部由JVM垃圾回收進程自動完成。與C語言不同,Java開發者不需要自己編寫代碼實現垃圾回收。這是Java深受大家歡迎的眾多特性之一,能夠幫助程序員更好地編寫Java程序。
下面四篇教程是了解Java 垃圾回收(GC)的基礎:
- 垃圾回收簡介
- 圾回收是如何工作的?
- 垃圾回收的類別
- 垃圾回收監視和分析
這篇教程是系列第一部分。首先會解釋基本的術語,比如JDK、JVM、JRE和HotSpotVM。接着會介紹JVM結構和Java 堆內存結構。理解這些基礎對於理解后面的垃圾回收知識很重要。
Java關鍵術語
- JavaAPI:一系列幫助開發者創建Java應用程序的封裝好的庫。
- Java 開發工具包 (JDK):一系列工具幫助開發者創建Java應用程序。JDK包含工具編譯、運行、打包、分發和監視Java應用程序。
- Java 虛擬機(JVM):JVM是一個抽象的計算機結構。Java程序根據JVM的特性編寫。JVM針對特定於操作系統並且可以將Java指令翻譯成底層系統的指令並執行。JVM確保了Java的平台無關性。
- Java 運行環境(JRE):JRE包含JVM實現和Java API。
Java HotSpot 虛擬機
每種JVM實現可能采用不同的方法實現垃圾回收機制。在收購SUN之前,Oracle使用的是JRockit JVM,收購之后使用HotSpot JVM。目前Oracle擁有兩種JVM實現並且一段時間后兩個JVM實現會合二為一。
HotSpot JVM是目前Oracle SE平台標准核心組件的一部分。在這篇垃圾回收教程中,我們將會了解基於HotSpot虛擬機的垃圾回收原則。
JVM體系結構
下面圖片總結了JVM的關鍵組件。在JVM體系結構中,與垃圾回收相關的兩個主要組件是堆內存和垃圾回收器。堆內存是內存數據區,用來保存運行時的對象實例。垃圾回收器也會在這里操作。現在我們知道這些組件是如何在框架中工作的。
Java堆內存
我們有必要了解堆內存在JVM內存模型的角色。在運行時,Java的實例被存放在堆內存區域。當一個對象不再被引用時,滿足條件就會從堆內存移除。在垃圾回收進程中,這些對象將會從堆內存移除並且內存空間被回收。堆內存以下三個主要區域:
1.新生代(Young Generation)
- Eden空間(Eden space,任何實例都通過Eden空間進入運行時內存區域)
- S0 Survivor空間(S0 Survivor space,存在時間長的實例將會從Eden空間移動到S0 Survivor空間)
- S1 Survivor空間 (存在時間更長的實例將會從S0 Survivor空間移動到S1 Survivor空間)
2.老年代(Old Generation)實例將從S1提升到Tenured(終身代)
3.永久代(Permanent Generation)包含類、方法等細節的元信息
永久代空間在Java SE8特性中已經被移除。
在本系列的第二篇將會介紹Java垃圾回收是如何工作的。
本教程是為了理解基本的Java垃圾回收以及它是如何工作的。這是垃圾回收教程系列的第二部分。希望你已經讀過了第一部分:《Java 垃圾回收介紹》。
Java 垃圾回收是一項自動化的過程,用來管理程序所使用的運行時內存。通過這一自動化過程,JVM 解除了程序員在程序中分配和釋放內存資源的開銷。
啟動Java垃圾回收
作為一個自動的過程,程序員不需要在代碼中顯示地啟動垃圾回收過程。System.gc()和Runtime.gc()用來請求JVM啟動垃圾回收。
雖然這個請求機制提供給程序員一個啟動 GC 過程的機會,但是啟動由 JVM負責。JVM可以拒絕這個請求,所以並不保證這些調用都將執行垃圾回收。啟動時機的選擇由JVM決定,並且取決於堆內存中Eden區是否可用。JVM將這個選擇留給了Java規范的實現,不同實現具體使用的算法不盡相同。
毋庸置疑,我們知道垃圾回收過程是不能被強制執行的。我剛剛發現了一個調用System.gc()有意義的場景。通過這篇文章了解一下適合調用System.gc() 這種極端情況。
Java垃圾回收過程
垃圾回收是一種回收無用內存空間並使其對未來實例可用的過程。
首頁所有文章資訊Web架構基礎技術書籍教程Java小組工具資源
Java GC系列(2):Java垃圾回收是如何工作的?
2014/10/28 | 分類: 基礎技術, 教程 | 3 條評論 | 標簽: GC, 垃圾回收教程
分享到: 37
本文由 ImportNew - 伍翀 翻譯自 javapapers。歡迎加入翻譯小組。轉載請見文末要求。
目錄
- 垃圾回收介紹
- 垃圾回收是如何工作的?
- 垃圾回收的類別
- 垃圾回收監視和分析
本教程是為了理解基本的Java垃圾回收以及它是如何工作的。這是垃圾回收教程系列的第二部分。希望你已經讀過了第一部分:《Java 垃圾回收介紹》。
Java 垃圾回收是一項自動化的過程,用來管理程序所使用的運行時內存。通過這一自動化過程,JVM 解除了程序員在程序中分配和釋放內存資源的開銷。
啟動Java垃圾回收
作為一個自動的過程,程序員不需要在代碼中顯示地啟動垃圾回收過程。System.gc()和Runtime.gc()用來請求JVM啟動垃圾回收。
雖然這個請求機制提供給程序員一個啟動 GC 過程的機會,但是啟動由 JVM負責。JVM可以拒絕這個請求,所以並不保證這些調用都將執行垃圾回收。啟動時機的選擇由JVM決定,並且取決於堆內存中Eden區是否可用。JVM將這個選擇留給了Java規范的實現,不同實現具體使用的算法不盡相同。
毋庸置疑,我們知道垃圾回收過程是不能被強制執行的。我剛剛發現了一個調用System.gc()有意義的場景。通過這篇文章了解一下適合調用System.gc() 這種極端情況。
Java垃圾回收過程
垃圾回收是一種回收無用內存空間並使其對未來實例可用的過程。
Eden 區:當一個實例被創建了,首先會被存儲在堆內存年輕代的 Eden 區中。
注意:如果你不能理解這些詞匯,我建議你閱讀這篇 垃圾回收介紹 ,這篇教程詳細地介紹了內存模型、JVM 架構以及這些術語。
Survivor 區(S0 和 S1):作為年輕代 GC(Minor GC)周期的一部分,存活的對象(仍然被引用的)從 Eden 區被移動到 Survivor 區的 S0 中。類似的,垃圾回收器會掃描 S0 然后將存活的實例移動到 S1 中。
(譯注:此處不應該是Eden和S0中存活的都移到S1么,為什么會先移到S0再從S0移到S1?)
死亡的實例(不再被引用)被標記為垃圾回收。根據垃圾回收器(有四種常用的垃圾回收器,將在下一教程中介紹它們)選擇的不同,要么被標記的實例都會不停地從內存中移除,要么回收過程會在一個單獨的進程中完成。
老年代: 老年代(Old or tenured generation)是堆內存中的第二塊邏輯區。當垃圾回收器執行 Minor GC 周期時,在 S1 Survivor 區中的存活實例將會被晉升到老年代,而未被引用的對象被標記為回收。
老年代 GC(Major GC):相對於 Java 垃圾回收過程,老年代是實例生命周期的最后階段。Major GC 掃描老年代的垃圾回收過程。如果實例不再被引用,那么它們會被標記為回收,否則它們會繼續留在老年代中。
內存碎片:一旦實例從堆內存中被刪除,其位置就會變空並且可用於未來實例的分配。這些空出的空間將會使整個內存區域碎片化。為了實例的快速分配,需要進行碎片整理。基於垃圾回收器的不同選擇,回收的內存區域要么被不停地被整理,要么在一個單獨的GC進程中完成。
垃圾回收中實例的終結
在釋放一個實例和回收內存空間之前,Java 垃圾回收器會調用實例各自的 finalize() 方法,從而該實例有機會釋放所持有的資源。雖然可以保證 finalize() 會在回收內存空間之前被調用,但是沒有指定的順序和時間。多個實例間的順序是無法被預知,甚至可能會並行發生。程序不應該預先調整實例之間的順序並使用 finalize() 方法回收資源。
- 任何在 finalize過程中未被捕獲的異常會自動被忽略,然后該實例的 finalize 過程被取消。
- JVM 規范中並沒有討論關於弱引用的垃圾回收機制,也沒有很明確的要求。具體的實現都由實現方決定。
- 垃圾回收是由一個守護線程完成的。
對象什么時候符合垃圾回收的條件?
- 所有實例都沒有活動線程訪問。
- 沒有被其他任何實例訪問的循環引用實例。
Java 中有不同的引用類型。判斷實例是否符合垃圾收集的條件都依賴於它的引用類型。
引用類型 | 垃圾收集 |
---|---|
強引用(Strong Reference) | 不符合垃圾收集 |
軟引用(Soft Reference) | 垃圾收集可能會執行,但會作為最后的選擇 |
弱引用(Weak Reference) | 符合垃圾收集 |
虛引用(Phantom Reference) | 符合垃圾收集 |
在編譯過程中作為一種優化技術,Java 編譯器能選擇給實例賦 null 值,從而標記實例為可回收。
class Animal {
public static void main(String[] args) {
Animal lion = new Animal();
System.out.println("Main is completed.");
}
protected void finalize() {
System.out.println("Rest in Peace!");
}
}
在上面的類中,lion 對象在實例化行后從未被使用過。因此 Java 編譯器作為一種優化措施可以直接在實例化行后賦值lion = null。因此,即使在 SOP 輸出之前, finalize 函數也能夠打印出 'Rest in Peace!'。我們不能證明這確定會發生,因為它依賴JVM的實現方式和運行時使用的內存。然而,我們還能學習到一點:如果編譯器看到該實例在未來再也不會被引用,能夠選擇並提早釋放實例空間。
- 關於對象什么時候符合垃圾回收有一個更好的例子。實例的所有屬性能被存儲在寄存器中,隨后寄存器將被訪問並讀取內容。無一例外,這些值將被寫回到實例中。雖然這些值在將來能被使用,這個實例仍然能被標記為符合垃圾回收。這是一個很經典的例子,不是嗎?
- 當被賦值為null時,這是很簡單的一個符合垃圾回收的示例。當然,復雜的情況可以像上面的幾點。這是由 JVM 實現者所做的選擇。目的是留下盡可能小的內存占用,加快響應速度,提高吞吐量。為了實現這一目標, JVM 的實現者可以選擇一個更好的方案或算法在垃圾回收過程中回收內存空間。
- 當 finalize() 方法被調用時,JVM 會釋放該線程上的所有同步鎖。
GC Scope 示例程序
Class GCScope {
GCScope t;
static int i = 1;
public static void main(String args[]) {
GCScope t1 = new GCScope();
GCScope t2 = new GCScope();
GCScope t3 = new GCScope();
// No Object Is Eligible for GC
t1.t = t2; // No Object Is Eligible for GC
t2.t = t3; // No Object Is Eligible for GC
t3.t = t1; // No Object Is Eligible for GC
t1 = null;
// No Object Is Eligible for GC (t3.t still has a reference to t1)
t2 = null;
// No Object Is Eligible for GC (t3.t.t still has a reference to t2)
t3 = null;
// All the 3 Object Is Eligible for GC (None of them have a reference.
// only the variable t of the objects are referring each other in a
// rounded fashion forming the Island of objects with out any external
// reference)
}
protected void finalize() {
System.out.println("Garbage collected from object" + i);
i++;
}
class GCScope {
GCScope t;
static int i = 1;
public static void main(String args[]) {
GCScope t1 = new GCScope();
GCScope t2 = new GCScope();
GCScope t3 = new GCScope();
// 沒有對象符合GC
t1.t = t2; // 沒有對象符合GC
t2.t = t3; // 沒有對象符合GC
t3.t = t1; // 沒有對象符合GC
t1 = null;
// 沒有對象符合GC (t3.t 仍然有一個到 t1 的引用)
t2 = null;
// 沒有對象符合GC (t3.t.t 仍然有一個到 t2 的引用)
t3 = null;
// 所有三個對象都符合GC (它們中沒有一個擁有引用。
// 只有各對象的變量 t 還指向了彼此,
// 形成了一個由對象組成的環形的島,而沒有任何外部的引用。)
}
protected void finalize() {
System.out.println("Garbage collected from object" + i);
i++;
}
GC OutOfMemoryError 的示例程序
GC並不保證內存溢出問題的安全性,粗心寫下的代碼會導致 OutOfMemoryError。
import java.util.LinkedList;
import java.util.List;
public class GC {
public static void main(String[] main) {
List l = new LinkedList();
// Enter infinite loop which will add a String to the list: l on each
// iteration.
do {
l.add(new String("Hello, World"));
} while (true);
}
}
輸出:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.LinkedList.linkLast(LinkedList.java:142)
at java.util.LinkedList.add(LinkedList.java:338)
at com.javapapers.java.GCScope.main(GCScope.java:12)
Java系列筆記(3) - Java 內存區域和GC機制
本部分來自Java系列筆記(3) - Java 內存區域和GC機制
目錄
- Java垃圾回收概況
- Java內存區域
- Java對象的訪問方式
- Java內存分配機制
- Java GC機制
- 垃圾收集器
Java垃圾回收概況
Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,作為Java開發者,一般不需要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題,也不需要像C程序員那樣戰戰兢兢。這是因為在Java虛擬機中,存在自動內存管理和垃圾清掃機制。概括地說,該機制對JVM(Java Virtual Machine)中的內存進行標記,並確定哪些內存需要回收,根據一定的回收策略,自動的回收內存,永不停息(Nerver Stop)的保證JVM中的內存空間,防止出現內存泄露和溢出問題。
關於JVM,需要說明一下的是,目前使用最多的Sun公司的JDK中,自從1999年的JDK1.2開始直至現在仍在廣泛使用的JDK6,其中默認的虛擬機都是HotSpot。2009年,Oracle收購Sun,加上之前收購的EBA公司,Oracle擁有3大虛擬機中的兩個:JRockit和HotSpot,Oracle也表明了想要整合兩大虛擬機的意圖,但是目前在新發布的JDK7中,默認的虛擬機仍然是HotSpot,因此本文中默認介紹的虛擬機都是HotSpot,相關機制也主要是指HotSpot的GC機制。
Java GC機制主要完成3件事:確定哪些內存需要回收,確定什么時候需要執行GC,如何執行GC。經過這么長時間的發展(事實上,在Java語言出現之前,就有GC機制的存在,如Lisp語言),Java GC機制已經日臻完善,幾乎可以自動的為我們做絕大多數的事情。然而,如果我們從事較大型的應用軟件開發,曾經出現過內存優化的需求,就必定要研究Java GC機制。
學習Java GC機制,可以幫助我們在日常工作中排查各種內存溢出或泄露問題,解決性能瓶頸,達到更高的並發量,寫出更高效的程序。
我們將從4個方面學習Java GC機制,1,內存是如何分配的;2,如何保證內存不被錯誤回收(即:哪些內存需要回收);3,在什么情況下執行GC以及執行GC的方式;4,如何監控和優化GC機制。
Java內存區域
了解Java GC機制,必須先清楚在JVM中內存區域的划分。在Java運行時的數據區里,由JVM管理的內存區域分為下圖幾個模塊:
其中:
1,程序計數器(Program Counter Register):程序計數器是一個比較小的內存區域,用於指示當前線程所執行的字節碼執行到了第幾行,可以理解為是當前線程的行號指示器。字節碼解釋器在工作時,會通過改變這個計數器的值來取下一條語句指令。
每個程序計數器只用來記錄一個線程的行號,所以它是線程私有(一個線程就有一個程序計數器)的。
如果程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值為Undefined,由於程序計數器只是記錄當前指令地址,所以不存在內存溢出的情況,因此,程序計數器也是所有JVM內存區域中唯一一個沒有定義OutOfMemoryError的區域。
2,虛擬機棧(JVM Stack):一個線程的每個方法在執行的同時,都會創建一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操作站、動態鏈接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。
局部變量表中存儲着方法的相關局部變量,包括各種基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會占用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內都不會改變。
虛擬機棧中定義了兩種異常,如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多數Java虛擬機都允許動態擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內存不足,此時,會拋出OutOfMemoryError(內存溢出)。
每個線程對應着一個虛擬機棧,因此虛擬機棧也是線程私有的。
3,本地方法棧(Native Method Statck):本地方法棧在作用,運行機制,異常類型等方面都與虛擬機棧相同,唯一的區別是:虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一起使用。
本地方法棧也是線程私有的。
4,堆區(Heap):堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的內存中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要內存區域,堆區由所有線程共享,在虛擬機啟動時創建。堆區的存在是為了存儲對象實例,原則上講,所有的對象都在堆區上分配內存(不過現代技術里,也不是這么絕對的,也有棧上直接分配的)。
一般的,根據Java虛擬機規范規定,堆內存需要在邏輯上是連續的(在物理上不需要),在實現時,可以是固定大小的,也可以是可擴展的,目前主流的虛擬機都是可擴展的。如果在執行垃圾回收之后,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java heap space異常。
關於堆區的內容還有很多,將在下節“Java內存分配機制”中詳細介紹。
5,方法區(Method Area):在Java虛擬機規范中,將方法區作為堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者將方法區定義為“永久代”,這是因為,對於之前的HotSpot Java虛擬機的實現方式中,將分代收集的思想擴展到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機,並不將方法區當做永久代,HotSpot本身,也計划取消永久代。本文中,由於筆者主要使用Oracle JDK6.0,因此仍將使用永久代一詞。
方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。
方法區在物理上也不需要是連續的,可以選擇固定大小或可擴展大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表着在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。
在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以后進一步深入研究時使用。
在方法區上定義了OutOfMemoryError:PermGen space異常,在內存不足時拋出。
運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類鏈接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字符“abc”已經在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,並返回地址)。
6,直接內存(Direct Memory):直接內存並不是JVM管理的內存,可以這樣理解,直接內存,就是JVM以外的機器內存,比如,你有4G的內存,JVM占用了1G,則其余的3G就是直接內存,JDK中有一種基於通道(Channel)和緩沖區(Buffer)的內存分配方式,將由C語言實現的native函數庫分配在直接內存中,用存儲在JVM堆中的DirectByteBuffer來引用。由於直接內存收到本機器內存的限制,所以也可能出現OutOfMemoryError的異常。
Java對象的訪問方式
一般來說,一個Java的引用訪問涉及到3個內存區域:JVM棧,堆,方法區。
以最簡單的本地變量引用:Object obj = new Object()為例:
Object obj表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數據;
new Object()作為實例對象數據存儲在堆中;
堆中還記錄了Object類的類型信息(接口、方法、field、對象類型等)的地址,這些地址所執行的數據存儲在方法區中;
在Java虛擬機規范中,對於通過reference類型引用訪問具體對象的方式並未做規定,目前主流的實現方式主要有兩種:
1,通過句柄訪問(圖來自於《深入理解Java虛擬機:JVM高級特效與最佳實現》):
通過句柄訪問的實現方式中,JVM堆中會專門有一塊區域用來作為句柄池,存儲相關句柄所執行的實例數據地址(包括在堆中地址和在方法區中的地址)。這種實現方法由於用句柄表示地址,因此十分穩定。
2,通過直接指針訪問:(圖來自於《深入理解Java虛擬機:JVM高級特效與最佳實現》)
通過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區中的相應類型數據。這種方法最大的優勢是速度快,在HotSpot虛擬機中用的就是這種方式。
Java內存分配機制
這里所說的內存分配,主要指的是在堆上的分配,一般的,對象的內存分配都是在堆上進行,但現代技術也支持將對象拆成標量類型(標量類型即原子類型,表示單個值,可以是基本類型或String等),然后在棧上分配,在棧上分配的很少見,我們這里不考慮。
Java內存分配和回收的機制概括的說,就是:分代分配,分代回收。對象將根據存活的時間被分為:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)。如下圖(來源於《成為JavaGC專家part I》,http://www.importnew.com/1993.html):
年輕代(Young Generation):對象被創建時,內存的分配首先發生在年輕代(大對象可以直接被創建在年老代),大部分的對象在創建后很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱為Minor GC或叫Young GC。注意,Minor GC並不代表年輕代內存不足,它事實上只表示在Eden區上的GC。
年輕代上的內存分配是這樣的,年輕代可以分為3個區域:Eden區(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示內存首次分配的區域,再貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。內存分配過程為(來源於《成為JavaGC專家part I》,http://www.importnew.com/1993.html):
- 絕大多數剛創建的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,因此在其上分配內存極快;
- 最初一次,當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩余的對象復制到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
- 下次Eden區滿了,再執行一次Minor GC,將消亡的對象清理掉,將存活的對象復制到Survivor1中,然后清空Eden區;
- 將Survivor0中消亡的對象清理掉,將其中可以晉級的對象晉級到Old區,將存活的對象也復制到Survivor1區,然后清空Survivor0區;
- 當兩個存活區切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代,但這只是個最大值,並不代表一定是這個值)之后,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被復制到老年代。
從上面的過程可以看出,Eden區是連續的空間,且Survivor總有一個為空。經過一次GC和復制,一個Survivor中保存着當前還活着的對象,而Eden區和另一個Survivor區的內容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的“停止-復制(Stop-and-copy)”清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另一個Survivor中),這不代表着停止復制清理法很高效,其實,它也只在這種情況下高效,如果在老年代采用停止復制,則挺悲劇的。
在Eden區,HotSpot虛擬機使用了兩種技術來加快內存分配。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術的做法分別是:由於Eden區是連續的,因此bump-the-pointer技術的核心就是跟蹤最后創建的一個對象,在對象創建時,只需要檢查最后一個對象后面是否有足夠的內存即可,從而大大加快內存分配速度;而對於TLAB技術是對於多線程而言的,將Eden區分為若干段,每個線程使用獨立的一段,避免相互影響。TLAB結合bump-the-pointer技術,將保證每個線程都使用Eden區的一段,並快速的分配內存。
年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC后存活了下來),則會被復制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC。
可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否采用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。
如果對象比較大(比如長字符串或大數組),Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發提前GC,應少用,更應避免使用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。
可能存在年老代對象引用新生代對象的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這里。Young GC時,只要查這里即可,不用再去查全部老年代,因此性能大大提高。
Java GC機制
GC機制的基本算法是:分代收集,這個不用贅述。下面闡述每個分代的收集方法。
年輕代:
事實上,在上一節,已經介紹了新生代的主要垃圾回收方法,在新生代中,使用“停止-復制”算法進行清理,將新生代內存分為2部分,1部分 Eden區較大,1部分Survivor比較小,並被划分為兩個等量的部分。每次進行清理時,將Eden區和一個Survivor中仍然存活的對象拷貝到 另一個Survivor中,然后清理掉Eden和剛才的Survivor。
這里也可以發現,停止復制算法中,用來復制的兩部分並不總是相等的(傳統的停止復制算法兩部分內存相等,但新生代中使用1個大的Eden區和2個小的Survivor區來避免這個問題)
由於絕大部分的對象都是短命的,甚至存活不到Survivor中,所以,Eden區與Survivor的比例較大,HotSpot默認是 8:1,即分別占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的內存超過了10%,則需要將一部分對象分配到 老年代。用-XX:SurvivorRatio參數來配置Eden區域Survivor區的容量比值,默認是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
老年代存儲的對象比年輕代多得多,而且不乏大對象,對老年代進行內存清理時,如果使用停止-復制算法,則相當低效。一般,老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內存的連續。
在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩余空間大小,如果大於,則直接觸發一次Full GC,否則,就查看是否設置了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍內存分配失敗;如果不允許,則仍然進行Full GC(這代表着如果設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)。
方法區(永久代):
永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:
- 類的所有實例都已經被回收
- 加載類的ClassLoader已經被回收
- 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)
永久代的回收並不是必須的,可以通過參數來設置是否對類進行回收。HotSpot提供-Xnoclassgc進行控制
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類加載和卸載信息
-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持
垃圾收集器
在GC機制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具體實現,Java虛擬機規范中對於垃圾收集器沒有任何規定,所以不同廠商實現的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下圖(圖來源於《深入理解Java虛擬機:JVM高級特效與最佳實現》,圖中兩個收集器之間有連線,說明它們可以配合使用):
在介紹垃圾收集器之前,需要明確一點,就是在新生代采用的停止復制算法中,“停 止(Stop-the-world)”的意義是在回收內存時,需要暫停其他所 有線程的執行。這個是很低效的,現在的各種新生代收集器越來越優化這一點,但仍然只是將停止的時間變短,並未徹底取消停止。
- Serial收集器:新生代收集器,使用停止復制算法,使用一個線程進行GC,串行,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)
- ParNew收集器:新生代收集器,使用停止復制算法,Serial收集器的多線程版,用多個線程進行GC,並行,其它工作線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。
- Parallel Scavenge 收集器:新生代收集器,使用停止復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鍾,其中運行用戶代碼99分鍾,垃 圾收集1分鍾,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行后台運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。使用-XX:+UseParallelGC開關控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間占總時間的比例,默認99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效),用開關參數-XX:+UseAdaptiveSizePolicy可以進行動態控制,如自動調整Eden/Survivor比例,老年代對象年齡,新生代大小等,這個參數在ParNew下沒有。
- Serial Old收集器:老年代收集器,單線程收集器,串行,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存的對象,壓縮是將移動對象,將空間填滿保證內存分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行標記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
- Parallel Old收集器:老年代收集器,多線程,並行,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復制到預先准備好的區域,而不是像Sweep(清理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力於獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標記清除算法,多線程,優點是並發收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內存回收,優先使用ParNew+CMS(原因見后面),當用戶線程內存不足時,采用備用方案Serial Old收集。
G1收集器:在JDK1.7中正式發布,與現狀的新生代、老年代概念有很大不同,目前使用較少,不做介紹。
注意並發(Concurrent)和並行(Parallel)的區別:
- 並發是指用戶線程與GC線程同時執行(不一定是並行,可能交替,但總體上是在同時執行的),不需要停頓用戶線程(其實在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個CPU上執行);
- 並行收集是指多個GC線程並行工作,但此時用戶線程是暫停的;
所以,Serial是串行的,Parallel收集器是並行的,而CMS收集器是並發的.
關於JVM參數配置和內存調優實例,見我的下一篇博客(編寫中:Java系列筆記(4) - JVM監控與調優),本來想寫在同一篇博客里的,無奈內容太多,只好另起一篇。
說明:
本文是Java系列筆記的第3篇,這篇文章寫了很久,主要是Java內存和GC機制相對復雜,難以理解,加上本人這段時間項目和生活中耗費的時間很多,所以進度緩慢。文中大多數筆記內容來源於我在網絡上查到的博客和《深入理解Java虛擬機:JVM高級特效與最佳實現》一書。
本人能力有限,如果有錯漏,請留言指正。
參考資料:
- 《JAVA編程思想》,第5章;
- 《Java深度歷險》,Java垃圾回收機制與引用類型;
- 《深入理解Java虛擬機:JVM高級特效與最佳實現》,第2-3章;
- 成為JavaGC專家Part II — 如何監控Java垃圾回收機制, http://www.importnew.com/2057.html
- JDK5.0垃圾收集優化之--Don't Pause,http://calvin.iteye.com/blog/91905
- 【原】java內存區域理解-初步了解,http://iamzhongyong.iteye.com/blog/1333100
- 關於施用full gc頻繁的分析及解決:http://www.07net01.com/zhishi/383213.html
面試題:“你能不能談談,java GC是在什么時候,對什么東西,做了什么事情?”
本部分來自面試題:“你能不能談談,java GC是在什么時候,對什么東西,做了什么事情?”
面試題目:
地球人都知道,Java有個東西叫垃圾收集器,它讓創建的對象不需要像c/cpp那樣delete、free掉,你能不能談談,GC是在什么時候,對什么東西,做了什么事情?
一.回答:什么時候?
1.系統空閑的時候。
分析:這種回答大約占30%,遇到的話一般我就會准備轉向別的話題,譬如算法、譬如SSH看看能否發掘一些他擅長的其他方面。
2.系統自身決定,不可預測的時間/調用System.gc()的時候。
分析:這種回答大約占55%,大部分應屆生都能回答到這個答案,起碼不能算錯誤是吧,后續應當細分一下到底是語言表述導致答案太籠統,還是本身就只有這樣一個模糊的認識。
3.能說出新生代、老年代結構,能提出minor gc/full gc
分析:到了這個層次,基本上能說對GC運作有概念上的了解,譬如看過《深入JVM虛擬機》之類的。這部分不足10%。
4.能說明minor gc/full gc的觸發條件、OOM的觸發條件,降低GC的調優的策略。
分析:列舉一些我期望的回答:eden滿了minor gc,升到老年代的對象大於老年代剩余空間full
gc,或者小於時被HandlePromotionFailure參數強制full
gc;gc與非gc時間耗時超過了GCTimeRatio的限制引發OOM,調優諸如通過NewRatio控制新生代老年代比例,通過
MaxTenuringThreshold控制進入老年前生存次數等……能回答道這個階段就會給我帶來比較高的期望了,當然面試的時候正常人都不會記得每個參數的拼寫,我自己寫這段話的時候也是翻過手冊的。回答道這部分的小於2%。
總結:程序員不能具體控制時間,系統在不可預測的時間調用System.gc()函數的時候;當然可以通過調優,用NewRatio控制newObject和oldObject的比例,用MaxTenuringThreshold 控制進入oldObject的次數,使得oldObject 存儲空間延遲達到full gc,從而使得計時器引發gc時間延遲OOM的時間延遲,以延長對象生存期。
二.回答:對什么東西?
1.不使用的對象。
分析:相當於沒有回答,問題就是在問什么對象才是“不使用的對象”。大約占30%。
2.超出作用域的對象/引用計數為空的對象。
分析:這2個回答站了60%,相當高的比例,估計學校教java的時候老師就是這樣教的。第一個回答沒有解決我的疑問,gc到底怎么判斷哪些對象在不在作用域的?至於引用計數來判斷對象是否可收集的,我可以會補充一個下面這個例子讓面試者分析一下obj1、obj2是否會被GC掉?
class C{
public Object x;
}
C obj1、obj2 = new C();
obj1.x = obj2;
obj2.x = obj1;
obj1、obj2 = null;
3.從gc root開始搜索,搜索不到的對象。
分析:根對象查找、標記已經算是不錯了,小於5%的人可以回答道這步,估計是引用計數的方式太“深入民心”了。基本可以得到這個問題全部分數。
PS:有面試者在這個問補充強引用、弱引用、軟引用、幻影引用區別等,不是我想問的答案,但可以加分。
4.從root搜索不到,而且經過第一次標記、清理后,仍然沒有復活的對象。
分析:我期待的答案。但是的確很少面試者會回答到這一點,所以在我心中回答道第3點我就給全部分數。
總結:超出了作用域或引用計數為空的對象;從gc root開始搜索找不到的對象,而且經過一次標記、清理,仍然沒有復活的對象。
三.回答:做什么?
1.刪除不使用的對象,騰出內存空間。
分析:同問題2第一點。40%。
2.補充一些諸如停止其他線程執行、運行finalize等的說明。
分析:起碼把問題具體化了一些,如果像答案1那樣我很難在回答中找到話題繼續展開,大約占40%的人。
補充一點題外話,面試時我最怕遇到的回答就是“這個問題我說不上來,但是遇到的時候我上網搜一下能做出來”。做程序開發確實不是去鍛煉茴香豆的“茴”有幾種寫法,不死記硬背我同意,我不會糾語法、單詞,但是多少你說個思路呀,要直接回答一個上網搜,我完全沒辦法從中獲取可以評價應聘者的信息,也很難從回答中繼續發掘話題展開討論。建議大家盡量回答引向自己熟悉的,可討論的領域,展現給面試官最擅長的一面。
3.能說出諸如新生代做的是復制清理、from survivor、to survivor是干啥用的、老年代做的是標記清理、標記清理后碎片要不要整理、復制清理和標記清理有有什么優劣勢等。
分析:也是看過《深入JVM虛擬機》的基本都能回答道這個程度,其實到這個程度我已經比較期待了。同樣小於10%。
4.除了3外,還能講清楚串行、並行(整理/不整理碎片)、CMS等搜集器可作用的年代、特點、優劣勢,並且能說明控制/調整收集器選擇的方式。
分析:同上面2個問題的第四點。
總結:刪除不使用的對象,回收內存空間;運行默認的finalize,當然程序員想立刻調用就用dipose調用以釋放資源如文件句柄,JVM用from survivor、to survivor對它進行標記清理,對象序列化后也可以使它復活。
千萬不要說網上google下,就算說也要說出自己以前遇到這樣的問題是怎么處理的,對這個知識有什么認識想法,然后可以反問下考官,這樣可以不讓技術型的考官為如何繼續話題而對你無語,呵呵。