這篇博客是對Java垃圾回收的總結,主要是對Java Garbage Collection Introduction以及后續的三篇博客的翻譯。我把這四篇博客翻譯到這一篇博客里,把參考的其他博客的鏈接附在文章末尾。
Java Garbage Collection Introduction
在Java中,分配和回收對象的內存空間是在JVM中、由垃圾回收機制自動完成的。不像C語言,使用Java的開發人員不需要寫代碼來進行垃圾回收。Java之所以如此流行以及能夠幫助程序員更好的開發,這是其中一個原因。
這個系列一共有四篇博客來介紹Java的垃圾回收,分別是:
- Java Garbage Collection Introduction
- How Java Garbage Collection Works?
- Types of Java Garbage Collectors
- Java Garbage Collection Monitoring and Analysis
這一節是這篇博客的第一部分。主要內容是解釋一些基礎的術語,比如JDK, JVM, JRE, HotSpot VM,然后理解JVM的整體架構和Java堆內存結構。理解這些概念對學習Java垃圾回收很有幫助。
Key Java Terminologies
Java API:Java API是幫助程序員進行Java開發的一系列封裝庫的集合。
Java Development Kit (JDK):JDK是一套幫助程序員進行Java開發的工具,這些工具主要是進行編譯、運行、打包、分配和監視Java程序。
Java Virtual Machine (JVM):JVM是一個抽象的計算機。Java程序是針對JVM規范來寫的。對不同的操作系統來說,JVM有所不同,它主要作用是把Java指令翻譯為操作系統的指令,並且執行它們。JVM能夠保證Java代碼獨立於平台。
Java Runtime Environment (JRE):JRE包括JVM實現和Java API。
Java HotSpot Virtual Machine
不同的JVM在垃圾回收的實現上可能有點不一樣。 在SUN被收購之前,Oracle有JRockit JVM,並且在SUN收購之后,Oracle獲得了HotSpot JVM。目前Oracle維護了兩個JVM實現,並且已經聲明,在一段時間內這兩個JVM實現將被合並到一個。
HotSpot JVM是標准Oracle SE平台的核心組件。在這個垃圾回收教程中,我們將看到基於HotSpot虛擬機的垃圾回收原理。
JVM Architecture
下圖總結了JVM的關鍵組件。在JVM架構中,兩個主要涉及到垃圾回收的組件是堆內存(heap memory)和垃圾回收器(garbage collector)。堆內存是運行時數據所在的區域,這個區域存着對象實例,垃圾回收器也在這個區域內操作。我們現在看看這些東西如何適應到更大的方案。
Runtime Data Area
正如虛擬機規范所說的那樣,JVM中的內存分為5個虛擬的區域:
- Method Area:也被稱為非堆區域(在HotSpot JVM的實現當中)。它被分為兩個主要的子區域:持久代和代碼緩存。前者會存儲包括類定義,結構,字段,方法(數據及代碼)以及常量在內的類相關數據。后者是用來存儲編譯后的代碼。編譯后的代碼就是本地代碼,它是由JIT編譯器生成的,這個編譯器是Oracle HotSpot JVM所特有的。
- Heap Memory:后續會詳細介紹。
- Java Stacks:和Java類中的方法密切相關,它會存儲局部變量以及方法調用的中間結果及返回值,Java中的每個線程都有自己專屬的棧,這個棧是別的線程無法訪問的。
- PC Registers:特定線程的程序計數器,包含JVM正在執行的指令的地址(如果是本地方法的話它的值則未定義)
- Native Method Stack:用於本地方法(非Java代碼),按線程分配
Java Heap Memory
理解Java Heap Memory在JVM模型中的角色很重要。在運行期間,Java實例保存在heap memory區域。當一個對象沒有引用指向它時,它就需要從內存里被清除。在垃圾回收的過程中,這些對象在heap memory里被清除,然后這些空間能夠再被利用。heap memory主要有三個區域:
- 年輕代。年輕代分三個區:Eden區(任何進去內存區域的對象實例都要經過eden區)、Survivor0區(從eden區過來S0區的、稍微老一點的對象)、Survivor1區(從S0區過來、稍微老一點的對象)
- 年老代。這個區主要是從S1過來的對象。
- 持久代。這個區包含一些元數據信息比如,類、方法等。
更新:持久代區域已經從Java SE 8移除。取代它的是另一個內存區域也被稱為元空間(本地堆內存中的一部分)。
How Java Garbage Collection Works?
這個系列博客主要是幫助理解Java回收的基本概念以及其如何工作。這部分是這個系列的第二部分,希望你有閱讀過第一部分Java Garbage Collection Introduction。
Java垃圾回收是一個自動處理的過程,目的是管理程序運行時的內存問題。通過這種方式,JVM能夠自動減輕程序員在分配和釋放內存資源的問題。
Java Garbage Collection GC Initiation
垃圾回收作為一個自動的過程,程序員不用在代碼里顯式地啟動垃圾回收機制。System.gc()和Runtime.gc()能夠顯式地讓JVM執行垃圾回收過程。
雖然這個機制能夠讓程序員啟動垃圾回收,但是這個責任還是在JVM上。JVM能夠選擇拒絕你的請求,所以我們沒法保證這兩個命令肯定會執行垃圾回收。JVM會通過eden區的可用內存來判斷是否執行垃圾回收。JVM規范把這個選擇留給了JVM的具體實現,所以這些實現的細節對不同的JVM來說也不一樣。
毫無疑問的是,垃圾回收機制不能強制執行。我剛剛發現了一個調用System.gc()的應用場景,看這篇文章就知道什么時候調用System.gc()是合適的。
Java Garbage Collection Process
垃圾回收是這樣一個過程:回收不再使用的內存空間,讓這些空間能夠被將來的實例對象所用。
Eden Space
當一個實例被創建的時候,它首先存在堆內存區域的年輕代的eden區。
注意:假如你不懂這些術語,建議你去讀Java Garbage Collection Introduction,這篇博客把內存模型、JVM架構和一些專業術語解釋的很詳細。
Survivor Space (S0 and S1)
作為次要垃圾回收(minor GC)周期的一部分,那些仍然被引用的對象會從eden區被搬到survivor的S0區。同樣的過程,垃圾回收器會掃描S0區,然后把實例搬到S1區。
那些沒有被引用的對象會被標記。通過不同的垃圾回收器(一共有四類垃圾回收器,后續的教程會講這個)來決定這些被標記的對象在內存中被移除還是在單獨的進程中來完成。
Old Generation
年老代或者持久代是堆內存中的第二個邏輯部分。當垃圾回收器執行次要垃圾回收周期的時候,那些仍在S1區存活的實例將被移到年老代,S1區域中沒有被引用的實例將會被標記以便清除。
Major GC
在垃圾回收的過程中,年老代是實例對象生命周期的最后一個環節。Major GC是掃描年老代的堆內存的垃圾回收過程。如果有對象沒有被引用,那它們將被標記以便清除,如果有被引用,那它們將繼續留在年老代。
Memory Fragmentation
一旦實例對象在堆內存中被刪除,那它們所占的區域就又可以被將來的實例對象使用。這些空的區域在內存中將會形成很多碎片。為了更快的為實例分配內存,我們應該解決這個碎片問題。根據不同垃圾回收器的選擇,被回收的內存將在回收的過程同時或者在GC另外獨立的過程中壓縮整合。
Finalization of Instances in Garbage Collection
就在清除一個對象並回收它的內存空間之前,垃圾回收器將會調用各個實例的finalize()方法,這樣實例對象就有機會可以釋放掉它占用的資源。盡管finalize()方法是保證在回收內存空間之前執行的,但是對具體的執行時間和執行順序是沒有任何保證的。多個實例之間的finalize()執行順序是不能提前預知的,甚至有可能它們是並行執行的。程序不應該預先假設實例執行finalize()的順序,也不應該使用finalize()方法來回收資源。
- 在finalize過程中拋出的任何異常都默認被忽略掉了,同時該對象的銷毀過程被取消
- JVM規范並沒有討論關於弱引用的垃圾回收,並且是明確聲明的。具體的細節留給實現者決定。
- 垃圾回收是由守護進程執行的
When an object becomes eligible for garbage collection?
- 任何不能被活着的線程用到的實例
- 不能被其他對象用到的循環引用對象
Java中有多種不同的引用類型。實例的可回收性取決於它的引用類型。
引用 | 垃圾回收 |
---|---|
強引用 | 不適合被垃圾回收 |
軟引用 | 可能會被回收,但是是在最后被回收 |
弱引用 | 適合被回收 |
虛引用 | 適合被回收 |
在編譯期間,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實例化后把lion賦值為null而作為一個優化的手段。這樣finlizer方法可能會在Main方法之前打印結果。我們不能確定地保證結果,因為這和JVM的具體實現以及運行時的內存有關。但是我們能知道的一點是:編譯器發現一個實例在之后的程序中不再被引用時可以選擇提前釋放實例的內存。
- 這里有個實例何時變成可回收更好的例子。實例所有的屬性可以被存儲在寄存器中,之后可以從寄存器中讀取這些屬性值。在任何情況下,這些值將來都不會被寫回到實例對象中。盡管這些值在未來還是被使用到了,但是實例對象依然可以被標記為可回收的。(注:這個例子我沒看懂)
- 我們可以簡單地認為,當一個實例被賦值為null時就可以被回收。我們也可以像上面提到的、復雜地考慮這個問題。這些都是由JVM的具體實現決定的。其目標都是希望留下最少的痕跡、提高響應性能和增大吞吐量。為了能夠達到這些目的,JVM實現者可以在垃圾回收中選擇更好的模式或算法來回收內存。
- 當finalize()被調用的時候,JVM釋放掉當前線程的所有同步塊。
Example Program for 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++;
}
Example Program for 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)
Types of Java Garbage Collectors
在這個教程中,我們將學習各種各樣的垃圾回收器。Java垃圾回收是一個自動處理的過程,目的是管理程序運行時的內存問題。這是這個系列教程的第三部分,在前面兩個部分,我們學習了How Java Garbage Collection Works,這部分很有意思,我建議你完整讀一遍。而第一部分Java Garbage Collection Introduction中,我們學習了JVM的架構、堆內存模型和Java相關的術語。
Java有四種垃圾回收器:
- Serial Garbage Collector
- Parallel Garbage Collector
- CMS Garbage Collector
- G1 Garbage Collector
這四種垃圾回收器各有自己的利弊。更重要的是,我們程序員可以選擇JVM用哪種垃圾回收器。我們一般是通過設置JVM參數來選擇垃圾回收器。各個垃圾回收器在不同應用場景下的效率會有很大的差異。因此了解各種不同類型的垃圾回收器以及它們的應用場景是非常重要的。
Serial Garbage Collector
串行垃圾回收器控制所有的應用線程。它是為單線程環境設計的,只使用一個線程來執行垃圾回收工作。它的工作方式是暫停所有應用線程來執行垃圾回收工作,這種方式不適用於服務器的應用環境。它最適用的是簡單的命令行程序。
使用-XX:+UseSerialGC JVM參數來開啟使用串行垃圾回收器。
Parallel Garbage Collector
並行垃圾回收器也稱作基於吞吐量的回收器。它是JVM的默認垃圾回收器。與Serial垃圾回收器不同的是,它使用多個線程來執行垃圾回收工作。和Serial回收器一樣,它在執行垃圾回收工作是也需要暫停所有應用線程。
CMS Garbage Collector
並發標記清除(Concurrent Mark Sweep,CMS)垃圾回收器,使用多個線程來掃描堆內存並標記可被清除的對象,然后清除標記的對象。CMS垃圾回收器只在下面這兩種情形下暫停工作線程:
- 在老年代中標記引用對象的時候
- 在並行垃圾回收的過程中堆內存中有變化發生
對比與並行垃圾回收器,CMS回收器使用更多的CPU來保證更高的吞吐量。如果我們可以有更多的CPU用來提升性能,那么CMS垃圾回收器是比並行回收器更好的選擇。
使用-XX:+UseParNewGC JVM參數來開啟使用CMS垃圾回收器。
G1 Garbage Collector
G1垃圾回收器應用於大的堆內存空間。它將堆內存空間划分為不同的區域,對各個區域並行地做回收工作。G1在回收內存空間后還立即堆空閑空間做整合工作以減少碎片。CMS卻是在全部停止(stop the world,STW)時執行內存整合工作。G1會根據各個區域的垃圾數量來對區域評判優先級。
使用-XX:UseG1GC JVM參數來開啟使用G1垃圾回收器。
Java 8 Improvement
在使用G1垃圾回收器時,開啟使用-XX:+UseStringDeduplacaton JVM參數。它會通過把重復的String值移動到同一個char[]數組來優化堆內存占用。這是Java 8 u 20引入的選項。
以上給出的四個Java垃圾回收器,在什么時候使用哪一個取決於應用場景,硬件配置和吞吐量要求。
Garbage Collection JVM Options
下面是與Java垃圾回收相關、比較重要的JVM選項。
Type of Garbage Collector to run
選項 | 描述 |
---|---|
-XX:+UseSerialGC | Serial Garbage Collector |
-XX:+UseParallelGC | Parallel Garbage Collector |
-XX:+UseConcMarkSweepGC | CMS Garbage Collector |
-XX:ParallelCMSThreads= | CMS Collector – number of threads to use |
-XX:+UseG1GC | G1 Gargbage Collector |
GC Optimization Options
選項 | 描述 |
---|---|
-Xms | Initial heap memory size |
-Xmx | Maximum heap memory size |
-Xmn | Size of Young Generation |
-XX:PermSize | Initial Permanent Generation size |
-XX:MaxPermSize | Maximum Permanent Generation size |
Example Usage of JVM GC Options
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
Java Garbage Collection Monitoring and Analysis
這篇教程主要介紹垃圾回收機制的監控和分析,我們會用到一個工具來監控一個Java應用的垃圾回收過程。如果你是一個新手,你最好把這系列博客的前幾篇都看一下,你可以從Java Garbage Collection Introduction開始。
Java Garbage collection monitoring and analysis tools
下面說的工具各自都有利弊,我們可以通過選擇合適的工具來進行分析以此提高Java應用的性能。我們這篇教程主要用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安裝路徑,就在\Java\jdk1.8.0\bin路徑下。當然還有很多其他的工具,jvisualvm只是其中之一。
Java VisualVM提供了一個包含Java程序信息的可視化接口。它包含很多其他的工具且集成到這一個。現在像JConsole, jstat, jinfo, jstack, and jmap都被集成到Java VisualVM中了。
Java VisualVM主要用來:
- generate and analyze heap memory dumps
- view and operate on MBeans
- monitor garbage collection
- memory and CPU profiling
Launch VisualVM
JDK目錄下就有Java VisualVM。
Install Visual GC Plugin
我們需要安裝一個可視化插件,以便觀察Java GC過程。
Monitor GC
啟動你的Java應用,Java VisualVM會自動開始監控,並把結果展示在Java VisualVM可視化接口中。你可以選擇不同的組件來觀察你感興趣的指標。