本篇內容主要轉載自http://blog.csdn.net/calvinxiu/archive/2007/05/18/1614473.aspx,作者“江南白衣”
結合自身的學習,加入了《Thinking in Java 3rd Edition》中的部份相關內容
一. 引子
首先需要明確的一點是:Java中的所有對象(基本類型除外)都在堆上進行分配。
然而,Java語言的速度并不比其他那些在堆棧上分配空間的語言慢,其原因就在于Java的垃圾回收機制對于對象的創建具有非常明顯的效果。
我們可以把C++的對想像成一個院子,里面每個對象都負責管理自己的底盤。一段時間以后,對象可能被銷毀,但地盤必須被重用。
而Java中的堆更像一個傳送帶:你每分配一個對象,它就往前移動一格。這意味著對象存儲空間的分配速度非常快。Java的“堆指針”只是簡單地移動到尚未分配的區域,其效率比得上C++在堆棧上分配空間的效率。當然,實際過程中還存在諸如簿記工作的少量額外開銷,但不會有像查找空間這樣的大動作。
當然,Java中的堆并非完全像傳送帶那樣工作。要真是那樣的話,勢必會導致頻繁的內存頁面調度(這將極大影響性能),并最終耗盡資源。
其中的秘密在于垃圾回收器的介入。當它工作時,將一面回收空間,一面使堆中的對象緊湊排列,這樣“堆指針”就可以很容易移動到更靠近傳送帶的開始處,也就盡量避免了頁面錯誤。
Java通過垃圾回收期對對象重新排列,從而實現了一種高速的、有無限空間可分配的堆模型。
二. 垃圾回收算法
1. 引用計數
首先介紹一種最直觀最簡單但卻相當不實用(實際上也并沒有被JVM采用)的回收算法——“引用計數”。我們介紹它是為了讓大家對垃圾回收有個初步的概念,再通過與其他算法的對比,了解到其他算法的精華與優越性。
所謂“引用計數”,是指每個對象都有一格引用計數器,當有引用連接至對象時,引用計數加1。當引用離開作用域或被設置為null時,引用計數減1。雖然管理引用計數的開銷不大,但需要在整個生命周期中持續地開銷。垃圾回收器會在含有全部對象的列表上遍歷,當發現某個對象的引用計數為0時,就釋放其占用的空間
這個算法除了低效外,還有個致命的缺陷:如果對象之間存在循環引用,可能會出現“對象應該被回收,但引用計數卻不為零”的情況。對垃圾回收器而言,定位這樣存在交互引用的對象組所需的工作量極大。
2. 理論依據
在正式介紹JVM中常用的幾種垃圾回收算法之前,我們先來看一下JVM判斷待回收對象的基本思想:對任何“活”的對象,一定能最終追溯到其存活在堆棧或靜態存儲區之中的引用。這個引用鏈條可能會穿過數個對象層次。由此,如果你從堆棧和靜態存儲區開始,遍歷所有引用,就能找到所有“活”的對象。
即對于發現的每個引用,你必須追蹤它所引用的對象,然后是此對象包含的所有的引用,如此反復的執行,直到“根源于堆棧和靜態存儲區的引用”所形成的網絡全部被訪問為止。你所訪問過的所有對象必須都是“活”的。
注意,這就解決了“存在交互引用的整體對象”的問題,這些對象根本不會被發現,因此也就被自動回收了。
3. “停止——復制”算法
“停止——復制”算法是本篇將要介紹的三種JVM垃圾回收算法之一。顧名思義,這個算法需要先暫停程序的運行(因此它不屬于后臺回收模式),然后將所有“活”的對象從當前堆(堆A)復制到另一個堆(堆B),然后一次性回收整個堆A。
該算法的優點在于:當對象被復制到新堆時,它們是一個挨著一個的,所以新堆保持緊湊隊列,然后就可以按照前述方法簡單、直接地分配新空間了。
該算法主要有三個缺點:
缺點1:需要兩個堆,然后需要在兩個堆之間來回倒騰,從而使得維護比實際需要多一倍的空間。
缺點2:復制。當程序進入穩定狀態后,可能只會產生少量的垃圾,甚至沒有垃圾。盡管如此,該算法仍然會將所有內存自一處復制到另外一處,這很浪費。
缺點3:需要暫停程序的運行。當需要操作的堆空間較大時,耗費的時間是很可觀的。
4. “標記——清掃”算法
“標記——清掃”算法主要適用于垃圾較少的情況。
該算法同樣是要找出所有“活”的對象。每當它找到一個“活”對象,就會給對象設一個標記,這個過程中不會回收任何對象。只有全部標記工作完成時,清楚動作才會開始。在清除過程中,再次遍歷整個內存區域,把所有沒有標記的對象進行回收處理。
相對于“停止——復制”算法,“標記——清掃”算法具有如下優點:
優點1:支持用戶線程與垃圾收集線程并發執行(后臺回收模式),一開始會很短暫的停止一次所有線程來開始初始標記根對象,然后標記線程與應用線程與應用線程一起并發運行,最后又很短的暫停一次,多線程并行地重新標記之前可能因為并發而漏掉的對象,然后就開始與應用程序的并發清除過程。可見,最長的兩個遍歷過程都是與應用程序并發執行的,比“停止——復制”算法改進很多
優點2:當垃圾較少時,運行效率要比“停止——復制”方法高很多
但該算法也有其自身的缺點:
缺點:在清除過程中,釋放沒有被標記的對象,導致剩下的堆空間不是連續的,產生很多碎片。
5. “標記——整理”算法
綜合了上述兩種的做法和優點,先標記活躍對象,然后將其合并成較大的內存塊
三. 分代
分代是Java垃圾收集的一大亮點,根據對象的生命周期長短,把堆分為3個代:Young,Old和Permanent,根據不同代的特點采用不同的收集算法,揚長避短也。
1. Young(Nursery),年輕代
研究表明大部分對象都是朝生暮死,隨生隨滅的。因此所有收集器都為年輕代選擇了復制算法。
復制算法優點是只訪問活躍對象,缺點是復制成本高。因為年輕代只有少量的對象能熬到垃圾收集,因此只需少量的復制成本。而且復制收集器只訪問活躍對象,對那些占了最大比率的死對象視而不見,充分發揮了它遍歷空間成本低的優點。
Young的默認值為4M,隨堆內存增大,約為1/15,JVM會根據情況動態管理其大小變化。
-XX:NewRatio= 參數可以設置Young與Old的大小比例,-server時默認為1:2,但實際上young啟動時遠低于這個比率?如果信不過JVM,也可以用-Xmn硬性規定其大小,有文檔推薦設為Heap總大小的1/4。
Young里面又分為3個區域,一個Eden,所有新建對象都會存在于該區,兩個Survivor區,用來實施復制算法。每次復制就是將Eden和第一塊Survior的活對象復制到第2塊,然后清空Eden與第一塊Survior。Eden與Survivor的比例由-XX:SurvivorRatio=設置,默認為32。Survivio大了會浪費,小了的話,會使一些年輕對象潛逃到老人區,引起老人區的不安,但這個參數對性能并不重要。
2. Old(Tenured),年老代
年輕代的對象如果能夠挺過數次收集,就會進入老人區。老人區使用標記整理算法。因為老人區的對象都沒那么容易死的,采用復制算法就要反復的復制對象,很不合算,只好采用標記清理算法,但標記清理算法其實也不輕松,每次都要遍歷區域內所有對象,所以還是沒有免費的午餐啊。
-XX:MaxTenuringThreshold=設置熬過年輕代多少次收集后移入老人區,CMS中默認為0,熬過第一次GC就轉入,可以用-XX:+PrintTenuringDistribution查看。
3. Permanent,持久代
裝載Class信息等基礎數據,默認64M,如果是類很多很多的服務程序,需要加大其設置-XX:MaxPermSize=,否則它滿了之后會引起fullgc()或Out of Memory。 注意Spring,Hibernate這類喜歡AOP動態生成類的框架需要更多的持久代內存
4. minor/major collection
每個代滿了之后都會促發collection,(另外Concurrent Low Pause Collector默認在老人區68%的時候促發)。
GC用較高的頻率對young進行掃描和回收,這種叫做minor collection。
而因為成本關系對Old的檢查回收頻率要低很多,同時對Young和Old的收集稱為major collection。
System.gc()會引發major collection,使用-XX:+DisableExplicitGC禁止它,或設為CMS并發-XX:+ExplicitGCInvokesConcurrent
5. 小結
Young -- 復制算法
Old(Tenured) -- 標記清除/標記整理算法
四. 收集器
1.古老的串行收集器(Serial Collector)
使用 -XX:+UseSerialGC,策略為年輕代串行復制,年老代串行標記整理。
2.吞吐量優先的并行收集器(Throughput Collector)
使用 -XX:+UseParallelGC ,也是JDK5 -server的默認值。策略為:
1).年輕代暫停應用程序,多個垃圾收集線程并行的復制收集,線程數默認為CPU個數,CPU很多時,可用–XX:ParallelGCThreads=減少線程數。
2).年老代暫停應用程序,與串行收集器一樣,單垃圾收集線程標記整理。
所以需要2+的CPU時才會優于串行收集器,適用于后臺處理,科學計算。
可以使用-XX:MaxGCPauseMillis= 和 -XX:GCTimeRatio 來調整GC的時間。
3.暫停時間優先的并發收集器(Concurrent Low Pause Collector-CMS)
使用-XX:+UseConcMarkSweepGC,策略為:
1).年輕代同樣是暫停應用程序,多個垃圾收集線程并行的復制收集。
2).年老代則只有兩次短暫停,其他時間應用程序與收集線程并發的清除。
注意并行與并發的區別:并行指多條垃圾收集線程并行;并發指用戶線程與垃圾收集線程并發,程序在繼續運行,而垃圾收集程序運行于另一個個CPU上
五. 其他
Java虛擬機中有許多附加技術用以提升速度。尤其是與加載器操作有關的,被稱為“即時”(Just-In-Time,JIT)編譯的技術。這種技術可以把程序全部或部份翻譯成本地機器碼(這本來是Java虛擬機的工作),程序運行速度因此得以提升。
當需要裝載某個類(通常是在你為該類創建第一個對象)時,編譯器會先找到其.class文件,然后將該類的字節碼裝入內存。此時,有兩種方案可供選擇:
一種是就讓即使編譯器編譯所有代碼。但這個做法有兩個缺陷:這種加載動作散落在整個生命周期內,累加起來要花更多時間;并且會增加可執行代碼的長度(字節碼要比即時編譯器展開后的本地機器碼小很多),這將導致頁面調度,從而降低程序速度。
另一種做法稱為“惰性編譯(lazy evaluation)”,意思是即使編譯器只在必要的時候才編譯代碼。這樣,從不會被執行的代碼也許壓根就不會被JIT所編譯。JDK 1.4中的Java HotSpot技術就采用了類似方法,代碼每次被執行的時候都會做一些優化,所以執行的次數越多,它的速度越快。