JVM 之 記憶體分配與回收策略

李詩雨發表於2020-01-05

不詩意的女程式媛不是好廚師~ 轉載請註明出處,From李詩雨---[blog.csdn.net/cjm24848365…]

本篇思維導圖

1.GC(Garbage Collection垃圾回收)

1.1 誰需要GC?

:重點!

方法區/元空間:(只需要知道這裡 也有垃圾回收 即可)

棧: 不需要 。執行緒私有的,隨執行緒消亡而消亡,不需要過多考慮垃圾回收問題。

1.2 GC 觸發的條件:記憶體不夠了

新生代不夠了 → Minor GC

老年代不夠了 → Full GC

補充:堆的進一步劃分

堆的進一步劃分

新生代(PSYoungGen)

​ 又分為:

​ •Eden空間

​ •From Survivor空間

​ •To Survivor空間

▶老年代(ParOldGen)

2. GC如何判斷物件是否存活?

2.1 引用計數演算法

引用計數法 即 給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1。當計數器為0時,就認為該物件就是不可能再被使用的。

優點:

快、方便、實現簡單。

▶缺點:

物件互相引用時很難判斷物件是否該回收

2.2 可達性分析 (面試重點)▲▲▲

在這裡插入圖片描述

這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

作為GC Roots的物件包括下面幾種:

1.虛擬機器棧(棧幀中的本地變數表)中的物件。(方法中的引數,方法體中的區域性變數)

2.方法區中 類靜態屬性的物件。 (static)

3.方法區中 常量的物件。 (final static)

4.本地方法棧中 JNI(即一般說的Native方法)的物件。

來個栗子:

public class GCRoots {

Object o =new Object(); //o不是GCRoots,方法執行完以後,o可回收。
static Object GCRoot1 =new Object(); //GC Roots---方法區中 類靜態屬性的物件
final  static Object GCRoot2 =new Object();//GCRoots---方法區中 常量的物件

public static void main(String[] args) {
    //可達
    Object object1 = GCRoot1; //注意:“ = ” 不是賦值,在物件中是引用,傳遞的是右邊物件的地址
    Object object2 = object1;
    Object object3 = object1;
    Object object4 = object3;
}
public void method1(){
    //不可達(方法執行完後可回收)
    Object object5 = o;//o不是GCRoots
    Object object6 = object5;
    Object object7 = object5;
}
//本地變數表中引用的物件
public void stack(){
    Object ostack =new Object();    //本地變數表的物件
    Object object9 = ostack;
    //以上object9 在方法沒有(執行完)出棧前都是可達的
}


}
複製程式碼

我們來畫個圖理解一下上面的程式碼:

在這裡插入圖片描述

2.3 再談引用

無論是通過引用計數演算法判斷物件的引用數量,還是通過可達性分析演算法判斷物件的引用鏈是否可達,判斷物件是否存活都與引用有關,那麼就讓我們再次來談一談引用。

▶強引用

強引用就是指在程式程式碼中普遍存在的,類似於**“Object obj = new Object() ”**這類的就是強引用。

只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件

▶軟引用

軟引用是用來描述 一些有用但是並非必需 的物件。

用軟引用關聯的物件,系統將要發生OOM之前,這些物件就會被回收。

​ 聯想記憶順序:只有男人吃軟飯一說,沒有男人吃弱飯一說,如果很強的男人排第一,那吃軟飯的男人可以排第二,吃弱飯都不是男人了,所以排第三,最後是虛引用。

☛用一個栗子來說明軟引用的使用(PS: VM引數配置為 -Xms10m -Xmx10m -XX:+PrintGC):

public class TestSoftRef {
    public static class User{
        public int id = 0;
        public String name = "";
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }

    }

    public static void main(String[] args) {
        User u = new User(1,"鄭爽"); //new是強引用
        //軟引用的使用示例:
        SoftReference<User> userSoft = new SoftReference<User>(u);
        u = null;//幹掉強引用,確保這個例項只有userSoft的軟引用
        //--- 如果是 SoftReference<User> userSoft = new SoftReference<User>(new User()); 就沒法幹掉強引用
        System.out.println(userSoft.get());
        System.gc();//進行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userSoft.get());
        //往堆中填充資料,導致OOM
        List<byte[]> list = new LinkedList<>();
        try {
            for(int i=0;i<100;i++) {
                System.out.println("*************"+userSoft.get());
                list.add(new byte[1024*1024*1]); //1M的物件
            }
        } catch (Throwable e) {
            //丟擲了OOM異常時列印軟引用物件
            System.out.println("Exception*************"+userSoft.get());
        }

    }
}
複製程式碼

看下列印結果:

在這裡插入圖片描述

軟引用的使用場景:

例如,一個程式用來處理使用者提供的圖片。

如果將所有圖片讀入記憶體,這樣雖然可以很快的開啟圖片,但記憶體空間使用巨大,一些使用較少的圖片浪費記憶體空間,需要手動從記憶體中移除。

如果每次開啟圖片都從磁碟檔案中讀取到記憶體再顯示出來,雖然記憶體佔用較少,但一些經常使用的圖片每次開啟都要訪問磁碟,代價巨大。

這個時候就可以用軟引用構建快取。

▶弱引用

一些有用(程度比軟引用更低)但是並非必需,用弱引用關聯的物件,只能生存到下一次垃圾回收之前,GC發生時,不管記憶體夠不夠,都會被回收。

☛ Talk is cheap,show me the code!

public class TestWeakRef {
    public static class User{
        public int id = 0;
        public String name = "";
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }

    }

    public static void main(String[] args) {
        User u = new User(1,"小爽");
        WeakReference<User> userWeak = new WeakReference<User>(u);
        u = null;//幹掉強引用,確保這個例項只有userWeak的弱引用
        System.out.println(userWeak.get());
        System.gc();//進行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userWeak.get());
    }
}
複製程式碼

列印結果分析:

在這裡插入圖片描述

注意: 軟引用 SoftReference和弱引用 WeakReference,可以用在記憶體資源緊張的情況下以及建立不是很重要的資料快取。當系統記憶體不足的時候,快取中的內容是可以被釋放的。

實際運用(WeakHashMap、ThreadLocal

▶虛引用

幽靈引用,最弱,被垃圾回收的時候收到一個通知

3. 垃圾收集演算法

3.1複製演算法(Copying)

在這裡插入圖片描述

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半。


▶特點

▪ 實現簡單、執行高效

▪ 記憶體複製、沒有記憶體碎片

▪ 利用率只有一半


▶注意事項:

▪ 新生代 使用該演算法

▪ 新生代中3個區的比例8:1:1

▪ 空間擔保


對8:1:1比例的說明:

新生代中的物件98%是“朝生夕死”的,所以並不需要按1:1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor,當回收時將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清除掉Eden和剛才用過的Survivor空間。 HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的記憶體會被浪費。


對空間擔保的說明:

當然,98%的物件可回收,只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。 記憶體的分配擔保就好比我們去銀行貸款,如果我們信譽良好,在98%的情況下都能按時償還,於是銀行可能會預設我們下一次也能按時按量的償還貸款。只需要有一個擔保人能保證如果我們不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了,記憶體的分配擔保也一樣,如果另一塊survival空間沒有足夠空間存放,上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代。

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

在這裡插入圖片描述

演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

它的主要不足空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。


▶特點

▪ 利用率百分之百

▪ 不需要記憶體複製

▪ 有記憶體碎片

3.3標記-整理演算法(Mark-Compact)

在這裡插入圖片描述
首先標記出所有需要回收的物件,在標記完成後,後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。


▶特點

▪ 利用率百分之百

▪ 沒有記憶體碎片

▪ 需要記憶體複製

▪ 效率一般般

4.堆記憶體分配策略

▶物件優先在Eden分配

如果Eden記憶體空間不足,就會發生Minor GC

▶大物件直接進入老年代

大物件:需要大量連續記憶體空間的Java物件,比如很長的字串和大型陣列,

大物件對虛擬機器的記憶體分配來說是一個壞訊息(∵1、大物件容易導致記憶體還有不少空間時,就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們)。

比遇到一個大物件更加壞的訊息就是遇到一群“朝生夕滅”的“短命大物件”(∵ 2、會進行大量的記憶體複製)。

虛擬機器提供了一個 -XX:PretenureSizeThreshold 引數 ,

大於這個數量直接在老年代分配,預設為0 ,表示絕不會直接分配在老年代。

▶長期存活的物件將進入老年代

預設15歲,-XX:MaxTenuringThreshold 引數可調整

▶動態物件年齡判定

為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

▶空間分配擔保

新生代中有大量的物件存活,survivor空間不夠,當出現大量物件在MinorGC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代.只要老年代的連續空間大於新生代物件的總大小或者歷次晉升的平均大小,就進行Minor GC,否則FullGC。

所以,新生代一般不會記憶體溢位,因為有老年代做擔保。

相關文章