1. 程式人生 > >【淺度渣文】JVM——簡述垃圾回收

【淺度渣文】JVM——簡述垃圾回收

原文連結:http://www.dubby.cn/detail.html?id=9062

垃圾回收的簡單描述

什麼是自動垃圾收集?

自動垃圾收集是檢視堆記憶體的過程,可以識別哪些物件正在使用,哪些不是,以及刪除未使用的物件。一個正在使用的物件或一個被引用的物件,意味著你的程式的某個部分仍然保持著一個指向這個物件的指標。未使用的物件或未引用的物件不再被程式的任何部分引用。所以未被引用的物件所使用的記憶體可以被回收。

在像C這樣的程式語言中,分配和釋放記憶體是一個手動過程。在Java中,釋放記憶體的過程由垃圾收集器自動處理。基本過程可以描述如下。

第1步:標記

這個過程的第一步就是標記。這是垃圾收集器標記記憶體中哪些物件正在被使用,哪些物件已經沒有被使用。

image

有用的物件顯示為藍色,沒有用的物件顯示為黃色。在標記階段掃描所有物件,然後做出這個決定。如果必須掃描系統中的所有物件,這可能是非常耗時的過程。

第2步:普通刪除

記憶體維護著一個空閒記憶體列表,每次分配空間時,會來這個列表上找到合適的空間分配。正常刪除時,會把沒有用到的物件的記憶體空間還給空閒列表。

image

另一種第2步:刪除並壓縮

為了進一步提高效能,除了刪除未引用的物件之外,還可以壓縮剩餘的引用物件。 通過移動被引用的物件,這使得新的記憶體分配變得更容易和更快。

image

為什麼使用分代垃圾收集?

如前所述,標記和壓縮JVM中的所有物件效率不高。 隨著越來越多的物件被分配,物件列表的增長和增長導致更長和更長的垃圾收集時間。 然而,應用程式的實證分析表明,大多數物件是短暫的。

這裡給個數據的例子。

image

正如你所看到的,隨著時間的推移物件保持存活的越來越少。 實際上,大多數物件的壽命都很短,如圖左側較高的值所示。

JVM 的分代

根據上面的物件的行為特性,我們可以總結出一個更好的方式來提高JVM垃圾回收的效率。所以,就把堆記憶體分成幾種代,新生代老年代永久代(Java8之後就沒有永久代了,取而代之的是元資料Metaspace)。

image

一個新的物件會被分配在新生代上,並且新的物件會在新生代裡慢慢變老。當新生代的空間被佔滿後,就會觸發一次minor gc。假設新生代裡的物件死亡率很高的話,那麼新生代的垃圾回收就是很優的。一個充滿死亡物件的新生代收集起來其實很快。倖存下來的物件會慢慢變老,直到可以移入老年代。

Stop the World Event——所有的新生代手機都是停止世界的事件。Stop the World Event的意思是,所有的應用程式的執行緒都會被暫停,直到垃圾回收完成。新生代GC總是Stop the World。

老年代是存放那些經歷了多次minor gc,年紀達到一個閾值之後的存活的物件。一般來說,會給物件設定一個年齡閾值,達到閾值之後,就會移入老年代。最後,老年代需要進行垃圾回收,就會觸發一次major gc。

Major gc也是導致Stop the World。在大部分情況下,major gc是會比minor gc慢很多。所以,對於一個關注響應時間的應用來說,應該儘可能的降低major gc的次數。這裡也要注意到,major gc的停頓時間(Stop the World的時間)是和你選取的垃圾收集器有關的。

永久代包含了JVM所需要的class和method的定義等元資料。永久代會隨著JVM執行時載入的class而填充新的元資料。除此之外,Java SE的類庫也會被儲存在這裡。

如果JVM檢測到這部分class不會被使用了,而且需要更多的記憶體空間來載入其他的class,那麼class也會被回收(unloaded/解除安裝)。這個收集包含在一次full gc中。(即便是在Java8之後,沒有了所謂了永久代,取而代之的是元資料,但是,也會存在型別解除安裝的回收)

分代垃圾回收

現在你已經明白了為什麼需要把堆細分成不同的幾個代,現在是時候仔細的看看這種空間是如何工作的了。下面的圖演示了在JVM中,物件的分配和變老的過程。

1.首先,任何物件都會被分配在eden區。兩個suvivor區一開始都是空的。

這裡是為了給讀者介紹垃圾回收器的設計過程,和一步步的思考過程,在之後還是會有很多優化,可能會和一開始的設計意圖相違背,請見諒。比如,有的物件甚至不分配到堆裡(逃逸分析),有的大物件甚至會直接分配到old區(大物件分配),有的物件甚至會分配到堆外記憶體(nio等),等等各種特殊情況。

image

2.當eden滿了之後,就會觸發一次minor gc。

image

3.活著的物件會被移到第一個suvovor區(第一個第二個都是相對的)。沒有被是用的物件就直接被清除了。

image

4.下一次minor gc發生時,同樣的操作。沒有被使用的物件被清除,活著的物件和被移到另一個suvivor區。而且,這些物件年齡會+1,然後被移入第二個suvivor。所有的活著的物件都被移入這個新的suvivor1,那麼eden和suvivor0又都空了。但是,現在在suvivor1中,物件的年齡是不一樣的。

image

5.下一次minor gc,又會重複上面的步驟。不過物件是從eden和suvicor1移入到suvivor0中了。

image

6.終於,隨便不斷的minor gc,物件的年齡越來越大,達到了閾值(這裡是8)時,他們會晉升帶老年代。

image

7.隨著更多的minor gc,也有更多的物件晉升到老年代。

image

8.上面已經涵蓋了新生代的整個過程。最後,老年代需要進行一次major gc來清除,壓縮老年代的空間。

image

執行並觀察

上面說了那麼多,相信機智的讀者已經大致瞭解垃圾回收的過程了。現在讓我們親眼看一看這個執行過程。這一部分,我們會執行一個Java應用程式,然後使用Visual VM分析回收的過程。Visual VM是JDK提供給我們的一個工具,開發者可以使用這個工具對JVM進行各個方面的監視。

1. 你需要先去Oracle官網下載JavaDemo

2.啟動示例程式碼

確保你的電腦已經安裝了JDK,並下載了上一步說的demo。然後解壓到本地一個目錄下。我的目錄是/Users/teeyoung/Desktop/code4me/javademos8

然後執行Java2demo.jar,java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2Demo.jar

注意:1.這些命令稍後會解釋;2.-XX:PermSize=20m -XX:MaxPermSize=20m如果是在Java 8之後是提示無效,以為已經被移除了。

程式執行起來是這個樣子:

image

你可以看到很多tab,那些演示了Java的繪圖功能(看到這個程式,讓我想到了買家秀和賣家秀,別人寫的程式碼和我寫的程式碼)。

隨意點選各個tab,大致是這樣的:

image

這個介面可以看到垃圾回收行為的結果,看右下角的記憶體監視。我們先讓他執行著,我們稍後會用到它的。

3.啟動VisualVM

如果你的jdk/bin已經在你的path下了,那麼直接執行jvisualvm,否則需要輸入完整的路徑,如:/usr/bin/jvisualvm

image

4.安裝Visual GC外掛

我們需要安裝Visual GC這個外掛,但是java.net這個站點都關了,無法聯網安裝,所以我寫了另一篇文章演示如何安裝外掛。請移步jvisualvm外掛安裝的正確姿勢(解決網路問題):http://www.dubby.cn/detail.html?id=9061

安裝之後就是這個樣子:

image

5.分析Java2Demo

首先雙擊Java2Demo這個本地程序,或者右擊->Open:

image

然後點選Visual GC這個tab:

image

然後就自由嘗試各個tab頁,看看每個資訊代表JVM的什麼指標。還有,你可以嘗試著改變Java2Demo上的string和image的數量,看看對垃圾回收有什麼影響。

Java垃圾收集器

現在你知道了垃圾回收的基本概念,還有如何去監視JVM的垃圾回收。現在我們來了解Java給我們提供的不同的垃圾回收器,還有我們需要掌握如何使用這些垃圾回收器的命令列。

通用的命令列

這裡給出一些通用的命令列,不管你是什麼收集器,都會用到的。

選項 描述
-Xms 設定JVM啟動時,堆的初始大小
-Xmx 設定堆的最大的容量
-Xmn 設定新生代的容量
-XX:PermSize 設定永久代的初始大小(Java 8以廢棄
-XX:MaxPermSize 設定永久代的最大容量(Java 8以廢棄
-XX:MinHeapFreeRatio 設定堆最小空閒容量,低於這個閾值就擴容,但是堆總量還是要在Xmx和Xms之間
-XX:MaxHeapFreeRatio 設定堆最大空閒容量,高於這個閾值就收縮,但是堆總量還是要在Xmx和Xms之間

Serial收集器

Serial收集器是客戶端預設的收集器。使用Serial收集器,minor gc和major gc都是單執行緒處理。而且,老年代使用併發-壓縮演算法。把老年代的活著的物件移到老年代的前面,後面空出空閒區域,以供後續分配,可以避免空間碎片。

使用場景

Serial收集器是一些客戶端(PC,不是伺服器)應用使用,而且對於低延時要求不高的。他的優勢是單執行緒處理。直到今天,對於一些不是很重要,堆記憶體只有幾百MB的應用來說,Serial GC依然是個很有效的垃圾收集器。

還有一個廣泛使用Serial收集器的場景是,一個機器上執行著很多JVM(在某些場景下,JVM的數量比處理器的核數還要多)。在這種情況,使用Serial收集器可以減少JVM之間衝突,即便GC的時間變長了。

最後,隨著嵌入式裝置的普及,記憶體少,核數少,Serial收集器可能會重新綻放光彩。

命令列選項

開始Serial收集器:

-XX:+UseSerialGC
複製程式碼

給個完整的例子:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2demo.jar
複製程式碼

Parallel收集器

Parallel收集器在回收新生代時,使用多執行緒進行收集。預設的回收執行緒數等於機器的核數。可以使用-XX:ParallelGCThreads=<desired number>來設定希望的執行緒數。

在只有一個CPU的機器上,即便你已經開啟了Parallel收集器,JVM還是會使用預設的收集器來工作。

使用場景

Parallel收集器也被稱為吞吐收集器。因為他可以利用多執行緒來加快應用的吞吐。這個收集器一般被用作有很多工作需要做,而且對低延要求時不那麼高的應用。例如,批處理(報表,賬單,或者是很大的資料庫查詢等)。

-XX:+UseParallelGC

這個命令列選項是開啟新生代的多執行緒收集,老年代的多執行緒收集。老年代也是整理方式。

給個完整的例子:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelGC -jar Java2demo.jar
複製程式碼

-XX:+UseParallelOldGC

這個選項是開啟新生代的多執行緒收集,老年代的多執行緒收集。老年代也是整理方式。

整理:就是會把活著的物件移到記憶體的前面,這樣就物件和物件之間的小的空閒的空間(記憶體碎片)。記憶體碎片可能導致,空閒空間足夠,但是大物件無法分配的情況。

完整的例子:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelOldGC -jar Java2demo.jar
複製程式碼

CMS收集器

併發(Concurrent)標記(Mark)清除(Sweep)收集器(CMS)(也被叫做:併發低延時收集器)是一個手機老年代的垃圾收集器。他試圖把大部分垃圾收集工作和應用程式的執行緒併發執行,以降低所造成的停頓(Stop the World)時間。通常情況下,CMS不會壓縮整理活著物件。所以,會存在記憶體碎片的問題。如果記憶體碎片成為你的問題,那麼可以考慮換用更大的堆(哈哈,也可以考慮換收集器,但是,換更大的堆是直接並且簡單的方法)。

注意:CMS收集器在新生代的收集方式和Parallel在新生代的收集方式一樣(單執行緒,複製)。

使用場景

CMS適用對低延時有高要求的應用。比如,響應事件的桌面應用,響應請求的Web伺服器,或者響應查詢的資料庫。

命令列選項

開啟命令:

-XX:+UseConcMarkSweepGC
複製程式碼

設定執行緒數:

-XX:ParallelCMSThreads=<n>
複製程式碼

這裡給個完整的例子:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=2 -jar Java2demo.jar
複製程式碼

G1收集器

具體的可以檢視【淺度渣文】JVM——G1收集器:http://www.dubby.cn/detail.html?id=9059

這裡簡單描述一下吧,G1在Java 7才出現的,是一個併發的,低延時的,整理收集器。對堆記憶體管理和之前的收集器都不一樣。

命令列選項

開啟命令:

-XX:+UseG1GC
複製程式碼

完整的例子:

java -Xmx12m -Xms3m -XX:+UseG1GC -jar Java2demo.jar
複製程式碼