歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Java內存模型與垃圾回收

1、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語言中,可作為GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

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

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

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

/**  
 * 此代碼演示了兩點:  
 * 1.對象可以在被GC時自我拯救。  
 * 2.這種自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統自動調用一次  
 * @author zzm  
 */  
public class FinalizeEscapeGC {  
 
  public static FinalizeEscapeGC SAVE_HOOK = null;  
 
  public void isAlive() {  
   System.out.println("yes, i am still alive :)");  
  }  
 
  @Override  
  protected void finalize() throws Throwable {  
   super.finalize();  
   System.out.println("finalize mehtod executed!");  
   FinalizeEscapeGC.SAVE_HOOK = this;  
  }  
 
  public static void main(String[] args) throws Throwable {  
   SAVE_HOOK = new FinalizeEscapeGC();  
 
   //對象第一次成功拯救自己  
   SAVE_HOOK = null;  
   System.gc();  
   //因為finalize方法優先級很低,所以暫停0.5秒以等待它  
   Thread.sleep(500);  
   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(500);  
   if (SAVE_HOOK != null) {  
    SAVE_HOOK.isAlive();  
   } else {  
    System.out.println("no, i am dead :(");  
   }  
  }  
} 

運行結果為:

finalize mehtod executed!  
yes, i am still alive :)  
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 {
    public String msg = "";
}

public class Hello {
    public 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

3、典型的垃圾回收算法

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

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

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

2.Copying(復制)算法

  為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:

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

很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麼Copying算法的效率將會大大降低。

3.Mark-Compact(標記-整理)算法

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

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算法。

參考資料

  1、《深入理解Java虛擬機》2章和3章內容

  2、Java方法區和運行時常量池溢出問題分析 http://www.linuxidc.com/Linux/2016-05/131387.htm

Copyright © Linux教程網 All Rights Reserved