歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Java Garbage Collection基礎之Java 垃圾回收機制技術詳解

最近還是在找工作,在面試某移動互聯網公司之前認為自己對Java的GC機制已經相當了解,其他面試官問的時候也不存在問題,直到那天該公司一個做搜索的面試官問了我GC的問題,具體就是:老年代使用的是哪中垃圾回收算法,並詳細解釋第一步做什麼,第二部做什麼?這時候才發現具體一步一步怎麼來的,確實不知道。那結果就可想而知,面試官就對我不感興趣了。那一瞬間,感覺自己不應該過分輕信別人的博客,要相信官方的文檔,因為有些寫博客的技術人員也許自身對某些技術都不是很了解,只是自己記錄下學習和使用的經歷,再或者文章可能是從別人那裡轉發而來。所以,寫下這篇文章,記錄這次慘痛的教訓,也希望自己以後學東西能夠追根溯源,知其然,更要知其所以然!

本文主要介紹,JVM的組件,自動垃圾收集器是如何工作的,分代垃圾收集器的收集過程,使如何用Visual VM來監視應用的虛擬機,以及JVM中垃圾收集器的種類。

一、JVM架構

1、HotSpot 架構

HotSpot JVM架構支持較強的基本特征和功能,此外還支持高性能和高吞吐率的特性。例如,JVM JIT編譯器產生動態優化的代碼,亦即,編譯器是在Java運行的時候的時候進行優化,並為當然的系統架構生成高性能的目標機器指令。此外,經過對運行時環境和多線程垃圾回收器不斷地設計和優化,現在的HotSpot JVM甚至在大型的系統上都具有較高的伸縮性。JVM 的主要組件包括:ClassLoader、運行時數據區和執行引擎。

2、 HotSpot關鍵組件

與性能密切相關的JVM的關鍵組件,有堆、JIT編譯器,垃圾收集器,在下圖中這些組件用深色標注。

性能優化只需要關注這三個組件即可。堆是存儲對象的地方,該區域由用戶指定(可以在啟動應用程序的時候指定)的垃圾回收器來管理。大多數優化選項都是通過配置堆的大小和選擇最合適的垃圾回收器來實現。JIT編譯器對性能也能產生比較大的影響,但是對於更新版本的(本文檔為JDK1.7)JVM很少需要對其進行優化。

二、應用程序性能的衡量要素

通常數來,當優化一個Java應用的時候,我們通常重點關心的是響應時間或吞吐量兩者其中的一個。再此對這兩個概念做下介紹,便於加深對優化的理解。

1、響應時間

響應時間指的是應用或者系統對一個請求數據的回應。例如:

      桌面UI對鼠標事件的響應速度

       網站返回頁面的速度

      數據庫查詢返回的速度

所以,對於重點關心響應時間的應用,較長時間的應用暫停時不可接受的。我們要做到盡可能的提升響應速度,減少響應時間。

2、吞吐量

吞吐量重點關心特定時間內應用程序處理工作的最大值。例如,吞吐量可以通過以下形式來衡量:

      給定時間內的完成的事物數量

      一個小時你完成的批處理程序的個數

      一個小時內完成的數據庫查詢的次數

這種情況下,應用程序能容忍較高的暫停時間,因此,高吞吐量的應用程序有更長的時間基准,快速響應是不必考慮的。

三、自動垃圾收集器是如何工作的

1、什麼是自動垃圾收集機制?

自動垃圾收集機制是查看堆內存、區分在使用的對象和未使用的對象、刪除未使用的對象的一個過程。對於使用對象或者引用對象,指的是你的程序持有一個指向那個對象的引用。對於未使用的對象或者是無引用對象,則不被你程序的任何部分持有引用。所以,無引用對象使用的內存是可以被重新回收利用的。

在類C語言的編程語言中,內存的分配和回收都是手動的。而在Java中,內存的回收是由垃圾回收器自動處理的。基本的步驟可以描述如下:

步驟一:標記

第一步是標記,通過這一步驟來區分哪塊內存在使用,那哪塊內存未使用。

 

引用對象用藍色標識,未引用的對象用金色標識。在標記階段,掃描所有的對象並判斷。如果系統中所有的對象都要被掃描的,那麼這一步驟可能非常耗時。

步驟二:正常刪除

 正常刪除移除無引用對象,留下引用對象及指向空閒空間的指針。

內存分配器持有空閒內存的引用,這些空閒內存都鏈接到一個List中,當需要的時候可以分配給新的對象。

 步驟二a:帶壓縮刪除

為了進一步改善性能,除了刪除未引用的對象,用戶也可以壓縮存活的引用對象。把引用對象移動到一起,通過這種方法可以使更快速、更方便的分配新的內存。

2、為什麼使用分代垃圾回收機制?

在早期的JVM上,不得不在所有的對象上進行標記-壓縮,這顯然是非常低效。隨著越來越多的對象被分配,對象列表也逐漸增大,這就導致越來越長的垃圾回收時間。然而,根據經驗我們分析得到大部分對象的生命周期是非常短暫的。

下面是這些數據的一個例子,Y軸代表的是分配的字節數,X軸代表的是隨著時間的推移分配的字節數。

從圖中可以看到,隨著時間的推移,分配後的對象遺留的越來越少。事實上,大多數對象有非常短的生命周期,從圖中左邊較高的值得寬度可以得出。

3、JVM各代介紹

從上面對象分配行為中,我們知道據此可以增強JVM的性能。因此,堆被分解為較小的三個部分或者三個代。具體分為:年輕代、老年代、持久代。

年輕代:所有創建的新對象都是在年輕代分配堆空間,在這一代變老。當年輕代被填滿的時候,這就會導致一個小收集。如果對象的死亡率很高,小回收就可以獲得優化。年輕代中死亡的對象越多,回收的速度也就越快。幸存對象逐漸變老(年紀增大),最終會移動到老年代。

全局暫停事件:所有的小收集都是一個個全局暫停事件。這意味著所有的應用線程都會停止,直到收集操作完成。小回收總會導致全局暫停事件。

老年代:老年代用於存儲較長生命周期的對象。典型的說來就是,為年輕代對象設置了阈值,當年輕代逐漸變老,到達這個阈值的時候,對象就會被移動到老年代。隨著時間的推移,老年代也會被填滿,最終導致老年代也要進行垃圾回收。這個事件叫做大收集。

大收集也是全局暫停事件。通常大收集比較慢,因為它涉及到所有的存活對象。所以,對於對相應時間要求高的應用,應該將大收集最小化。此外,對於大收集,全局暫停事件的暫停時長會受到用於老年代的垃圾回收器的影響。

持久代:持久代存儲了描述應用程序類和方法的元數據,JVM運行應用程序的時候需要這些元數據。持久代由JVM在運行時基於應用程序所使用的類產生。此外,Java SE類庫的類和方法可能也存儲在這裡。

如果JVM發現有些類不在被其他類所需要,同時其他類需要更多的空間,這時候這些類可能就會被垃圾回收。

 四、垃圾回收的一般步驟

從上面的介紹中我們已經理解了為什麼堆被分成不同的代,下面我們就需要更精確的理解這些空間是如何進行交互的。下面的一組圖片展示了JVM中垃圾回收的一般過程,從對象分配到對象逐漸變老。

1、首先,所有新生成的對象都是放在年輕代的Eden分區的,初始狀態下兩個Survivor分區都是空的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。

 

2、當Eden區滿的的時候,小垃圾收集就會被觸發。

3、當Eden分區進行清理的時候,會把引用對象移動到第一個Survivor分區,無引用的對象刪除。

4、在下一個小垃圾收集的時候,在Eden分區中會發生同樣的事情:無引用的對象被刪除,引用對象被移動到另外一個Survivor分區(S1)。此外,從上次小垃圾收集過程中第一個Survivor分區(S0)移動過來的對象年齡增加,然後被移動到S1。當所有的幸存對象移動到S1以後,S0和Eden區都會被清理。注意到,此時的Survivor分區存儲有不同年齡的對象。

5、在下一個小���圾收集,同樣的過程反復進行。然而,此時Survivor分區的角色發生了互換,引用對象被移動到S0,幸存對象年齡增大。Eden和S1被清理。

6、這幅圖展示了從年輕代到老年代的提升。當進行一個小垃圾收集之後,如果此時年老對象此時到達了某一個個年齡阈值(例子中使用的是8),JVM會把他們從年輕代提升到老年代。

7、隨著小垃圾收集的持續進行,對象將會被持續提升到老年代。

8、這樣幾乎涵蓋了年輕一代的整個過程。最終,在老年代將會進行大垃圾收集,這種收集方式會清理-壓縮老年代空間。

五、JVM垃圾收集器的種類

現在我們已經知道垃圾收集器的一些基本原理,並且借助VisualVM可以觀察到垃圾收集器的實時表現。本節將會詳細講解Java可以使用的垃圾回收器,以及在命令行如何選用配置它們。配置JVM有很多可以用的命令行參數,本節選用常用的配置參數進行詳細解。

與堆配置相關的參數

Java中有很多可以使用的命令行參數,這一節將會介紹常用的一些命令行參數。

  參數 描述 -Xms JVM啟動的時候設置初始堆的大小 -Xmx 設置最大堆的大小 -Xmn 設置年輕代的大小 -XX:PermSize 設置持久代的初始的大小 -XX:MaxPermSize 設置持久代的最大值

1、串行收集器:

在Java SE 5和6中,串行收集器是客戶端環境(client-style machines)機器的默認設置。在這種情況下,小垃圾收集和大垃圾收集都是串行進行的(使用單個的虛擬CPU)。

使用的算法說明:

串行收集器在年輕代使用的是拷貝算法,這個算法比較簡單,在這裡不做詳述。而年老代和持久代使用標記-清掃-壓縮(mark-sweep-compact)算法。標記階段,收集器識別哪些對象仍然活著。清掃階段“掃蕩”整個代,識別垃圾。之後,收集器執行平移壓縮(sliding compaction),將存活的對象平移到代的前端(持久代類似),相應的在尾部留下一整塊連續的空閒空間。壓縮後,以後的分配就可以在年老代和持久代使用空閒指針(bump-the-pointer)技術。這種壓縮算法能夠在堆上迅速分配內存塊。

示例:大多數客戶端式(client-style machines)機器上運行的應用程序通常都是選擇串行收集器,這些應用對短暫停沒有要求。它之所以叫這個名字,是因為它能充分利用單個虛擬處理器進行垃圾回收的工作。在今天的硬件上,串行收集器可以有效的管理許多擁有幾百M堆內存的重要應用程序,並且擁有相對短的最壞暫停(Full GC僅有幾秒左右)。

在有大量JVM運行在同一個機器上(在某些情況下,JVM的個數比可以用的處理器的個數多)的應用環境下,串行垃圾收集器也被廣泛使用。在這種環境下,要進行垃圾回收的JVM最好使用一個處理器,雖然這樣會使垃圾回收的時間變得更長,但可以降低與其他JVM的沖突。這時,使用串行垃圾回收器能夠獲得很好的權衡。最後,如果在較小的內存和較少的CPU核心上對硬件進行稍加擴充,將能獲得更好的性能。

命令行參數

使用串行垃圾回收器 -XX:+UseSerialGC

給事例應用使用串行垃圾回收器的命令行如下:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar
c:\javademos\demo\jfc\Java2D\Java2demo.jar

2、並行收集器

並行垃圾收集器在年輕代使用多線程進行垃圾回收。默認情況下,在N個CPU的主機上,並行垃圾收集器使用N個垃圾收集器線程進行垃圾回收。垃圾收集器線程的個數可以在命令行進行設置:-XX:ParallelGCThreads=<期望的數值>

 在單核的CPU上,盡管我們請求設置的是並行垃圾收集器,但JVM還是使用默認的垃圾收集器。在兩個CPU的主機上,並行垃圾收集器與默認的串行垃圾收集器所表現出來的性能相當,年輕代的垃圾收集器暫停時間與兩個以上CPU的主機相比也有所減少。並行垃圾收集器有兩種使用方式。

使用的算法說明:

年輕代:與串行垃圾收集器年輕代相同的拷貝算法,只不過是該算法的並行版本,使用多個CPU並行的運行,減少了垃圾收集的開銷,因此增加了吞吐量。

年老代:與串行垃圾收集器老年代想聽的標記-清掃-壓縮(mark-sweepcompact)算法,只不過是該算法的並行版本。

示例並行收集器也叫做吞吐量收集器,因為其可以使用多個CPU來增大應用程序的吞吐量。當應用程序需要處理大量的工作同事可以接受較長的暫停時,可以使用並行垃圾收集器。例如,想打印報告或者賬單這樣的批處理,或者進行大量的數據庫查詢。

-XX:+UseParallelGC

使用這個命令行參數,就會將年輕代設置為多線程的收集器,老年代使用單線程的收集器。該選項,還會在老年代進行單線程的壓縮工作。

啟動示例應用程序Java2Demo的命令行如下:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelGC -jar
c:\javademos\demo\jfc\Java2D\Java2demo.jar

-XX:+UseParallelOldGC
使用該參數,年輕代和老年代都會使用多線程的收集器,同時,也使用多線程的壓縮收集器。HotSpot僅僅在老年代進行整理,在年輕代是一個復制收集器,因此沒必要進行整理。

壓縮描述的是這樣一種行為,移動對象使得個對象之間沒有空閒位置。再一次垃圾收集的清理之後,存活對象在內存中的存儲位置之間可能存在空閒區。整理移動對象,使得對象的存儲都是順序的,彼此之間沒有空閒區。垃圾收集器可能也是一個不帶壓縮的收集器。所以,並行收集器和並行壓縮收集器之間的區別就是後者在垃圾收集清理操作之後,對內存空間進行一次整理。

啟動示例應用程序Java2Demo的命令行如下:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelOldGC -jar
c:\javademos\demo\jfc\Java2D\Java2demo.jar

3、並發標記清理收集器

並發標記清理收集器(CMS,又叫作並發低暫停收集器)在老年代進行收集。由於垃圾收集能使用應用線程的並發進行大多數的垃圾收集工作,所以它降低了應用程序的暫停時間。

正常說來,並發低暫停的收集器對存活對象不進行復制和壓縮的工作。這種情況下,垃圾收集器沒有移動任何存活對象。如果因此而帶來了內存的碎片問題,那就為其分配一個更大的堆。

注意CMS收集器在年輕代使用和並行收集器一樣的算法。

示例:CMS收集器常常應用於需要低暫停及可以與垃圾收集器共享資源的場景。例如:桌面UI應用程序對事件的響應,Web服務器對請求的響應,以及數據庫對查詢請求的響應。

命令行參數

 如果要使用CMS收集器,使用 -XX:+UseConcMarkSweepGC ,同時,可以設置並發的線程數目 -XX:ParallelCMSThreads=<n> 。

啟動示例應用程序Java2Demo的命令行如下:

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=2 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar


4、G1 垃圾回收器

在Java 7中可以使用G1垃圾回收器,它設計的初衷是用於長期取代CMS收集器。G1垃圾收集器是一個並行、並發,同時也是基於增量整理的低暫停垃圾收集器。與前面所描述的垃圾收集器相比,從布局方面與它們有很大的不同。但本文不對該部分做詳細的說明,有興趣可以參考具體的文獻資料。

命令行參數:

 如果要使用CMS收集器,使用 -XX:+UseG1GC

啟動示例應用程序Java2Demo的命令行如下:

java -Xmx12m -Xms3m -XX:+UseG1GC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

六、總結

本文對Java中的JVM進行了較為詳細的介紹。我們知道了堆和垃圾收集器在Java JVM中是非常重要的部分。自動的垃圾收集是通過分代垃圾收集的方法來完成的。一旦我們知道了這一原理,我們就可以通過Visual VM虛擬機工具來觀察整個過程。

Copyright © Linux教程網 All Rights Reserved