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的實例被存放在堆內存區域。當一個對象不再被引用時,滿足條件就會從堆內存移除。在垃圾回收進程中,這些對象將會從堆內存移除並且內存空間被回收。堆內存以下三個主要區域:
- 新生代(Young Generation)
- Eden空間(Eden space,任何實例都通過Eden空間進入運行時內存區域)
- S0 Survivor空間(S0 Survivor space,存在時間長的實例將會從Eden空間移動到S0 Survivor空間)
- S1 Survivor空間 (存在時間更長的實例將會從S0 Survivor空間移動到S1 Survivor空間)
- 老年代(Old Generation)實例將從S1提升到Tenured(終身代)
- 永久代(Permanent Generation)包含類、方法等細節的元信息
永久代空間在Java SE8特性中已經被移除。
目錄
本教程是為了理解基本的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
值,從而標記實例為可回收。
1
2
3
4
5
6
7
8
9
10
|
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 示例程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
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
。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
);
}
}
|
輸出:
1
2
3
4
|
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垃圾收集器。
在這篇教程中我們將學習幾種現有的垃圾回收器。在Java中,垃圾回收是一個自動的進程可以替代程序員進行內存的分配與回收這些復雜的工作。這篇是垃圾回 收教程系列的第三篇,在前面的第2部分我們看到了在Java中垃圾回收是如何工作的,那是篇有意思的文章,我推薦你去看一下。第一部分介紹了Java的垃 圾回收,主要有JVM體系結構,堆內存模型和一些Java術語。
Java有四種類型的垃圾回收器:
- 串行垃圾回收器(Serial Garbage Collector)
- 並行垃圾回收器(Parallel Garbage Collector)
- 並發標記掃描垃圾回收器(CMS Garbage Collector)
- G1垃圾回收器(G1 Garbage Collector)
每種類型都有自己的優勢與劣勢。重要的是,我們編程的時候可以通過JVM選擇垃圾回收器類型。我們通過向JVM傳遞參數進行選擇。每種類型在很大程度上有 所不同並且可以為我們提供完全不同的應用程序性能。理解每種類型的垃圾回收器並且根據應用程序選擇進行正確的選擇是非常重要的。
1、串行垃圾回收器
串行垃圾回收器通過持有應用程序所有的線程進行工作。它為單線程環境設計,只使用一個單獨的線程進行垃圾回收,通過凍結所有應用程序線程進行工作,所以可能不適合服務器環境。它最適合的是簡單的命令行程序。
通過JVM參數-XX:+UseSerialGC
可以使用串行垃圾回收器。
2、並行垃圾回收器
並行垃圾回收器也叫做 throughput collector 。它是JVM的默認垃圾回收器。與串行垃圾回收器不同,它使用多線程進行垃圾回收。相似的是,它也會凍結所有的應用程序線程當執行垃圾回收的時候
3、並發標記掃描垃圾回收器
並發標記垃圾回收使用多線程掃描堆內存,標記需要清理的實例並且清理被標記過的實例。並發標記垃圾回收器只會在下面兩種情況持有應用程序所有線程。
- 當標記的引用對象在tenured區域;
- 在進行垃圾回收的時候,堆內存的數據被並發的改變。
相比並行垃圾回收器,並發標記掃描垃圾回收器使用更多的CPU來確保程序的吞吐量。如果我們可以為了更好的程序性能分配更多的CPU,那么並發標記上掃描垃圾回收器是更好的選擇相比並發垃圾回收器。
通過JVM參數 XX:+USeParNewGC
打開並發標記掃描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器適用於堆內存很大的情況,他將堆內存分割成不同的區域,並且並發的對其進行垃圾回收。G1也可以在回收內存之后對剩余的堆內存空間進行壓縮。並發掃描標記垃圾回收器在STW情況下壓縮內存。G1垃圾回收會優先選擇第一塊垃圾最多的區域
通過JVM參數 –XX:+UseG1GC
使用G1垃圾回收器
Java 8 的新特性
在使用G1垃圾回收器的時候,通過 JVM參數 -XX:+UseStringDeduplication
。 我們可以通過刪除重復的字符串,只保留一個char[]來優化堆內存。這個選擇在Java 8 u 20被引入。
我們給出了全部的四種Java垃圾回收器,需要根據應用場景,硬件性能和吞吐量需求來決定使用哪一種。
垃圾回收的JVM配置
下面的JVM關鍵配置都與Java垃圾回收有關。
運行的垃圾回收器類型
配置 | 描述 |
---|---|
-XX:+UseSerialGC | 串行垃圾回收器 |
-XX:+UseParallelGC | 並行垃圾回收器 |
-XX:+UseConcMarkSweepGC | 並發標記掃描垃圾回收器 |
-XX:ParallelCMSThreads= | 並發標記掃描垃圾回收器 =為使用的線程數量 |
-XX:+UseG1GC | G1垃圾回收器 |
GC的優化配置
配置 | 描述 |
---|---|
-Xms | 初始化堆內存大小 |
-Xmx | 堆內存最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
使用JVM GC參數的例子
1
|
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
|
在Java垃圾回收教程的下一部分,我們將會用一個Java程序演示如何監視和分析垃圾回收。
目錄
在這個Java GC系列教程中,讓我們學習用於垃圾回收監視和分析的工具。然后,選用一種工具來監視一個Java示例程序的垃圾回收過程。如果你是一名初學者,你最好仔細閱讀該系列教程。你可以從這里(垃圾回收介紹)開始。
Java GC監視和分析工具
下面是一些可用的工具,每個都有自己的優勢和缺點。我們可以通過選擇正確的工具並分析,來提升應用程序的性能。這篇教程中,我們選用Java VisualVM。
- Java VisualVM
- Naarad
- GCViewer
- IBM Pattern Modeling and Analysis Tool for Java Garbage Collector
- HPjmeter
- IBM Monitoring and Diagnostic Tools for Java-Garbage Collection and Memory
- Visualizer
- Verbose GC Analyzer
Java VisualVM
Java VisualVM使用是免費的,其需要安裝Java SE SDK。看一下Java JDK的bin文件夾中(路徑:\Java\jdk1.8.0\bin),這里面有很多javac和java工具,jvisualvm就是其中之一。
Java VisualVM能夠被用於:
- 生成並分析堆的內存轉儲;
- 在MBeans上觀察並操作;
- 監視垃圾回收;
- 內存和CPU性能分析;
1、啟動VisualVM
jvisualvm位於JDK bin文件夾下,直接點擊就可以。
2、安裝可視化GC插件
我們需要安裝可視化GC插件,以便在Java GC過程中有良好的視覺感受。
3、監視GC
現在,是時候監視垃圾回收進程了,開啟你的Java程序,它將自動被檢測到並顯示到Java VisualVM界面,左側“Application”(應用程序)窗口下,“Local”(本地節點)下,所有本地運行的Java程序都會被列出。
Java VisualVM是一個Java應用程序,因此它也會被列在其中,教程的意圖在於使用VisualVM來監視它自己的GC進程。
雙擊“Local”(本地)下的VisualVM圖標。
現在,程序監控窗口在右側打開,這有許多不同關於應用程序性能的相關監視指數的tab頁,目前為止,我們最感興趣的是“Visual GC”,點擊它。
上面圖片顯示在Old、Eden、S0和S1上空間利用情況,下圖顯示了每部分空間的分配和釋放情況。它按照指定的刷新率保持持續刷新。
上面圖片所展示的是正常運行程序的情況,當出現內存泄露或者反常的行為時,它會在圖表中明確的顯示出來。最少我們能理解他是與對象內存分配和垃圾回收相關的事情。隨后,通過其他tab頁(像“Threads”)和Thread Dump的幫助,我們能夠減少這個問題。
在“Monitor”tab頁中,我們能夠監控並定時展示所有堆內存使用情況圖。通過“Perform GC”按鈕可以啟動垃圾回收進程。
在“Sampler”tab頁中,我們能夠啟動內存和CPU性能分析,它將顯示詳細每個實例使用的實時報告,它將幫助我們明確性能問題。
這篇教程是我們四篇Java垃圾回收系列教程的最后一篇。