垃圾回收機制是由垃圾收集器Garbage Collection
GC來實現的,GC是后臺的守護進程。它的特別之處是它是一個低優先級進程,但是可以根據內存的使用情況動態的調整他的優先級。因此,它是在內存中低到一定限度時才會自動運行,從而實現對內存的回收。這也是垃圾回收的時間不確定的原因為何要這樣設計:因為GC也是進程,也要消耗CPU等資源,如果GC執行過于頻繁會對java的程序的執行產生較大的影響(java解釋器本來就不快),因此JVM的設計者們選著了不定期的gc。
一、為什么要進行垃圾回收
我們知道Java是一門面向對象的語言,在一個系統運行中,會伴隨著很多對象的創建,而這些對象一旦創建了就占據了一定的內存,在上一篇中,我們介紹過創建的對象是保存在堆中的,當對象使用完畢之后,不對其進行清理,那么會一直占據內存空間,很明顯內存空間是有限的,如果不回收這些無用的對象占據的內存,那么新創建的對象申請不了內存空間,系統就會拋出異常而無法運行,所以必須要經常進行內存的回收,也就是垃圾收集。
二、回收
在堆里面存放著Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一 件事情就是要確定這些對象之中哪些還“存活”著,哪些已經“死去”(即不可能再被任何途徑 使用的對象)。
1、引用計數算法
引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,就將該對象實例分配給一個變量,該變量計數設置為1。當任何其它變量被賦值為這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命周期或者被設置為一個新值時,對象實例的引用計數器減1。任何引用計數器為0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1
優缺點
優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。
缺點:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0。
2、可達性分析算法
可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以后,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點,無用的節點將會被判定為是可回收的對象。
在Java語言中,可作為GC Roots的對象包括下面幾種:
a) 虛擬機棧中引用的對象(棧幀中的本地變量表);
b) 方法區中類靜態屬性引用的對象;
c) 方法區中常量引用的對象;
d) 本地方法棧中JNI(Native方法)引用的對象。
這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。
二、如何判斷對象為垃圾對象
在JVM中主要的垃圾收集算法有:標記-清除、標記-清除-壓縮(簡稱**“標記-整理”)、標記-復制-清除(簡稱“復制”)、分代收集算法**。這幾種收集算法互相配合,針對不同的內存區域采取對應的收集算法實現(這里具體是由相應的垃圾收集器實現)
垃圾回收涉及到大量的程序細節,而且各個平臺的虛擬機操作內存的方式也不一樣,但是他們進行垃圾回收的算法是通用的,所以這里我們也只介紹幾種通用算法。
①、標記-清除算法
算法實現:分為標記-清除兩個階段,首先根據上面的根搜索算法標記出所有需要回收的對象,在標記完成后,然后在統一回收掉所有被標記的對象。
缺點:
1、效率低:標記和清除這兩個過程的效率都不高。
2、容易產生內存碎片:因為內存的申請通常不是連續的,那么清除一些對象后,那么就會產生大量不連續的內存碎片,而碎片太多時,當有個大對象需要分配內存時,便會造成沒有足夠的連續內存分配而提前觸發垃圾回收,甚至直接拋出OutOfMemoryExecption。
②、復制算法
為了解決標記-清除算法的兩個缺點,復制算法誕生了。
算法實現:將可用內存按容量劃分為大小相等的兩塊區域,每次只使用其中一塊,當這一塊的內存用完了,就將還活著的對象復制到另一塊區域上,然后再把已使用過的內存空間一次性清理掉。
優點:每次都是只對其中一塊內存進行回收,不用考慮內存碎片的問題,而且分配內存時,只需要移動堆頂指針,按順序進行分配即可,簡單高效。
缺點:將內存分為兩塊,但是每次只能使用一塊,也就是說,機器的一半內存是閑置的,這資源浪費有點嚴重。并且如果對象存活率較高,每次都需要復制大量的對象,效率也會變得很低。
③、標記-整理算法
上面我們說過復制算法會浪費一半的內存,并且對象存活率較高時,會有過多的復制操作,效率低下。
如果對象存活率很高,基本上不會進行垃圾回收時,標記-整理算法誕生了。
算法實現:首先標記出所有存活的對象,然后讓所有存活對象向一端進行移動,最后直接清理到端邊界以外的內存。
局限性:只有對象存活率很高的情況下,使用該算法才會效率較高。
④、分代收集算法
當前商業虛擬機都是采用此算法,但是其實這不是什么新的算法,而是上面幾種算法的合集。
算法實現:根據對象的存活周期不同將內存分為幾塊,然后不同的區域采用不同的回收算法。
對于 HotSpot 虛擬機,它將堆空間分為老年代和新生代兩塊區域
1、對于存活周期較短,每次都有大批對象死亡,只有少量存活的區域,采用復制算法,因為只需要付出少量存活對象的復制成本即可完成收集;
2、對于存活周期較長,沒有額外空間進行分配擔保的區域,采用標記-整理算法,或者標記-清除算法。
堆有新生代和老年代兩塊區域組成,而新生代區域又分為三個部分,分別是 Eden,From Surivor,To Survivor ,比例是8:1:1。
新生代采用復制算法,每次使用一塊Eden區和一塊Survivor區,當進行垃圾回收時,將Eden和一塊Survivor區域的所有存活對象復制到另一塊Survivor區域,然后清理到剛存放對象的區域,依次循環。
老年代采用標記-清除或者標記-整理算法,根據使用的垃圾回收器來進行判斷。
至于為什么要這樣,這是由于內存分配的機制導致的,新生代存的基本上都是朝生夕死的對象,而老年代存放的都是存活率很高的對象。關于內存分配下篇博客我們會詳細進行介紹。
四、何時進行垃圾回收
理清了什么是垃圾,怎么回收垃圾,最后一點就是Java虛擬機何時進行垃圾回收呢?
程序員可以調用 System.gc()方法,手動回收,但是調用此方法表示希望進行一次垃圾回收。但是它不能保證垃圾回收一定會進行,而且具體什么時候進行是取決于具體的虛擬機的,不同的虛擬機有不同的對策。
其次虛擬機會自行根據當前內存大小,判斷何時進行垃圾回收,比如前面所說的,新生代滿了,新產生的對象無法分配內存時,便會觸發垃圾回收機制。
這里需要說明的是宣告一個對象死亡,至少要經歷兩次標記,前面我們說過,如果對象與GC Roots 不可達,那么此對象會被第一次標記并進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法,當對象沒有覆蓋 finalize()方法,或者該方法已經執行了一次,那么虛擬機都將視為沒有必要執行finalize()方法。
如果這個對象有必要執行 finalize() 方法,那么該對象將會被放置在一個有虛擬機自動建立、低優先級,名為 F-Queue 隊列中,GC會對F-Queue進行第二次標記,如果對象在finalize() 方法中成功拯救了自己(比如重新與GC Roots建立連接),那么第二次標記時,就會將該對象移除即將回收的集合,否則就會被回收。