這裡會記錄 《深入了解java虛拟機》的所有知識點喲~
概述
java記憶體運作時區域的各個部分,其中程式計數器、虛拟機棧和本地方法棧3個區域随線程而生,随線程而滅;棧中的棧幀随着方法的進入和退出而有條不紊地執行者出棧和入棧操作。每一個棧幀中配置設定多少記憶體基本上是在類結構确定下來時就已知的,是以這幾個區域的記憶體配置設定和回收都具有确定性,在這幾個記憶體内就不需要過多考慮回收的問題,因為方法結束或者線程結束時,記憶體自然就跟着回收了。
而java堆和方法區則不一樣,一個接口中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們隻有在程式處于運作期間時才能知道會建立哪些對象,這部分記憶體的配置設定和回收是動态的,垃圾收集器所關注的是這部分記憶體。
對象已死嗎
在堆裡面存放着幾乎所有的對象執行個體,垃圾收集器在對堆進行回收前,第一件事情就是要确定這些對象之中哪些還“存活”着,哪些已經“死去”
1. 引用計數算法
判斷一個對象是否存活的算法通常是這樣的
- 給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;
- 當引用失效時,計數器值就減1;
- 任何時刻計數器為0的對象就是不可能再被使用的。
很多主流的java虛拟機裡面沒有選用引用技術算法來管理記憶體,主要的原因就是: 它很難解決對象之間互相循環引用的問題
2. 可達性分析算法
該算法的基本思路:
通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象的GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的
在java語言中,可作為GC Roots的對象包括以下幾種:
- 虛拟機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜态屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(一般說的native方法)引用的對象
再談引用
在JDK 1.2以後,java對引用的概念進行了擴充,将引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
1. 強引用(Strong Reference)
如果一個對象具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,JVM甯願抛出OutOfMemoryError錯誤,使程式異常終止,也不會随意回收具有強引用的對象來解決記憶體不足的問題。
強引用的例子:
Object obj = new Object();
2. 軟引用(Soft Reference)
如果一個對象隻具有軟引用,則記憶體空間足夠,垃圾收集器就不會回收它;如果記憶體空間不足了,就會回收這些對象的記憶體。
弱引用的例子:
MyObject aReference = new MyObject();
SoftReference aSoftReference = new SoftReference(aReference);
此時,對于這個MyObject對象,有兩個引用路徑,一個是來自SoftReference對象的軟引用,一個來自變量aReference的強引用,是以這個MyObject對象是強可及對象。
之後,我們結束aReference對這個MyObject的強引用
執行完上述操作後,這個對象成為了軟可及對象。如果垃圾收集線程進行記憶體垃圾收集,并不會因為有一個SoftReference對該對象的引用而始終保留該對象。JVM的垃圾收集線程對軟可及對象和其他一般java對象進行了差別對待:軟可及對象的清理時由垃圾收集線程根據其特定算法按照記憶體需求決定的。即,垃圾收集線程會在虛拟機抛出OutOfMemoryError之前回收軟可及對象,而且虛拟機會盡可能優先回收長時間閑置不用的軟可及對象,對那些剛剛建構的或者剛剛使用過的“新”軟可及對象會被JVM盡可能保留。
使用ReferenceQueue清除失去了軟引用對象的SoftReference
作為一個java對象,SoftReference對象除了具有儲存軟引用的特殊性之外,也具有java對象的一般性。是以,當軟可及對象被回收後,雖然這個SoftReference對象的
get()
方法傳回null,但這個SoftReference對象已經不再具有存在的價值,需要一個适當的清除機制,避免大量的SoftReference對象帶來的記憶體洩漏。在
java.lang.ref
包裡還提供了ReferenceQueue。如果在建立SoftReference對象的時候,使用了一個ReferenceQueue對象作為參數提供給SoftReference的構造方法
ReferenceQueue queue = new ReferenceQueue();
SoftReference ref = new SoftReference(aMyObject, queue);
那麼當這個SoftReference所軟引用的aMyOhject被垃圾收集器回收的同時,ref所強引用的SoftReference對象被列入ReferenceQueue。也就是說,ReferenceQueue中儲存的對象是Reference對象,而且是已經失去了它所軟引用的對象的Reference對象。另外從ReferenceQueue這個名字也可以看出,它是一個隊列,當我們調用它的poll()方法的時候,如果這個隊列中不是空隊列,那麼将傳回隊列前面的那個Reference對象。
在任何時候,我們都可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的非強可及對象被回收。如果隊列為空,将傳回一個null,否則該方法傳回隊列中前面的一個Reference對象。利用這個方法,我們可以檢查哪個SoftReference所軟引用的對象已經被回收。于是我們可以把這些失去所軟引用的對象的SoftReference對象清除掉。常用的方式為:
SoftReference ref = null;
while ((ref = (EmployeeRef) q.poll()) != null) {
// 清除ref
}
弱引用(Weak Reference)
被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。
什麼時候使用弱引用?
考慮下面的場景:現在有一個Product類代表一種産品,這個類被設計為不可擴充的,而此時我們想要為每一個産品增加一個編号。一種解決方案是使用
HashMap<Product, Integer>
。那麼問題來了,如果我們已經不再需要一個Product對象存在于記憶體中(比如:已經賣出了這件産品),假設指向它的引用為productA,我們這時會給productA指派為null,然而這時productA過去指向的Producr對象并不會被回收,因為它還被HashMap引用着。是以這種情況下,我們想要真正的回收一個Product對象,僅僅把它的強引用指派為null是不夠的,還要把相應的條目從HashMap中移除。顯然“從HashMap中移除不再需要的條目”這個工作我們不想自己完成,我們希望垃圾收集器自己完成。
如何使用弱引用?
以上面的場景為例子:
Product productA = new Product();
WeakReference<Product> weakProductA = new WeakReference<>(productA);
現在,若弱引用對象weakProductA就指向了Product對象的productA,那麼我們怎麼通過weakProductA 來擷取它所指向的Product對象productA呢?隻需要下面的代碼
實際上,對于這種情況,Java類庫為我們提供了WeakHashMap類,使用和這個類,它的鍵自然就是弱引用對象,無需我們再手動包裝原始對象。這樣一來,當productA變為null時(表明它所引用的Product已經無需存在于記憶體中),這時指向這個Product對象的就是由弱引用對象weakProductA了,那麼顯然這時候相應的Product對象時弱可達的,是以指向它的弱引用會被清除,這個Product對象随即會被回收,指向它的弱引用對象會進入引用隊列中。
與軟引用一樣,弱引用也存在引用隊列。
WeakReference類有兩個構造函數:
//建立一個指向給定對象的弱引用
WeakReference(T referent)
//建立一個指向給定對象并且等級到給定引用隊列的弱引用
WeakReference(T referent, ReferenceQueue<? super T> q)
我們可以看到第二個構造方法中提供了一個ReferenceQueue類型的參數,通過提供這個參數,我們便把建立的弱引用對象注冊到了一個引用隊列上,這樣當它被垃圾回收器清楚時,就會把它送入這個引用隊列中,我們便可以對這些被清楚的弱引用對象進行統一管理。
4. 虛引用
虛引用也成為幽靈引用或者幻影引用,它是最弱的一種引用關系。
垃圾收集過程中,對象的可觸及狀态改變的時候,可以把引用對象和引用隊列關聯起來。即垃圾收集器會把要回收的對象添加到引用隊列ReferenceQueue
虛引用的作用:
我們可以生命虛引用來引用我們感興趣的對象,在gc要回收的時候,gc收集器會把這個對象添加到ReferenceQueue,這樣我們如果檢查到ReferenceQueue中有我們感興趣的對象的時候,說明gc将要回收這個對象了。此時我們可以在gc回收之前做一些其他事情,比如記錄日志
在java中,
finalize()
函數本來是設計用來在對象被回收的時候來做一些操作的。但是對象被GC說明時候回收的時間是不固定的,這樣
finalize()
函數很尴尬。虛引用可以用來解決這個問題。
在建立虛引用的時候必須傳入一個引用隊列。在一個對象finalize函數被調用之後,這個對象的幽靈引用會被假如到引用隊列中。通過檢查隊列的内容就知道對象是不是要準備被回收了。
幽靈引用的使用并不常見,主要是實作細粒度的記憶體控制
示例:實作一個緩存。程式在确認原來的對象要被回收之後,才申請記憶體建立新的緩存。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomBuffer {
private byte[] data= new byte[];
private ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
private PhantomReference<byte[]> ref = new PhantomReference<byte[]>(data,queue);
public byte[] get(int size) {
if(size <= ) {
throw new IllegalArgumentException("Wrong buffer size");
}
if(data.length < size) {
data = null;
//強行運作垃圾回收器,調用該方法隻是建議JVM進行回收,并不一定馬上會進行GC
System.gc();
try {
//該方法會阻塞直到隊列非空
queue.remove();
//幽靈引用不會自動清空,要手動運作
ref.clear();
ref = null;
data = new byte[size];
ref = new PhantomReference<byte[]>(data, queue);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return data;
}
}
上述代碼中,每次申請新的緩存的時候,都要確定之前的位元組數組被成功回收。引用隊列的remove方法會阻塞知道虛引用被假如到引用隊列中。
對象的生存與死亡
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經曆兩次标記過程:
- 如果對象在進行可達性分析後發現沒有與GC Roots相連接配接的引用鍊,那它将會被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行
方法。當對象沒有覆寫finalize()
方法,或者finalize()
方法已經被虛拟機調用過,虛拟機将這兩種情況都視為“沒有必要執行”。finalize()
- 如果這個對象被判定為有必要執行
方法,那麼這個對象将會放置在一個叫finalize()
的隊列中,并在稍後由一個由虛拟機自動建立的、低優先級的Finalizer線程去執行它。這裡所謂的“執行”是指虛拟機會觸發這個方法,但并不承諾會等待它運作結束,這樣做的原因是,如果一個對象在F-Queue
方法中執行緩慢,或者發生了死循環,将很可能會導緻finalize()
隊列中企鵝他對象永久處于等待,甚至導緻整個記憶體回收系統崩潰。F-Queue
方法是對象逃脫死亡命運的最後一次機會,稍後GC将對finalize()
中的對象進行第二次小規模的标記,如果對象要在F-Queue
中成功成就自己——隻要重新與引用鍊上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)指派給某個類變量或者對象的成員變量,那再第二次标記時它将被移除出“即将回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。finalize()
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive:");
}
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
//對象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因為finalize方法優先級很低,是以暫停0.5秒以等待它
Thread.sleep();
if(SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no, i am dead");
}
//下面這段代碼與上面的完全相同,但是這次自救卻失敗了
SAVE_HOOK = null;
System.gc();
//因為finalize方法優先級很低,是以暫停0.5秒以等待它
Thread.sleep();
if(SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no, i am dead");
}
}
}
運作結果:
finalize method executed!
yes, i am still alive:
no, i am dead
代碼中有兩段完全一樣的代碼片段,執行結果卻是一次逃脫成功,一次逃脫失敗,這是因為任何一個對象的
finalize()
方法都隻會被系統自動調用一次,如果對象面臨下一次回收,它的
finalize()
方法不會被再次執行。
回收方法區
永久代的垃圾收集主要回收兩部分内容:廢棄常量和無用的類。
廢棄常量:回收廢棄常量與回收java堆中的對象非常類似。以常量池中字面量的回收為例,假如一個字元串“abc”已經進入了常量池,但是目前系統沒有任何一個String對象是叫做“abc”的,如果這時發生記憶體回收,而且必要的話,這個“abc”常量就會被系統清理出常量池。
廢棄類:判斷一個類是否是廢棄類,需要下面3個條件才能算是“無用的類”
- 該類所有的執行個體都可以被回收,也就是java堆中不存在該類的任何執行個體
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang,Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法
參考文章
https://blog.csdn.net/u011936381/article/details/11709245
http://www.importnew.com/21206.html
https://blog.csdn.net/imzoer/article/details/8044900