首頁  >  文章  >  Java  >  淺析Java記憶體模型與垃圾回收

淺析Java記憶體模型與垃圾回收

高洛峰
高洛峰原創
2017-01-17 15:41:561260瀏覽

1、Java記憶體模型

Java虛擬機在執行程式時把它管理的記憶體分為若干資料區域,這些資料區域分佈情況如下圖所示:

淺析Java記憶體模型與垃圾回收

程式計數器:一塊較小記憶體區域,指向目前所執行的字節碼。如果執行緒正在執行一個Java方法,這個計數器記錄正在執行的虛擬機器字節碼指令的位址,如果執行的是Native方法,這個計算器值為空。

Java虛擬機棧:執行緒私有的,其生命週期和執行緒一致,每個方法執行時都會建立一個堆疊幀用於儲存局部變數表、操作數棧、動態連結、方法出口等資訊。

本地方法堆疊:與虛擬機器堆疊功能類似,只不過虛擬機器堆疊為虛擬機器執行Java方法服務,而本地方法堆疊則為使用到的Native方法服務。

Java堆:是虛擬機器管理記憶體中最大的一塊,由所有執行緒共享,該區域用於存放物件實例,幾乎所有的物件都在該區域分配。 Java堆是記憶體回收的主要區域,從記憶體回收角度來看,由於現在的收集器大都採用分代收集演算法,所以Java堆還可以細分為:新生代和老年代,再細分一點的話可以分為Eden空間、From Survivor空間、To Survivor空間等。根據Java虛擬機器規格規定,Java堆可以處於物理上不連續的空間,只要邏輯上是連續的就行。

方法區:與Java一樣,是各個執行緒所共享的,用於儲存已被虛擬機器載入類別資訊、常亮、靜態變數、即時編譯器編譯後的程式碼等資料。

運行時常數池,運行時常數池是方法區的一部分,Class檔案中除了有類別的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常數池,用於存放編譯期生成的各種字面量和符號引用。運行期間可以將新的常數放入常數池中,用得比較多的就是String類別的intern()方法,當一個String實例呼叫intern時,Java查找常數池中是否有相同的Unicode的字串常數,若有,則傳回其引用;若沒有,則在常數池中增加一個Unicode等於該實例字串並傳回它的參考。

2、垃圾物件如何確定

Java堆中存放著幾所所有的物件實例,垃圾收集器在回收堆之前,首先需要確定哪些物件還"活著",哪些已經"死亡",也就是不會被任何途徑使用的物件。

引用計數法

引用計數法實現簡單,效率較高,在大部分情況下是一個不錯的演算法。其原理是:給物件添加一個引用計數器,每當有一個地方引用該物件時,計數器加1,當引用失效時,計數器減1,當計數器值為0時表示該物件不再被使用。要注意的是:引用計數法很難解決物件之間相互循環引用的問題,主流Java虛擬機器沒有選用引用計數法來管理記憶體。

可達性分析演算法

這個演算法的基本想法就是透過一系列的稱為「GC Roots」的物件作為起始點,從這些節點開始向下搜索,搜尋所走過的路徑稱為引用鏈( Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。如圖所示,物件object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的物件。

淺析Java記憶體模型與垃圾回收

在Java語言中,可作為GC Roots的物件包括以下幾種:

虛擬機堆疊(堆疊幀中的本地變數表)中引用的物件。

方法區中類別靜態屬性所引用的物件。

方法區中常數引用的物件。

本地方法堆疊中JNI(即一般說的Native方法)所引用的物件。

現在問題來了,可達性分析演算法會不會出現物件間循環引用問題呢?答案是肯定的,那就是不會出現物件間循環引用問題。 GC Root在物件圖之外,是特別定義的“起點”,不可能被物件圖內的物件所引用。

對像生存還是死亡(To Die Or Not To Die)

即使在可達性分析演算法中不可達的對象,也並非是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots連結的參考鏈,那麼它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finapze()方法。當物件沒有覆寫finapze()方法,或finapze()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為「沒有必要執行」。程式中可以透過覆蓋finapze()來一場"驚心動魄"的自我拯救過程,但是,這只有一次機會呦。

/** 
 * 此代码演示了两点: 
 * 1.对象可以在被GC时自我拯救。 
 * 2.这种自救的机会只有一次,因为一个对象的finapze()方法最多只会被系统自动调用一次 
 * @author zzm 
 */
pubpc class FinapzeEscapeGC { 
  
 pubpc static FinapzeEscapeGC SAVE_HOOK = null; 
  
 pubpc void isApve() { 
  System.out.println("yes, i am still apve :)"); 
 } 
  
 @Override
 protected void finapze() throws Throwable { 
  super.finapze(); 
  System.out.println("finapze mehtod executed!"); 
  FinapzeEscapeGC.SAVE_HOOK = this; 
 } 
  
 pubpc static void main(String[] args) throws Throwable { 
  SAVE_HOOK = new FinapzeEscapeGC(); 
  
  //对象第一次成功拯救自己 
  SAVE_HOOK = null; 
  System.gc(); 
  //因为finapze方法优先级很低,所以暂停0.5秒以等待它 
  Thread.sleep(500); 
  if (SAVE_HOOK != null) { 
SAVE_HOOK.isApve(); 
  } else { 
System.out.println("no, i am dead :("); 
  } 
  
  //下面这段代码与上面的完全相同,但是这次自救却失败了 
  SAVE_HOOK = null; 
  System.gc(); 
  //因为finapze方法优先级很低,所以暂停0.5秒以等待它 
  Thread.sleep(500); 
  if (SAVE_HOOK != null) { 
SAVE_HOOK.isApve(); 
  } else { 
System.out.println("no, i am dead :("); 
  } 
 } 
}

運行結果為:

finapze mehtod executed! 
yes, i am still apve :) 
no, i am dead :(

接著說引用

無論是透過引用計數演算法判斷物件的引用數量,或是透過可達性分析演算法判斷物件的引用鍊是否可達,判定物件是否存活都與「引用”有關。在JDK 1.2以前,Java中的引用的定義很傳統:如果reference類型的資料中儲存的數值代表的是另外一塊記憶體的起始位址,就稱這塊記憶體代表一個引用。在JDK 1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依序逐漸減弱。

• 強引用就是指在程式碼之中普遍存在的,類似「Object obj = new Object()」這類的引用,只要強引用還存在,垃圾收集器永遠不會回收被引用的物件。

• 軟引用是用來描述一些還有用但並非必需的物件。對於軟引用關聯的對象,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類別來實作軟引用。

• 弱引用也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收只被弱引用關聯的物件。在JDK 1.2之後,提供了WeakReference類別來實作弱引用。

• 虛引用也稱為幽靈引用或幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法透過虛引用來取得一個物件實例。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類別來實作虛引用。

軟引用使用範例:

package jvm;
 
import java.lang.ref.SoftReference;
 
class Node {
pubpc String msg = "";
}
 
pubpc class Hello {
pubpc static void main(String[] args) {
Node node1 = new Node(); // 强引用
node1.msg = "node1";
SoftReference<Node> node2 = new SoftReference<Node>(node1); // 软引用
node2.get().msg = "node2";
 
System.out.println(node1.msg);
System.out.println(node2.get().msg);
}
}

輸出結果為:

軟引用使用範例:

node2
node2

輸出結果為:

rrreee

3、典型的垃圾回收演算法

1.Mark-Sweep(標記-清除)演算法

淺析Java記憶體模型與垃圾回收之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除演算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。具體過程如下圖所示:

從圖中可以很容易看出標記-清除演算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多​​可能會導致後續過程中需要為大物件分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

2. Copying(複製)演算法

淺析Java記憶體模型與垃圾回收為了解決Mark-Sweep演算法的缺陷,Copying演算法就被提了出來。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體碎片的問題。具體過程如下圖所示:

這種演算法雖然實現簡單,運行高效且不容易產生記憶體碎片,但是卻對記憶體空間的使用做出了高昂的代價,因為能夠使用的記憶體縮減到原來的一半。

很顯然,Copying演算法的效率跟存活對象的數目多少有很大的關係,如果存活對像很多,那麼Copying演算法的效率將會大大降低。 🎜🎜3. Mark-Compact(標記-整理)演算法🎜

為了解決Copying演算法的缺陷,充分利用記憶體空間,提出了Mark-Compact演算法。這個演算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對像都向一端移動,然後清理掉端邊界以外的記憶體。具體過程如下圖所示:

淺析Java記憶體模型與垃圾回收

4.Generational Collection(分代收集)演算法

分代收集演算法是目前大部分JVM的垃圾收集器所採用的演算法。它的核心思想是根據物件存活的生命週期將記憶體劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特徵是每次垃圾收集時只有少量物件需要被回收,而新生代的特徵是每次垃圾回收時都有大量的物件需要被回收,那麼就可以根據不同世代的特徵採取最適合的收集演算法。

目前大部分垃圾收集器對於新生代都採取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中並不是按照1: 1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間(一般為8:1:1),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的物件複製到另一塊Survivor空間中,然後清理掉Eden和剛才使用過的Survivor空間。

而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact演算法。

以上這篇淺析Java記憶體模型與垃圾回收就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援PHP中文網。

更多淺析Java記憶體模型與垃圾回收相關文章請關注PHP中文網!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn