天天看點

JVM記憶體分區與垃圾回收算法

1.概述

對于從事c/c++程式開發的開發人員來說,他們擁有記憶體管理的直接權利,所有記憶體空間的管理都交給程式員手動實作。

對于java程式員來說不再需要直接參與記憶體的管理,這些工作都由jvm幫我們實作。這樣不容易出現“記憶體洩漏”等問題。這一切看起來很美好,如果不了解虛拟機怎麼使用記憶體的,一旦遇到記憶體洩漏和溢出方面的問題将無從下手。

2.JVM運作時記憶體分區

java虛拟機在執行java程式的過程中會把它所管理的記憶體劃分為若幹不同的區域,這些區域有不同的用途與特性。根據java虛拟機規範,java所管理的記憶體包括以下幾個運作時資料區。

JVM記憶體分區與垃圾回收算法

1.程式計數器

  • 程式計數器是一塊較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器。
  • 程式計數器處于線程獨占區。
  • 如果線程執行的是java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址。如果正在執行的是native方法,這個計數器的值為undefined。
  • 此區域是唯一沒有規定OutOfMemoryError情況的區域。

2.虛拟機棧

  • java虛拟機棧是線程私有的,它的生命周期與線程相同。
  • 虛拟機棧描述的是java方法執行的記憶體模型:每個方法在執行的時候會建立一個棧幀用于存儲局部變量表,操作數,動态連結,方法出口等資訊。每一個方法從調用到執行完畢就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。
  • 如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError錯誤。
  • 如果虛拟機棧不能申請到足夠的記憶體,就會抛出OutOfMemoryError錯誤。

3.本地方法棧

  • 本地方法棧為虛拟機執行native方法服務。
  • 本地方法棧的工作機制基本與java虛拟機棧一緻。

4.堆

  • 堆是java虛拟機管理的最大一塊記憶體區域,它被所有的線程共享。
  • 此區域的作用用來存儲對象執行個體,同時也是垃圾收集的工作區域。
  • 如果在堆中沒有完成記憶體配置設定,并且堆無法擴充的時候将會抛出OutOfMemoryError錯誤。

5.方法區

  • 與堆一樣也被各個線程共享。
  • 用于存儲已被虛拟機加載的類資訊,常量,靜态變量,即時編譯器編譯後的代碼等資料。
  • 當方法區無法滿足記憶體配置設定需求時,将抛出OutOfMemoryError錯誤。

6.運作時常量池

  • 運作時常量池是方法區的一部分,Class檔案除了有類的版本,字段,方法,接口等描述資訊外,還有一項資訊是常量池,用于存放編譯期生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池中存放。
  • 既然運作時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請記憶體時會抛出OutOfMemoryError錯誤。

7.直接記憶體

  • 直接記憶體并不是虛拟機運作時資料區的一部分,也不是java虛拟機規範中定義的記憶體區域。
  • 直接記憶體實質是jvm通過native方法向作業系統申請的記憶體區域,其大小會受到本機實體記憶體大小以及作業系統的限制。
  • 同樣存在OutOfMemoryError錯誤。

java 中對象的回收是由jvm自動完成的,其工作區域是堆。一個通用的垃圾回收器應該需要考慮以下兩個問題:

  1. 對象存活的判定,即什麼樣的對象需要被回收。
  2. 回收算法,即怎麼回收。
2.對象存活判定算法

a.引用計數法

  • 引用計數算法基本原理:

    給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數值減1;任何時刻計數器為0的對象是不可能再被使用的。

    JVM記憶體分區與垃圾回收算法
  • 引用計數計數算法的缺陷
class A {
    public Object b;
}

class B{
    public Object a;
}


public class Reference {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();

        a.b = b;
        b.a = a;
        
        a = null;
        b = null;
    }
}
           

雖然a和b都被指派為null,但是由于a和b存在循環引用,對象a和對象b的引用計數為1,這樣a和b永遠都不會被回收。

JVM記憶體分區與垃圾回收算法

b.可達性分析

  • 可達性分析算法基本原理:

    通過一系列的稱為"GC Roots"的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑成為引用鍊,當一個對象到GC Roots沒有任何引用鍊相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象不可用。

    JVM記憶體分區與垃圾回收算法
  • java語言中的GC Roots:

    1.在虛拟機棧中的引用對象。

    2.在方法區中的類靜态屬性引用的對象。

    3.在方法區中的常量引用的對象。

    4.在本地方法棧中JNI(native方法)的引用的對象。

3.常見垃圾回收算法

a.标記清除(Mark-Sweep)

标記-清除算法分為兩個階段:标記階段和清除階段。标記階段的任務是标記出所有需要被回收的對象,清除階段就是回收被标記的對象所占用的空間,過程如下圖所示:

JVM記憶體分區與垃圾回收算法

優缺點:

  • 标記和清除的效率不夠高。
  • 由于垃圾對象的不連續性容易産生記憶體碎片。

b.标記整理(Mark-Compact)

标記整理與标記清除算法步驟一樣,隻是後續不是直接清除垃圾對象,而是将所有存活的對象向一端移動,然後直接清理邊界以外的記憶體。過程如下圖所示:

JVM記憶體分區與垃圾回收算法

優缺點:

  • 解決了記憶體碎片的問題。
  • 由于需要将存活對象進行整理,是以效率較低。

c.複制算法

它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體碎片的問題。過程如下圖所示:

JVM記憶體分區與垃圾回收算法

優缺點:

  • 回收垃圾時簡單高效。
  • 對記憶體空間要求高,實際有效記憶體隻使用了一半。

d.分代算法

分代算法将堆空間劃分為幾塊,一般分為新生代和老年代,這樣可以根據各個年代的特點采用最适當的收集算法。新生代中隻有少量對象存活是以适合複制算法,老年代中對象存活率高,适合使用标記清除或标記整理算法。

JVM記憶體分區與垃圾回收算法

優缺點:

  • 通過對堆記憶體進行分代劃分,每個分代區域使用與之特性比對的回收算法,很好的解決了單一算法很難滿足所有場景的問題。
  • 對大記憶體場景支援不夠好。

e.分區算法

一般來說,堆空間越大,一次GC所需要的時間就越長。分區算法将整個堆空間劃分為連續的不同小區域,每個小區獨立使用,獨立回收。

JVM記憶體分區與垃圾回收算法

優缺點:

  • 适用于大記憶體,多CPU的實體環境。
4.JVM常見引用類型

a.強引用

特點:

1.強引用可以直接通路目标對象。

2.強引用所指向的對象在任何時候都不會被系統回收,虛拟機甯願抛出OOM異常,也不會強行回收引用所指向的對象。

public class StrongRef {
    public static void main(String[] args) {
        String str1 = new String("hello world");
        String str2 = str1;
        str1 = null;
        
        System.out.println("GC前: " + str2);
        System.gc();
        System.out.println("GC後: " + str2);
    }
}
           

運作結果:

GC前: hello world
GC後: hello world
           

b.軟應用

特點:

當記憶體資源不足時,軟引用對象會被回收,是以軟引用不會導緻OOM。

import java.lang.ref.SoftReference;

class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class SoftRef {
    public static void main(String[] args) {
        
        StringBuilder builder = new StringBuilder();
        for (int i = 0 ;i < 1024 * 10; ++i) {
            builder.append("hello");
        }
        User user = new User(builder.toString(), 1);
        SoftReference<User> stringSoftReference = new SoftReference<>(user);
        user = null;

        System.out.println(stringSoftReference.get());
        System.gc();

        System.out.println("First GC:");
        System.out.println(stringSoftReference.get());

        byte[] b = new byte[1024  * 400];
        System.gc();
        System.out.println("Second GC:");
        System.out.println(stringSoftReference.get());
    }
}
           

設定運作參數-Xmx1M

運作結果:

reference.[email protected]
First GC:
[email protected]
Second GC:
null
           

c.弱引用

特點:隻要發生GC,無論記憶體資源怎樣,弱引用對象都會被回收。

public class WeakRef {
    public static void main(String[] args) {
        String str = new String("hello world");
        java.lang.ref.WeakReference<String> ref = new java.lang.ref.WeakReference<String>(str);
        str = null;
        System.out.println("GC前: " + ref.get());
        System.gc();
        System.out.println("GC後: " + ref.get());
    }
}
           

運作結果:

GC前: hello world
GC後: null
           

d.幽靈引用

特點:一個持有幽靈引用的對象和沒有引用幾乎一樣,随時都可能被垃圾回收器回收。

繼續閱讀