目的:
使用垃圾回收器的唯一原因就是:回收程序不再使用的內存。
針對的目標對象:
Java的垃圾回收器會自動回收不再使用的Java對象,釋放內存。但是回收的是用new創建的,分配在堆上的內存。
finalize():
那麼,如果不是用這種方式創建的對象,該怎麼回收?比如:Java調用了本地的c語言方法創建了個對象,那麼這時,該對象不是放在堆上的。除非你手動去調用c的free()方法,否則,這個對象將永遠不會被清理。
Java的finalize()方法可以解決上面的問題。垃圾回收器在回收垃圾對象時,會首先去調用該對象的finalize()方法。所以,你可以在finalize()方法中調用c的free()方法。
一般教科書會寫,finalize()用於垃圾回收之前的清理工作,而實際上,除了上面講的極少數情況之外,我們一般情況下並不需要使用finalize()。
不保證發生:
雖然Java的垃圾回收器會根據對象的使用情況自動清理內存,但並不一定會發生,如果內存還夠用的話,虛擬機一般是不會浪費時間去作清理工作的。
如何判斷Java對象可以回收:
1.不被使用的“引用計數器法”:
每個對象都含有一個引用計數器,當有引用變量指向該對象時,引用計數器+1,當這個引用變量不再指向該對象,或者被置為null時,計數器-1。如下圖:
當第四種情況發生時,即:沒有引用變量指向“李四”那個對象了,這時,垃圾回收器在恰當的時候就會把李四所在的對象回收掉。
它簡單便捷,但是之所以沒被Java虛擬機采用的原因是:無法解決循環引用的問題。舉個簡單的例子:
objA有個instance變量,objB也有個instance變量,讓objA的instance指向B對象,而讓objB的instance變量指向A對象,那麼,B對象和A對象的引用計數器都是1,不為0,如果按照引用計數器的方法,A和B就不能被回收,但事實是,objA和objB這兩個引用變量已經是null了(它們指向的具體對象已經不再被引用了)。
2.根搜索算法
在主流的商用程序語言中(Java和C#,甚至古老的人Lisp語言),都是使用根搜索算法(GC Roots Tracing)判定對象是否存活的。
之前講過,對象的引用是放在棧中的,常量的引用是放在常量池之中的。如圖:
根搜索算法的思想是,從常量池和棧中的引用變量開始遍歷所有的引用變量,找到所有的活的對象(引用不為null)。然後再繼續尋找這個對象所包含的所有引用,反復進行,直到所有引用網絡被訪問完為止。
常量池或棧中的引用變量是根節點,擴展出的整個網絡就是一個引用鏈。最後,如果最終發現有對象到根節點的路徑是不可達的,說明這個對象是可回收的,這就解決了循環引用的問題:
如上圖,GCRoots是根節點,object5、6、7雖然各自引用,但是它們到GCRoots都是不可達的,所以,它們是可以被回收的。
怎樣回收?
每個虛擬機采用的回收算法是不同的,經典的案例如下:
標記-清除算法:
在使用“根搜索算法”尋找引用變量的同時,虛擬機會給每個存活的對象做一個標記,全部標記完成的時候才進行清除工作。
這樣的問題是,存活的對象在堆中不是連續存儲的,那麼清除“死亡”對象後,內存中就會留下大量碎片,如果在後面需要用到大內存對象時,內存空間不夠,就要重新整理內存。如圖回收前:
回收後:
復制算法:
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用過的內存空間一次清理掉。如圖回收前:
回收後(把存活著的對象搬到右側,左側剩下的就都是可清理的,然後統統清理掉。當右側需要清理的時候,類似的,把存活的對象再搬到左側,然後清空右側):
這種方式的缺點:很顯然,可用內存只有原來的一半兒。還有個缺點:如果左側大量的都是存活的對象,清理時仍然要全部搬到右側,很浪費時間。
現在的商業虛擬機都采用這種收集算法,但是保留區與運作區的比例有不同,且詳細又將堆內存劃分為新生代、老年代。新生代 ( Young ) 又被劃分為三個區域:Eden、From Survivor、To Survivor。關於新生代、老年代、堆內存等,詳細可查閱關於Java虛擬機的資料了解。