輕鬆理解JVM的分代模型

王子發表於2020-10-10

 

前言

上篇文章我們一起對jvm的記憶體模型有了比較清晰的認識,小夥伴們可以參考JVM記憶體模型不再是祕密這篇文章做一個複習。

本篇文章我們將針對jvm堆記憶體的分代模型做一個詳細的解析,和大家一起輕鬆理解jvm的分代模型。

相信看過其他文章的小夥伴們可能都知道,jvm的分代模型包括:年輕代、老年代、永久代。

那麼它們分別代表著什麼角色呢?我們先來看一段程式碼

public class Main {
    public static void main(String[] args) {
        while (true){
            load();
        }
    }

    public static void load(){
        SysUser sysUser = new SysUser();
        sysUser.setAvatar("1");
    }

}

這段程式碼本身沒有什麼特殊的含義,主要是理解jvm的執行機制。

首先一旦執行main()方法,就會把main()方法的棧幀壓入main執行緒的虛擬機器棧,然後呼叫load()方法後,又會把load()方法的棧幀壓入虛擬機器棧。

接著在執行load()方法時,會在java堆記憶體中建立一個SysUser物件例項,而棧幀中會有sysUser區域性變數引用堆記憶體中的SysUser物件例項。

如下圖:

 

 到這裡上篇文章都講解過,相信大家都能看懂。

 

變數的存活時間

現在我們思考一下會發現,這個SysUser物件實際上屬於一個短暫存活的物件,因為在load()方法執行完畢後,load()方法的棧幀就會出棧。

而一旦出棧,就沒有了sysUser這個區域性變數來引用SysUser這個物件的例項。

所以,其實這個SysUser物件已經沒有用了,但是它還在佔用著堆記憶體的空間,那麼對於這種沒有引用的物件例項jvm是如何處理的呢?

這就要說到jvm的垃圾回收機制了,jvm本身是有垃圾回收機制的,它是一個後臺執行緒,會把沒有人引用的SysUser物件例項給回收掉,不斷的釋放記憶體空間。

所以這個SysUser物件例項是一個存活時間很短的物件,可能在執行load()方法的時候被建立出來,執行之後就被垃圾回收掉了。

而這種物件在我們平時的開發中是很常見的,佔絕大多數比例。

 

現在我們將上邊的程式碼改造一下:

public class Main {
    private static SysUser sysUser = new SysUser();
    public static void main(String[] args) {
        while (true){
            load();
        }
    }

    public static void load(){
        sysUser.setAvatar("1");
    }
}

其實就是把區域性變數sysUser變成了靜態變數,這樣修改後,sysUser不在作為區域性變數儲存在棧中,而是和class類檔案一起儲存在方法區中,這樣SysUser物件例項就會一直被這個靜態變數引用,所以不會被垃圾回收,一直儲存在堆記憶體中。如下圖:

 

 

分代模型

接下來我們進入核心內容,就是jvm的分代模型了。

上文中我們發現,根據我們的編碼方式的不同,採用不同的方式建立和使用物件,物件的存活時間是不同的。

所以jvm將記憶體區分為兩個區域:年輕代和老年代

年輕代就是我們的第一種區域性變數的示例,建立和使用完畢後會被垃圾回收掉。

老年代就是第二種靜態變數的示例,建立後需要長期在堆記憶體中存活。

相信到這裡大家就應該理解了什麼樣的物件是短期存活的物件,什麼樣的物件是長期存活的物件,那麼它們是如何分別存在年輕代和老年代中的呢?為什麼要這麼區分呢?

其實這與垃圾回收機制是密不可分的。

對於年輕代裡的物件,他們的特點是建立後很快就會被回收,而對於老年代裡的物件,他們的特點是需要長期存活,所以這兩種物件是不能用一種垃圾回收演算法進行回收的,所以需要區分成兩個。

對於長期存在的靜態變數sysUser,其實剛開始的時候也是在年輕代的,那它是什麼時候進入老年代的呢?我們下文會講解這個問題。

那永久代又是什麼呢?其實永久代就是我們說的jvm的方法區,用於存放一下類資訊的,這部分之後的文章涉及到會詳解,現在理解到這就可以了。

 

新生代的垃圾回收

前文我們瞭解了,當load方法執行完畢出棧後,裡面的區域性變數sysUser就沒了,堆記憶體中的SysUser物件就沒有引用了,所以會被垃圾回收掉。

那麼問題來了,是沒有引用後就會立即發生垃圾回收,回收掉沒有被引用的物件例項嗎?

其實不是這樣的,垃圾回收是有觸發條件的。

有一個比較常見的場景是這樣的,假設我們的程式碼中建立了大量的物件,導致堆記憶體中囤積了大量的物件,然後這些物件現在都沒有人引用了。

這個時候,如果新生代預先分配的記憶體空間被佔滿了,那麼我們的程式碼此時要新建立一個物件的時候,發現新生代空間滿了,怎麼辦?

這個時候就會觸發一次新生代的垃圾回收,也稱為“Minor GC”或"Young GC",它會嘗試把新生代中沒有人引用的物件給回收掉,釋放空間。

下圖表達了這一過程:

 

 

長期存活的物件什麼時候進入老年代

接下來我們談論一個話題,靜態變數引用的長期存活的物件是什麼時候進入老年代的。

上文我們瞭解到,新生代的物件會經歷一次次的垃圾回收,而被靜態變數引用的物件因為一直被引用,所以一直不會被回收,所以此時jvm就有了一條規定。

如果新生代中的物件,在經歷了15次垃圾回收後,依然堅挺的存活著,那就證明它是個"老年人"了,然後它會被轉移到老年代中。

老年代就是存放這些年齡比較大的物件的。

那麼老年代中的物件會被垃圾回收嗎?

答案是肯定的,因為老年代裡的物件隨著程式碼的執行,也是可以不再被任何人引用的,就需要垃圾回收了。

或者說,隨著越來越多的物件進入老年代,老年代的記憶體也會被佔滿,所以一定是要對老年代進行垃圾回收的。

我們暫時不用考慮具體是怎麼回收的,這個內容在之後的文章中我們會有詳細的解析。

 

總結

今天就給大家準備了這麼多內容,可能有些小夥伴覺得還沒看夠,這些內容都比較簡單,我已經會了,有沒有更深入的東西呢?

別急,學習是循序漸進的事情,王子是想要用最簡單的大白話來和小夥伴們一起討論jvm的原理的,同時也想找一些案例來和大家一起探討,印象會更深刻。

所以今天小夥伴們瞭解到這裡就可以了,讓我們在後續的文章中不見不散,深入討論些更深層的內容吧。

 

 

往期文章推薦:

大白話談JVM的類載入機制

JVM記憶體模型不再是祕密

 

相關文章