1. 垃圾回收的意義
在C++中,對象所佔的內存在程序結束運行以前一直被佔用,在明確釋放以前不能分配給其它對象;而在Java中,當沒有對象引用指向原先分配給某個對象的內存時,該內存便成爲垃圾。JVM的一個系統級線程會自動釋放該內存塊。垃圾回收意味着程序再也不須要的對象是"無用信息",這些信息將被丟棄。當一個對象再也不被引用的時候,內存回收它佔領的空間,以便空間被後來的新對象使用。事實上,除了釋放沒用的對象,垃圾回收也能夠清除內存記錄碎片。因爲建立對象和垃圾回收器釋放丟棄對象所佔的內存空間,內存會出現碎片。碎片是分配給對象的內存塊之間的空閒內存洞。碎片整理將所佔用的堆內存移到堆的一端,JVM將整理出的內存分配給新的對象。
垃圾回收能自動釋放內存空間,減輕編程的負擔。這使Java 虛擬機具備一些優勢。首先,它能使編程效率提升。在沒有垃圾回收機制的時候,可能要花許多時間來解決一個難懂的存儲器問題。在用Java語言編程的時候,靠垃圾回收機制可大大縮短期。其次是它保護程序的完整性, 垃圾回收是Java語言安全性策略的一個重要部份。
垃圾回收的一個潛在的缺點是它的開銷影響程序性能。Java虛擬機必須追蹤運行程序中有用的對象,並且最終釋放沒用的對象。這一個過程須要花費處理器的時間。其次垃圾回收算法的不完備性,早先採用的某些垃圾回收算法就不能保證100%收集到全部的廢棄內存。固然隨着垃圾回收算法的不斷改進以及軟硬件運行效率的不斷提高,這些問題均可以迎刃而解。
2. 垃圾收集的算法分析
Java語言規範沒有明確地說明JVM使用哪一種垃圾回收算法,可是任何一種垃圾回收算法通常要作2件基本的事情:(1)發現無用信息對象;(2)回收被無用對象佔用的內存空間,使該空間可被程序再次使用。
大多數垃圾回收算法使用了根集(root set)這個概念;所謂根集就是正在執行的Java程序能夠訪問的引用變量的集合(包括局部變量、參數、類變量),程序可使用引用變量訪問對象的屬性和調用對象的方法。垃圾回收首先須要肯定從根開始哪些是可達的和哪些是不可達的,從根集可達的對象都是活動對象,它們不能做爲垃圾被回收,這也包括從根集間接可達的對象。而根集經過任意路徑不可達的對象符合垃圾收集的條件,應該被回收。下面介紹幾個經常使用的算法。
2.1. 引用計數法(Reference Counting Collector)
引用計數法是惟一沒有使用根集的垃圾回收的法,該算法使用引用計數器來區分存活對象和再也不使用的對象。通常來講,堆中的每一個對象對應一個引用計數器。當每一次建立一個對象並賦給一個變量時,引用計數器置爲1。當對象被賦給任意變量時,引用計數器每次加1當對象出了做用域後(該對象丟棄再也不使用),引用計數器減1,一旦引用計數器爲0,對象就知足了垃圾收集的條件。
基於引用計數器的垃圾收集器運行較快,不會長時間中斷程序執行,適宜地必須實時運行的程序。但引用計數器增長了程序執行的開銷,由於每次對象賦給新的變量,計數器加1,而每次現有對象出了做用域生,計數器減1。
2.2. tracing算法(Tracing Collector)
tracing算法是爲了解決引用計數法的問題而提出,它使用了根集的概念。基於tracing算法的垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每一個可達對象設置一個或多個位。在掃描識別過程當中,基於tracing算法的垃圾收集也稱爲標記和清除(mark-and-sweep)垃圾收集器.
2.3. compacting算法(Compacting Collector)
爲了解決堆碎片問題,基於tracing的垃圾回收吸取了Compacting算法的思想,在清除的過程當中,算法將全部的對象移到堆的一端,堆的另外一端就變成了一個相鄰的空閒內存區,收集器會對它移動的全部對象的全部引用進行更新,使得這些引用在新的位置能識別原來的對象。在基於Compacting算法的收集器的實現中,通常增長句柄和句柄表。
2.4. copying算法(Coping Collector)
該算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分紅一個對象區和多個空閒區,程序從對象區爲對象分配空間,當對象滿了,基於coping算法的垃圾回收就從根集中掃描活動對象,並將每一個活動對象複製到空閒區(使得活動對象所佔的內存之間沒有空閒間隔),這樣空閒區變成了對象區,原來的對象區變成了空閒區,程序會在新的對象區中分配內存。
一種典型的基於coping算法的垃圾回收是stop-and-copy算法,它將堆分紅對象區和空閒區域區,在對象區與空閒區域的切換過程當中,程序暫停執行。
2.5. generation算法(Generational Collector)
stop-and-copy垃圾收集器的一個缺陷是收集器必須複製全部的活動對象,這增長了程序等待時間,這是coping算法低效的緣由。在程序設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。所以,generation算法將堆分紅兩個或多個,每一個子堆做爲對象的一代 (generation)。因爲多數對象存在的時間比較短,隨着程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行後,上次運行存活下來的對象移到下一最高代的子堆中,因爲老一代的子堆不會常常被回收,於是節省了時間。
2.6. adaptive算法(Adaptive Collector)
在特定的狀況下,一些垃圾收集算法會優於其它算法。基於Adaptive算法的垃圾收集器就是監控當前堆的使用狀況,並將選擇適當算法的垃圾收集器。java
3. System.gc()方法程序員
命令行參數透視垃圾收集器的運行
使用System.gc()能夠無論JVM使用的是哪種垃圾回收的算法,均可以請求Java的垃圾回收。在命令行中有一個參數-verbosegc能夠查看Java使用的堆內存的狀況,它的格式以下:
java -verbosegc classfile
能夠看個例子:
算法
在這個例子中,一個新的對象被建立,因爲它沒有使用,因此該對象迅速地變爲不可達,程序編譯後,執行命令: java -verbosegc TestGC 後結果爲:
[Full GC 168K->97K(1984K), 0.0253873 secs]
機器的環境爲,Windows 2000 + JDK1.3.1,箭頭先後的數據168K和97K分別表示垃圾收集GC先後全部存活對象使用的內存容量,說明有168K-97K=71K的對象容量被回收,括號內的數據1984K爲堆內存的總容量,收集所須要的時間是0.0253873秒(這個時間在每次執行的時候會有所不一樣)。編程
須要注意的是,調用System.gc()也僅僅是一個請求(建議)。JVM接受這個消息後,並非當即作垃圾回收,而只是對幾個垃圾回收算法作了加權,使垃圾回收操做容易發生,或提前發生,或回收較多而已。安全
4. finalize()方法數據結構
在JVM垃圾回收器收集一個對象以前,通常要求程序調用適當的方法釋放資源,但在沒有明確釋放資源的狀況下,Java提供了缺省機制來終止該對象心釋放資源,這個方法就是finalize()。它的原型爲:
protected void finalize() throws Throwable
在finalize()方法返回以後,對象消失,垃圾收集開始執行。原型中的throws Throwable表示它能夠拋出任何類型的異常。
之因此要使用finalize(),是存在着垃圾回收器不能處理的特殊狀況。假定你的對象(並不是使用new方法)得到了一塊「特殊」的內存區域,因爲垃圾回收器只知道那些顯示地經由new分配的內存空間,因此它不知道該如何釋放這塊「特殊」的內存區域,那麼這個時候java容許在類中定義一個由finalize()方法。多線程
特殊的區域例如:1)因爲在分配內存的時候可能採用了相似 C語言的作法,而非JAVA的一般new作法。這種狀況主要發生在native method中,好比native method調用了C/C++方法malloc()函數系列來分配存儲空間,可是除非調用free()函數,不然這些內存空間將不會獲得釋放,那麼這個時候就可能形成內存泄漏。可是因爲free()方法是在C/C++中的函數,因此finalize()中能夠用本地方法來調用它。以釋放這些「特殊」的內存空間。2)又或者打開的文件資源,這些資源不屬於垃圾回收器的回收範圍。
換言之,finalize()的主要用途是釋放一些其餘作法開闢的內存空間,以及作一些清理工做。由於在JAVA中並無提夠像「析構」函數或者相似概念的函數,要作一些相似清理工做的時候,必須本身動手建立一個執行清理工做的普通方法,也就是override Object這個類中的finalize()方法。例如,假設某一個對象在建立過程當中會將本身繪製到屏幕上,若是不是明確地從屏幕上將其擦出,它可能永遠都不會被清理。若是在finalize()加入某一種擦除功能,當GC工做時,finalize()獲得了調用,圖像就會被擦除。要是GC沒有發生,那麼這個圖像就會框架
被一直保存下來。ide
一旦垃圾回收器準備好釋放對象佔用的存儲空間,首先會去調用finalize()方法進行一些必要的清理工做。只有到下一次再進行垃圾回收動做的時候,纔會真正釋放這個對象所佔用的內存空間。
在普通的清除工做中,爲清除一個對象,那個對象的用戶必須在但願進行清除的地點調用一個清除方法。這與C++"析構函數"的概念稍有抵觸。在C++中,全部對象都會破壞(清除)。或者換句話說,全部對象都"應該"破壞。若將C++對象建立成一個本地對象,好比在堆棧中建立(在Java中是不可能的,Java都在堆中),那麼清除或破壞工做就會在"結束花括號"所表明的、建立這個對象的做用域的末尾進行。若對象是用new建立的(相似於Java),那麼當程序員調用C++的 delete命令時(Java沒有這個命令),就會調用相應的析構函數。若程序員忘記了,那麼永遠不會調用析構函數,咱們最終獲得的將是一個內存"漏洞",另外還包括對象的其餘部分永遠不會獲得清除。
相反,Java不容許咱們建立本地(局部)對象--不管如何都要使用new。但在Java中,沒有"delete"命令來釋放對象,由於垃圾回收器會幫助咱們自動釋放存儲空間。因此若是站在比較簡化的立場,咱們能夠說正是因爲存在垃圾回收機制,因此Java沒有析構函數。然而,隨着之後學習的深刻,就會知道垃圾收集器的存在並不能徹底消除對析構函數的須要,或者說不能消除對析構函數表明的那種機制的須要(緣由見下一段。另外finalize()函數是在垃圾回收器準備釋放對象佔用的存儲空間的時候被調用的,絕對不能直接調用finalize(),因此應儘可能避免用它)。若但願執行除釋放存儲空間以外的其餘某種形式的清除工做,仍然必須調用Java中的一個方法。它等價於C++的析構函數,只是沒後者方便。
在C++中全部的對象運用delete()必定會被銷燬,而JAVA裏的對象並不是總會被垃圾回收器回收。In another word, 1 對象可能不被垃圾回收,2 垃圾回收並不等於「析構」,3 垃圾回收只與內存有關。也就是說,並非若是一個對象再也不被使用,是否是要在finalize()中釋放這個對象中含有的其它對象呢?不是的。由於不管對象是如何建立的,垃圾回收器都會負責釋放那些對象佔有的內存。函數
5. 觸發主GC(Garbage Collector)的條件
JVM進行次GC的頻率很高,但由於這種GC佔用時間極短,因此對系統產生的影響不大。更值得關注的是主GC的觸發條件,由於它對系統影響很明顯。總的來講,有兩個條件會觸發主GC:
1)當應用程序空閒時,即沒有應用線程在運行時,GC會被調用。由於GC在優先級最低的線程中進行,因此當應用忙時,GC線程就不會被調用,但如下條件除外。
2)Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程當中建立新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次以後仍不能知足內存分配的要求,JVM會再進行兩次GC做進一步的嘗試,若仍沒法知足要求,則 JVM將報「out of memory」的錯誤,Java應用將中止。
因爲是否進行主GC由JVM根據系統環境決定,而系統環境在不斷的變化當中,因此主GC的運行具備不肯定性,沒法預計它什麼時候必然出現,但能夠肯定的是對一個長期運行的應用來講,其主GC是反覆進行的。
6. 減小GC開銷的措施
根據上述GC的機制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特色進行設計和編碼,就會出現內存駐留等一系列負面影響。爲了不這些影響,基本的原則就是儘量地減小垃圾和減小GC過程當中的開銷。具體措施包括如下幾個方面:
(1)不要顯式調用System.gc()
此函數建議JVM進行主GC,雖然只是建議而非必定,但不少狀況下它會觸發主GC,從而增長主GC的頻率,也即增長了間歇性停頓的次數。
(2)儘可能減小臨時對象的使用
臨時對象在跳出函數調用後,會成爲垃圾,少用臨時變量就至關於減小了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減小了主GC的機會。
(3)對象不用時最好顯式置爲Null
通常而言,爲Null的對象都會被做爲垃圾處理,因此將不用的對象顯式地設爲Null,有利於GC收集器斷定垃圾,從而提升了GC的效率。
(4)儘可能使用StringBuffer,而不用String來累加字符串
因爲String是固定長的字符串對象,累加String對象時,並不是在一個String對象中擴增,而是從新建立新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程當中會產生多個垃圾對象,由於對次做「+」操做時都必須建立新的String對象,但這些過渡對象對系統來講是沒有實際意義的,只會增長更多的垃圾。避免這種狀況能夠改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。
(5)能用基本類型如Int,Long,就不用Integer,Long對象
基本類型變量佔用的內存資源比相應對象佔用的少得多,若是沒有必要,最好使用基本變量。
(6)儘可能少用靜態對象變量
靜態變量屬於全局變量,不會被GC回收,它們會一直佔用內存。
(7)分散對象建立或刪除的時間
集中在短期內大量建立新對象,特別是大對象,會致使忽然須要大量內存,JVM在面臨這種狀況時,只能進行主GC,以回收內存或整合內存碎片,從而增長主GC的頻率。集中刪除對象,道理也是同樣的。它使得忽然出現了大量的垃圾對象,空閒空間必然減小,從而大大增長了下一次建立新對象時強制主GC的機會。
下面這個例子向你們展現了垃圾收集所經歷的過程,並對前面的陳述進行了總結。
上面這個程序建立了許多Chair對象,並且在垃圾收集器開始運行後的某些時候,程序會中止建立Chair。因爲垃圾收集器可能在任什麼時候間運行,因此咱們不能準確知道它在什麼時候啓動。所以,程序用一個名爲gcrun的標記來指出垃圾收集器是否已經開始運行。利用第二個標記f,Chair可告訴main()它應中止對象的生成。這兩個標記都是在finalize()內部設置的,它調用於垃圾收集期間。另兩個static變量--created以及 finalized--分別用於跟蹤已建立的對象數量以及垃圾收集器已進行完收尾工做的對象數量。最後,每一個Chair都有它本身的(非 static)int i,因此能跟蹤瞭解它具體的編號是多少。編號爲47的Chair進行完收尾工做後,標記會設爲true,最終結束Chair對象的建立過程。
7. 關於垃圾回收的幾點補充 通過上述的說明,能夠發現垃圾回收有如下的幾個特色: (1)垃圾收集發生的不可預知性:因爲實現了不一樣的垃圾回收算法和採用了不一樣的收集機制,因此它有多是定時發生,有多是當出現系統空閒CPU資源時發生,也有多是和原始的垃圾收集同樣,等到內存消耗出現極限時發生,這與垃圾收集器的選擇和具體的設置都有關係。 (2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器可以精確標記活着的對象;(b)垃圾收集器可以精確地定位對象之間的引用關係。前者是徹底地回收全部廢棄對象的前提,不然就可能形成內存泄漏。然後者則是實現歸併和複製等算法的必要條件。全部不可達對象都可以可靠地獲得回收,全部對象都可以從新分配,容許對象的複製和對象內存的縮並,這樣就有效地防止內存的支離破碎。 (3)如今有許多種不一樣的垃圾收集器,每種有其算法且其表現各異,既有當垃圾收集開始時就中止應用程序的運行,又有當垃圾收集開始時也容許應用程序的線程運行,還有在同一時間垃圾收集多線程運行。 (4)垃圾收集的實現和具體的JVM 以及JVM的內存模型有很是緊密的關係。不一樣的JVM 可能採用不一樣的垃圾收集,而JVM 的內存模型決定着該JVM能夠採用哪些類型垃圾收集。如今,HotSpot 系列JVM中的內存系統都採用先進的面向對象的框架設計,這使得該系列JVM均可以採用最早進的垃圾收集。 (5)隨着技術的發展,現代垃圾收集技術提供許多可選的垃圾收集器,並且在配置每種收集器的時候又能夠設置不一樣的參數,這就使得根據不一樣的應用環境得到最優的應用性能成爲可能。 針對以上特色,咱們在使用的時候要注意: (1)不要試圖去假定垃圾收集發生的時間,這一切都是未知的。好比,方法中的一個臨時對象在方法調用完畢後就變成了無用對象,這個時候它的內存就能夠被釋放。 (2)Java中提供了一些和垃圾收集打交道的類,並且提供了一種強行執行垃圾收集的方法--調用System.gc(),但這一樣是個不肯定的方法。Java 中並不保證每次調用該方法就必定可以啓動垃圾收集,它只不過會向JVM發出這樣一個申請,究竟是否真正執行垃圾收集,一切都是個未知數。 (3)挑選適合本身的垃圾收集器。通常來講,若是系統沒有特殊和苛刻的性能要求,能夠採用JVM的缺省選項。不然能夠考慮使用有針對性的垃圾收集器,好比增量收集器就比較適合實時性要求較高的系統之中。系統具備較高的配置,有比較多的閒置資源,能夠考慮使用並行標記/清除收集器。 (4)關鍵的也是難把握的問題是內存泄漏。良好的編程習慣和嚴謹的編程態度永遠是最重要的,不要讓本身的一個小錯誤致使內存出現大漏洞。 (5)儘早釋放無用對象的引用。大多數程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設置爲null,暗示垃圾收集器來收集該對象,還必須注意該引用的對象是否被監聽,若是有,則要去掉監聽器,而後再賦空值。