最近仍是在找工做,在面試某移動互聯網公司以前認爲本身對Java的GC機制已經至關了解,其餘面試官問的時候也不存在問題,直到那天該公司一個作搜索的面試官問了我GC的問題,具體就是:老年代使用的是哪中垃圾回收算法,並詳細解釋第一步作什麼,第二部作什麼?這時候才發現具體一步一步怎麼來的,確實不知道。那結果就可想而知,面試官就對我不感興趣了。那一瞬間,感受本身不該該過度輕信別人的博客,要相信官方的文檔,由於有些寫博客的技術人員也許自身對某些技術都不是很瞭解,只是本身記錄下學習和使用的經歷,再或者文章多是從別人那裏轉發而來。因此,寫下這篇文章,記錄此次慘痛的教訓,也但願本身之後學東西可以追根溯源,知其然,更要知其因此然!前端
本文主要介紹,JVM的組件,自動垃圾收集器是如何工做的,分代垃圾收集器的收集過程,使如何用Visual VM來監視應用的虛擬機,以及JVM中垃圾收集器的種類。java
HotSpot JVM架構支持較強的基本特徵和功能,此外還支持高性能和高吞吐率的特性。例如,JVM JIT編譯器產生動態優化的代碼,亦即,編譯器是在Java運行的時候的時候進行優化,併爲固然的系統架構生成高性能的目標機器指令。此外,通過對運行時環境和多線程垃圾回收器不斷地設計和優化,如今的HotSpot JVM甚至在大型的系統上都具備較高的伸縮性。JVM 的主要組件包括:ClassLoader、運行時數據區和執行引擎。面試
與性能密切相關的JVM的關鍵組件,有堆、JIT編譯器,垃圾收集器,在下圖中這些組件用深色標註。算法
性能優化只須要關注這三個組件便可。堆是存儲對象的地方,該區域由用戶指定(能夠在啓動應用程序的時候指定)的垃圾回收器來管理。大多數優化選項都是經過配置堆的大小和選擇最合適的垃圾回收器來實現。JIT編譯器對性能也能產生比較大的影響,可是對於更新版本的(本文檔爲JDK1.7)JVM不多須要對其進行優化。數據庫
一般數來,當優化一個Java應用的時候,咱們一般重點關心的是響應時間或吞吐量二者其中的一個。再此對這兩個概念作下介紹,便於加深對優化的理解。編程
響應時間指的是應用或者系統對一個請求數據的迴應。例如:性能優化
桌面UI對鼠標事件的響應速度服務器
網站返回頁面的速度多線程
數據庫查詢返回的速度架構
因此,對於重點關心響應時間的應用,較長時間的應用暫停時不可接受的。咱們要作到儘量的提高響應速度,減小響應時間。
吞吐量重點關心特定時間內應用程序處理工做的最大值。例如,吞吐量能夠經過如下形式來衡量:
給定時間內的完成的事物數量
一個小時你完成的批處理程序的個數
一個小時內完成的數據庫查詢的次數
這種狀況下,應用程序能容忍較高的暫停時間,所以,高吞吐量的應用程序有更長的時間基準,快速響應是沒必要考慮的。
自動垃圾收集機制是查看堆內存、區分在使用的對象和未使用的對象、刪除未使用的對象的一個過程。對於使用對象或者引用對象,指的是你的程序持有一個指向那個對象的引用。對於未使用的對象或者是無引用對象,則不被你程序的任何部分持有引用。因此,無引用對象使用的內存是能夠被從新回收利用的。
在類C語言的編程語言中,內存的分配和回收都是手動的。而在Java中,內存的回收是由垃圾回收器自動處理的。基本的步驟能夠描述以下:
第一步是標記,經過這一步驟來區分哪塊內存在使用,那哪塊內存未使用。
引用對象用藍色標識,未引用的對象用金色標識。在標記階段,掃描全部的對象並判斷。若是系統中全部的對象都要被掃描的,那麼這一步驟可能很是耗時。
正常刪除移除無引用對象,留下引用對象及指向空閒空間的指針。
內存分配器持有空閒內存的引用,這些空閒內存都連接到一個List中,當須要的時候能夠分配給新的對象。
爲了進一步改善性能,除了刪除未引用的對象,用戶也能夠壓縮存活的引用對象。把引用對象移動到一塊兒,經過這種方法可使更快速、更方便的分配新的內存。
在早期的JVM上,不得不在全部的對象上進行標記-壓縮,這顯然是很是低效。隨着愈來愈多的對象被分配,對象列表也逐漸增大,這就致使愈來愈長的垃圾回收時間。然而,根據經驗咱們分析獲得大部分對象的生命週期是很是短暫的。
下面是這些數據的一個例子,Y軸表明的是分配的字節數,X軸表明的是隨着時間的推移分配的字節數。
從圖中能夠看到,隨着時間的推移,分配後的對象遺留的愈來愈少。事實上,大多數對象有很是短的生命週期,從圖中左邊較高的值得寬度能夠得出。
從上面對象分配行爲中,咱們知道據此能夠加強JVM的性能。所以,堆被分解爲較小的三個部分或者三個代。具體分爲:年輕代、老年代、持久代。
年輕代:全部建立的新對象都是在年輕代分配堆空間,在這一代變老。當年輕代被填滿的時候,這就會致使一個小收集。若是對象的死亡率很高,小回收就能夠得到優化。年輕代中死亡的對象越多,回收的速度也就越快。倖存對象逐漸變老(年紀增大),最終會移動到老年代。
全局暫停事件:全部的小收集都是一個個全局暫停事件。這意味着全部的應用線程都會中止,直到收集操做完成。小回收總會致使全局暫停事件。
老年代:老年代用於存儲較長生命週期的對象。典型的說來就是,爲年輕代對象設置了閾值,當年輕代逐漸變老,到達這個閾值的時候,對象就會被移動到老年代。隨着時間的推移,老年代也會被填滿,最終致使老年代也要進行垃圾回收。這個事件叫作大收集。
大收集也是全局暫停事件。一般大收集比較慢,由於它涉及到全部的存活對象。因此,對於對相應時間要求高的應用,應該將大收集最小化。此外,對於大收集,全局暫停事件的暫停時長會受到用於老年代的垃圾回收器的影響。
持久代:持久代存儲了描述應用程序類和方法的元數據,JVM運行應用程序的時候須要這些元數據。持久代由JVM在運行時基於應用程序所使用的類產生。此外,Java SE類庫的類和方法可能也存儲在這裏。
若是JVM發現有些類不在被其餘類所須要,同時其餘類須要更多的空間,這時候這些類可能就會被垃圾回收。
從上面的介紹中咱們已經理解了爲何堆被分紅不一樣的代,下面咱們就須要更精確的理解這些空間是如何進行交互的。下面的一組圖片展現了JVM中垃圾回收的通常過程,從對象分配到對象逐漸變老。
如今咱們已經知道垃圾收集器的一些基本原理,而且藉助VisualVM能夠觀察到垃圾收集器的實時表現。本節將會詳細講解Java可使用的垃圾回收器,以及在命令行如何選用配置它們。配置JVM有不少能夠用的命令行參數,本節選用經常使用的配置參數進行詳細解。
與堆配置相關的參數
Java中有不少可使用的命令行參數,這一節將會介紹經常使用的一些命令行參數。
參數 | 描述 |
-Xms | JVM啓動的時候設置初始堆的大小 |
-Xmx | 設置最大堆的大小 |
-Xmn | 設置年輕代的大小 |
-XX:PermSize | 設置持久代的初始的大小 |
-XX:MaxPermSize | 設置持久代的最大值 |
在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
並行垃圾收集器在年輕代使用多線程進行垃圾回收。默認狀況下,在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
併發標記清理收集器(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
在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虛擬機工具來觀察整個過程。