當我們的程序開啟運行之后就,就會在我們的java堆中不斷的產生新的對象,而這是需要占用我們的存儲空間的,因為創建一個新的對象需要分配對應的內存空間,顯然我的內存空間是固定有限的,所以我們需要對沒有用的對象進行回收,本文就來記錄下JVM中對象的銷毀過程。
1.怎么判斷對象是沒用的了
引用計數算法
我們在很多場景中會聽到java對象判斷存活的方式是計算該對象的引用計數器是否為0,如果為0就說明沒有其他變量引用該對象了,這個對象就可以被垃圾收集器回收了。但事實上JVM並不是采用該算法來判斷對象是否可以回收的,比如objectA.a=objectB及objectB.b=objectA除此之外沒有其他引用了。但是按照引用計數算法是不會回收這兩個對象的。但是這兩個對象也已經不能被其他對象訪問了,所以這就是問題。
可達性分析算法
java中判斷對象是否可以回收是通過可達性分析算法來實現的。如下圖:
在上圖中object5,object6及object7這三個對象雖然有相互之間的引用,但是通過GC Roots對象並不能引用到這三個對象,所以這三個對象是滿足回收條件的,而對象1到4通過GC Roots可達,所以這幾個對象任然存活。
GC Roots並不是一個對象,而是一組對象,在java中可以作為GC Roots對象的有如下幾種:
序號 | 類型 |
---|---|
1 | 虛擬機棧(本地變量表)中引用的對象 |
2 | 方法區中類靜態屬性引用的對象 |
3 | 方法區中常量引用的對象 |
4 | 本地方法棧中JNI(一般說的Native方法)引用的對象 |
2.對象的引用分類
判斷對象是否存活我們是通過GC Roots的引用可達性來判斷的,但是引用關系並不止一種,而是有四種分別是:強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference)和虛引用(Phantom Reference).引用強度依次減弱。
強引用
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMmoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用 對象來解決內存不足的問題。
軟引用
軟引用是用來描述一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
/**
* 軟引用:緩存場景的使用
* @author dengp
*
*/
public class SoftReferenceTest {
/**
* 運行參數 -Xmx200m -XX:+PrintGC
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
//存儲100M的緩存數據
byte[] cacheData = new byte[100 * 1024 * 1024];
//將緩存數據用軟引用持有
SoftReference<byte[]> cacheRef = new SoftReference<>(cacheData);
//將緩存數據的強引用去除
cacheData = null;
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//進行一次GC后查看對象的回收情況
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//在分配一個120M的對象,看看緩存對象的回收情況
// 空間不夠
byte[] newData = new byte[120 * 1024 * 1024];
System.out.println("分配后" + cacheData);
System.out.println("分配后" + cacheRef.get());
}
}
輸出結果
第一次GC前null
第一次GC前[B@15db9742
[GC (System.gc()) 104396K->103072K(175104K), 0.0054505 secs]
[Full GC (System.gc()) 103072K->102931K(175104K), 0.0095426 secs]
第一次GC后null
第一次GC后[B@15db9742
[GC (Allocation Failure) 103597K->102995K(175104K), 0.0099572 secs]
[GC (Allocation Failure) 102995K->102963K(175104K), 0.0044781 secs]
[Full GC (Allocation Failure) 102963K->102931K(175104K), 0.0226699 secs]
[GC (Allocation Failure) 102931K->102931K(199680K), 0.0022288 secs]
[Full GC (Allocation Failure) 102931K->519K(131072K), 0.0226120 secs]
分配后null
分配后null
從上面的示例中就能看出,軟引用關聯的對象不會被GC回收。JVM在分配空間時,若果Heap空間不足,就會進行相應的GC,但是這次GC並不會收集軟引用關聯的對象,但是在JVM發現就算進行了一次回收后還是不足(Allocation Failure),JVM會嘗試第二次GC,回收軟引用關聯的對象。
像這種如果內存充足,GC時就保留,內存不夠,GC再來收集的功能很適合用在緩存的引用場景中。在使用緩存時有一個原則,如果緩存中有就從緩存獲取,如果沒有就從數據庫中獲取,緩存的存在是為了加快計算速度,如果因為緩存導致了內存不足進而整個程序崩潰,那就得不償失了。
弱引用
弱引用也是用來描述非必須對象的,他的強度比軟引用更弱一些,被弱引用關聯的對象,在垃圾回收時,如果這個對象只被弱引用關聯(沒有任何強引用關聯他),那么這個對象就會被回收
/**
* 弱引用
* @author dengp
*
*/
public class WeakReferenceTest {
/**
* 運行參數 -Xmx200m -XX:+PrintGC
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
//存儲100M的緩存數據
byte[] cacheData = new byte[100 * 1024 * 1024];
//將緩存數據用軟引用持有
WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
//將緩存數據的強引用去除
cacheData = null;
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//進行一次GC后查看對象的回收情況
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//在分配一個120M的對象,看看緩存對象的回收情況
byte[] newData = new byte[120 * 1024 * 1024];
System.out.println("分配后" + cacheData);
System.out.println("分配后" + cacheRef.get());
}
}
輸出結果
第一次GC前null
第一次GC前[B@15db9742
[GC (System.gc()) 104396K->103072K(175104K), 0.0013337 secs]
[Full GC (System.gc()) 103072K->531K(175104K), 0.0070222 secs]
第一次GC后null
第一次GC后null
分配后null
分配后null
弱引用直接被回收掉了。那么弱引用的作用是什么?或者使用場景是什么呢?
static Map<Object,Object> container = new HashMap<>();
public static void putToContainer(Object key,Object value){
container.put(key,value);
}
public static void main(String[] args) {
//某個類中有這樣一段代碼
Object key = new Object();
Object value = new Object();
putToContainer(key,value);
//..........
/**
* 若干調用層次后程序員發現這個key指向的對象沒有用了,
* 為了節省內存打算把這個對象拋棄,然而下面這個方式真的能把對象回收掉嗎?
* 由於container對象中包含了這個對象的引用,所以這個對象不能按照程序員的意向進行回收.
* 並且由於在程序中的任何部分沒有再出現這個鍵,所以,這個鍵 / 值 對無法從映射中刪除。
* 很可能會造成內存泄漏。
*/
key = null;
}
在《Java核心技術卷1》這本書中對此做了說明
設計 WeakHashMap類是為了解決一個有趣的問題。如果有一個值,對應的鍵已經不再 使用了, 將會出現什么情況呢? 假定對某個鍵的最后一次引用已經消亡,不再有任何途徑引 用這個值的對象了。但是,由於在程序中的任何部分沒有再出現這個鍵,所以,這個鍵 / 值 對無法從映射中刪除。為什么垃圾回收器不能夠刪除它呢? 難道刪除無用的對象不是垃圾回 收器的工作嗎?
遺憾的是,事情沒有這樣簡單。垃圾回收器跟蹤活動的對象。只要映射對象是活動的, 其中的所有桶也是活動的, 它們不能被回收。因此,需要由程序負責從長期存活的映射表中 刪除那些無用的值。 或者使用 WeakHashMap完成這件事情。當對鍵的唯一引用來自散列條目時, 這一數據結構將與垃圾回收器協同工作一起刪除鍵 / 值對。
下面是這種機制的內部運行情況。WeakHashMap 使用弱引用(weak references) 保存鍵。 WeakReference 對象將引用保存到另外一個對象中,在這里,就是散列鍵。對於這種類型的 對象,垃圾回收器用一種特有的方式進行處理。通常,如果垃圾回收器發現某個特定的對象 已經沒有他人引用了,就將其回收。然而, 如果某個對象只能由 WeakReference 引用, 垃圾 回收器仍然回收它,但要將引用這個對象的弱引用放人隊列中。WeakHashMap將周期性地檢 查隊列, 以便找出新添加的弱引用。一個弱引用進人隊列意味着這個鍵不再被他人使用, 並 且已經被收集起來。於是, WeakHashMap將刪除對應的條目。
除了WeakHashMap使用了弱引用,ThreadLocal類中也是用了弱引用,可以自行了解下。
虛引用
一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲取一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知
3.finalize方法
當一個對象在堆內存中運行時,根據它被引用變量所引用的狀態,可以把它所處的狀態
可達狀態:當一個對象被創建后,若有一個以上的引用變量引用它,則這個對象在程序中處於可達狀態。
可恢復狀態:如果程序中某個對象不再有任何引用變量引用它,它就進入了可恢復狀態。此時,系統的垃圾回收機制准備回收該對象所占用的內存,在回收該對象之前,系統會調用所有可恢復狀態對象的finalize()方法進行資源清理。如果系統在調用finalize()方法時重新讓一個引用變量引用該對象,則這個對象會再次變成可達狀態;否則該對象將進入不可達狀態。
不可達狀態:當對象與所有引用變量的關聯都被切斷,且系統已經調用所有對象的finalize()方法后依然沒有使該對象變成可達狀態,那么這個對象將永久性地失去引用,最后變成不可達狀態。只有當一個對象處於不可達狀態時,系統才會真正回收該對象所占有的資源。
所以finalize方法只對象存活的最后一次機會,而且只會執行一次。
銷毀一個對象過程歸納如下:
4.方法區的回收
很多人認為方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規范中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收為例,假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果在這時候發生內存回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
- 加載該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看類的加載和卸載信息。
在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
參考《深入理解Java虛擬機》