讀書筆記:《深入理解java虛擬機》(二)

一. 垃圾回收

垃圾回收指的是將已死對象所佔內存空間釋放,避免內存泄漏。java垃圾收集器是針對java堆和方法區。垃圾回收需要考慮3個問題:哪些內存需要回收?什麼時候回收?如何回收?

在java堆裏面存放了幾乎所有對象實例,回收前需確定這些對象哪些還“活着”,哪些已經“死去”。通常有2種方法。

引用計數法:通過在對象請求頭分配一個空間來保存被引用次數,每當一個地方引用它時,計數就加一;當引用失效時,計數就減一,任何時刻當計數爲0時,對象就會被回收。但此方法很難解決對象間相互循環引用的問題,比如定義兩個對象,並相互引用,然後置空各自聲明引用,此時兩個對象已經不可能被引用訪問了,但由於它們互相引用着對方,計數不爲0,於是引用計數算法無法通知GC收集器回收它們。

可達性分析算法:通過一系列稱爲“GC Roots”的對象爲起點,從這些節點開始向下搜索,所走過的路徑稱爲引用鏈,當一個對象到GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。在java語言中,可作爲GC Roots 的對象有以下幾種。

a.虛擬機棧(棧幀中的本地變量表)中所引用的對象。

b.方法區中類靜態屬性引用的對象

c.方法區中常量引用的對象

d.本地方法棧中JNI(即一般說的Native方法)引用的對象

關於引用:在JDK1.2之前,java中引用的定義:如果reference類型的數據中類型中存儲的數值代表的是另外一個內存的起始地址,就稱這塊內存代表着一個引用。在這種定義下,對象只有被引用和沒有被引用兩種狀態。我們希望有一類對象,當內存空間還足夠時,則保留在內存中,如果內存空間進行垃圾回收後還是十分緊張,則可以拋棄這些對象。在JDK1.2之後,java對引用概念進行擴充,分爲強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference),虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

a.強引用:類似於“Object object = new Object()”這種引用,只要強引用在,垃圾收集器永遠不會回收掉被引用的對象。

b.軟引用:用來描述一些還有用但並非必需的對象。對於這種對象,在將發生內存溢出之前將會把這些對象列進回收範圍之中進行第二次回收。提供了SoftReference類來實現軟引用。

c.弱引用:用來描述非必需對象,強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集之前。提供了WeakReference類來實現弱引用。

d.虛引用:也稱幽靈引用或幻影引用,它是最弱的引用關係。一個對象是否有虛引用完全不會影響其生存時間,也不能通過虛引用來獲取對象實例。爲一個對象設置虛引用關聯目的只是在對象回收時收到一個系統通知。提供PhantomReference類。

關於回收:一個對象真正被回收,至少經歷兩次標記過程:如果對象在進行可達性分析後發現不可達,那它將會被第一次標記並且進行一次篩選,條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋此方法,或者此方法已被虛擬機調用過,這兩種情況都視爲沒有必要執行。如果對象判定需要執行,那麼此對象會放置在一個叫做F-Queue隊列中,並在稍後由一個虛擬機自動建立的,低優先級的Finalizer線程去執行,即觸發此方法,但不保證會等待它運行結束,稍後GC將對隊列中的對象進行第二次小規模標記,如果對象在finalize()方法中成功拯救自己(變成可達),那麼第二次標記時它將被移除出“即將回收集合”,否則基本被回收了(finalize()方法只會被虛擬機調用一次,面臨下次回收時無法通過finalize()自救)。

關於回收方法區:java虛擬機規範說過可以不要求虛擬機在方法區進行垃圾回收,而且在方法區回收性價比比較低。方法區主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量跟回收java堆中的對象非常類似,如果一個字符串“abc”已經進入常量池,但是當前系統沒有一個String對象引用這個常量,也沒有其他地方引用,如果此時發生回收,“abc”常量就會被清出常量池,常量池中其他類,接口,方法,字段的符號引用也與此相似。判斷一個常量是否廢棄比較簡單,但判斷一個類是否無用條件比較苛刻,需滿足3個條件:①該類所有實例都已經被回收②加載該類的ClassLoader已經被回收 ③該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類方法。滿足3個條件及可以進行回收,但不是必然。在大量使用反射,動態代理,CGLib等框架,動態生成JSP及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載功能,保證方法區不會溢出。

那如何回收呢?有以下幾種算法。

a.標記—清除算法。算法分爲標記和清除兩個階段。首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。這是最基礎的算法,後面的算法都是基於這種思想並改進的,它主要不足有兩個:一是效率問題,標記跟清除兩個過程效率都不高;另一個是空間問題,標記清除後會產生大量不連續的內存碎片,太多碎片導致後面需要分配較大對象時,無法找到足夠連續內存而不得不提前觸發另一次垃圾收集。

b.複製算法。爲了解決效率問題,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊用完了,就將還存活着的對象複製到另一塊上面,然後把已使用過的內存清理掉,內存分配時也不用考慮內存碎片,只要移動指針,按順序分配即可只是這種算法代價是將內存縮小爲原來的一半,代價太大。

c.標記—整理算法。標記過程和標記—清除算法一樣,但後續不是直接對可回收對象進行清除,而是讓所有存活對象向一端移動,然後直接清除掉端邊界以外內存。

d.分代收集法。根據對象的存活週期的不同把內存劃分爲幾塊。一般把java堆分爲新生代和老年代,這樣根據不同年代特點選擇不同算法,在新生代中,每次垃圾回收都發現有大量對象死去,那就選擇複製算法,而老年代中因爲對象存活率高,就必須使用標記清理或標記整理算法進行回收。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章