JVM垃圾回收基本原理和實戰系列之一

1、我們寫的Java代碼是怎麼運行起來的?

首先假設咱們寫好了一份Java代碼,那這份Java代碼中會包含很多“.java”爲後綴的代碼文件,比如User.java,OrderService.java,CustomerManager.java。咱們Java程序員平時在Eclipse、Intellij

IDEA等開發工具中,就有很多類似這樣的Java源代碼文件,當我們寫好這些“.java”後綴的代碼文件之後,接下來要部署到線上的機器上去運行,會怎麼做?一般來說,都是把代碼給打成“.jar”後綴的jar包,或者是“.war”後綴的war包,然後把打包好的jar包或者是war包給放到線上機器去部署。這個部署就有很多種途徑了,但是最基本的一種方式,就是通過Tomcat這類容器來部署代碼,也可以自己手動通過“java”命令來運行一個jar包中的代碼。如下圖:

在這裏插入圖片描述
這裏有一個非常關鍵的步驟,那就是“編譯”,也就是說,在我們寫好的“.java”代碼打包的過程中,一般就會把代碼編譯成“.class”後綴的字節碼文件,比如“User.class”,“Hello.class”,”Customer.class“,然後這個“.class”後綴的字節碼文件,纔是可以被運行起來的!所以,首先,無論對JVM機制是否熟悉,都先來回顧一下這個編譯的過程,以及“.class”字節碼文件的概念。如下圖:
在這裏插入圖片描述
接着需要思考下一個問題:對於編譯好的這些“.class”字節碼,怎麼讓他們運行起來?這個時候就需要使用諸如“java -jar”之類的命令來運行我們寫好的代碼了。此時,一旦採用“java”命令,實際上就會啓動一個JVM進程,這個JVM進程就會來負責運行這些“.class”字節碼文件,也就相當於是負責運行我們寫好的系統。所以,平時我們寫好的某個系統在一臺機器上部署的時候,一旦啓動這個系統,其實就是啓動了一個JVM進程,由它來負責運行這臺機器上運行的這個系統。如下圖:
在這裏插入圖片描述
接着下一步,JVM進程要運行這些“.class”字節碼文件中的代碼,是不是首先得把這些“.class”文件中包含的各種類給加載進去?這些“.class”文件不就是我們寫好的一個一個的類嗎?此時就會有一個“類加載器”的概念,此時會採用類加載器把編譯好的那些“.class”字節碼文件給加載到JVM中,然後供後續代碼運行來使用。如下圖:
在這裏插入圖片描述
接着,最後一步,JVM就會基於自己的字節碼執行引擎,來執行加載到內存裏的我們寫好的那些類了,比如代碼中有一個“main()”方法,那麼JVM就會從這個“main()”方法開始執行裏面的代碼。它需要哪個類的時候,就會使用類加載器來加載對應的類,反正對應的類就在“.class”文件中。如下圖:
在這裏插入圖片描述

2、Java類加載機制是怎樣的?

JVM會在什麼情況下去加載一個類呢?其實,類加載過程非常的瑣碎複雜,但是對於我們平時從工作中實用的角度來說,主要把握它的核心工作原理就可以。一個類從加載到使用,一般會經歷下面的這個過程:加載 -> 驗證 -> 準備 -> 解析 -> 初始化 -> 使用 -> 卸載。所以,首先要搞明白的第一個問題,就是JVM在執行我們寫好的代碼的過程中,一般在什麼情況下會去加載一個類呢?也就是說,啥時候會從“.class”字節碼文件中加載這個類到JVM內存裏來。其實答案非常簡單,就是在代碼中用到這個類的時候。舉個簡單的例子,比如下面有一個類(Kafka.class),裏面有一個“main()”方法作爲主入口。那麼一旦JVM進程啓動之後,它一定會先把這個類(Kafka.cass)加載到內存裏,然後從“main()”方法的入口代碼開始執行。
在這裏插入圖片描述
假設main方法中有這麼一行代碼,如下圖:
在這裏插入圖片描述
這時可能大家就想了,代碼中明顯需要使用“ReplicaManager”這個類去實例化一個對象,此時必須得把“ReplicaManager.class”字節碼文件中的這個類加載到內存裏來啊!所以這個時候就會觸發JVM通過類加載器,從“ReplicaManager.class”字節碼文件中加載對應的類到內存裏來使用,這樣代碼才能跑起來。如下圖:
在這裏插入圖片描述
簡單概括一下:首先代碼中包含“main()”方法的主類一定會在JVM進程啓動之後被加載到內存,開始執行“main()”方法中的代碼。接着遇到使用了別的類,比如“ReplicaManager”,此時就會從對應的“.class”字節碼文件加載對應的類到內存裏來。下面從使用角度出發,來看看驗證、準備和初始化的過程。
其實對於這三個概念,沒太大的必要去深究裏面的細節,這裏面的細節很多很繁瑣,對於大部分Java開發者而言,只要腦子裏有下面的幾個概念就可以了:

(1)驗證階段

簡單來說,這一步就是根據Java虛擬機規範,來校驗加載進來的“.class”文件中的內容,是否符合指定的規範。這個相信很好理解,假如說,“.class”文件被人篡改了,裏面的字節碼壓根兒不符合規範,那麼JVM是沒法去執行這個字節碼的!所以把“.class”字節碼文件加載到內存裏之後,必須先驗證一下,校驗它必須完全符合JVM規範,後續才能交給JVM來運行。如下圖:
在這裏插入圖片描述

(2)準備階段

假設有這麼一個“ReplicaManager”類,它的“ReplicaManager.class”文件內容剛剛被加載到內存之後,會進行驗證,確認這個字節碼文件的內容是規範的。接着就會進行準備工作,這個準備工作,其實就是給這個“ReplicaManager”類分配一定的內存空間,然後給它裏面的類變量(也就是static修飾的變量)分配內存空間,來一個默認的初始值。比如上面的示例裏,就會給“flushInterval”這個類變量分配內存空間,給一個“0”的初始值。如下圖:
在這裏插入圖片描述

(3)解析階段

這個階段乾的事兒,實際上是把符號引用替換爲直接引用的過程,其實這個部分的內容很複雜,涉及到JVM的底層。針對這部分內容,沒必要去深究,因爲從實用角度而言,對很多Java開發者在工作中實踐JVM技術其實也用不到,所以這裏暫時知道有這麼一個階段就可以了。如下圖:
在這裏插入圖片描述
核心階段:初始化

前面說過,在準備階段時,就會把“ReplicaManager”類給分配好內存空間,另外它的一個類變量“flushInterval”也會給一個默認的初始值“0”,那麼接下來,在初始化階段,就會正式執行類初始化代碼了。那麼,什麼是類初始化代碼呢?如下圖:
在這裏插入圖片描述
可以看到,對於“flushInterval”這個類變量,通過Configuration. getInt(“replica.flush.interval”)這段代碼來獲取一個值,並且賦值給它。但是在準備階段會執行這個賦值邏輯嗎?答案是NO!在準備階段,僅僅是給“flushInterval”類變量開闢一個內存空間,然後給個初始值“0”罷了。那麼這段賦值的代碼什麼時候執行呢?答案是在“初始化”階段來執行。

在初始化這個階段,就會執行類的初始化代碼,比如上面的 Configuration.getInt(“replica.flush.interval”) 代碼就會在這裏執行,完成一個配置項的讀取,然後賦值給這個類變量“flushInterval”。另外比如下圖的static靜態代碼塊,也會在這個階段來執行。類似下面的代碼語義,可以理解爲類初始化的時候,調用“loadReplicaFromDish()”方法從磁盤中加載數據副本,並且放在靜態變量“replicas”中:
在這裏插入圖片描述
搞明白了類的初始化是什麼,再來看看類的初始化規則。什麼時候會初始化一個類?一般來說有以下一些時機:比如“new ReplicaManager()”來實例化類的對象了,此時就會觸發類的加載到初始化的全過程,把這個類準備好,然後再實例化一個對象出來;或者是包含“main()”方法的主類,必須是立馬初始化的。此外,這裏還有一個非常重要的規則,就是如果初始化一個類的時候,發現他的父類還沒初始化,那麼必須先初始化他的父類,比如下面的代碼:
在這裏插入圖片描述
如果要“new ReplicaManager()”初始化這個類的實例,那麼會加載這個類,然後初始化這個類,但是初始化這個類之前,發現AbstractDataManager作爲父類還沒加載和初始化,那麼必須先加載這個父類,並且初始化這個父類。這個規則,必須得牢記,如下圖:
在這裏插入圖片描述
類加載器和雙親委派機制

現在搞明白了整個類加載從觸發時機到初始化的過程,接着再闡述一下類加載器的概念。因爲實現上述過程,那必須是依靠類加載器來實現的,那麼Java裏有哪些類加載器呢?簡單來說有下面幾種:

(1)啓動類加載器

Bootstrap ClassLoader,它主要是負責加載在機器上安裝的Java目錄下的核心類的。相信大家都知道,如果要在一個機器上運行自己寫好的Java系統,無論是Windows筆記本,還是Linux服務器,是不是都得裝一個JDK?那麼在Java安裝目錄下,就有一個“lib”目錄,大家可以自己去找找看,這裏就有Java最核心的一些類庫,支撐Java系統的運行。所以一旦JVM啓動,那麼首先就會依託啓動類加載器,去加載Java安裝目錄下的“lib”目錄中的核心類庫。

(2)擴展類加載器

Extension ClassLoader,這個類加載器其實也是類似的,就是Java安裝目錄下,有一個“lib\ext”目錄,這裏面有一些類,就是需要使用這個類加載器來加載的,支撐Java系統的運行。那麼JVM一旦啓動,是不是也得從Java安裝目錄下,加載這個“lib\ext”目錄中的類。

(3)應用程序類加載器

Application ClassLoader,這類加載器就負責去加載“ClassPath”環境變量所指定的路徑中的類,其實大致就理解爲去加載寫好的Java代碼,這個類加載器就負責加載寫好的那些類到內存裏。

(4)自定義類加載器

除了上面那幾種類加載器之外,還可以自定義類加載器,去根據自己的需求加載Java類。

JVM的類加載器是有親子層級結構的,就是說啓動類加載器是最上層的,擴展類加載器在第二層,第三層是應用程序類加載器,最後一層是自定義類加載器。如下圖:
在這裏插入圖片描述
基於這個親子層級結構,有一個雙親委派的機制,什麼意思呢?就是假設應用程序類加載器需要加載一個類,它首先會委派給自己的父類加載器去加載,最終傳導到頂層的類加載器去加載。但是如果父類加載器在自己負責加載的範圍內,沒找到這個類,那麼就會下推加載權利給自己的子類加載器。

比如JVM現在需要加載“ReplicaManager”類,此時應用程序類加載器會問問自己的爸爸,也就是擴展類加載器,你能加載到這個類嗎?然後擴展類加載器直接問自己的爸爸,啓動類加載器,你能加載到這個類嗎?啓動類加載器心想,我在Java安裝目錄下,沒找到這個類啊,自己找去!然後,就下推加載權利給擴展類加載器這個兒子,結果擴展類加載器找了半天,也沒找到自己負責的目錄中有這個類,這時它又會下推加載權利給應用程序類加載器這個兒子。然後應用程序類加載器在自己負責的範圍內,比如就是寫好的那個系統打包成的jar包,一下子發現,就在這裏!然後就自己把這個類加載到內存裏去了。這就是所謂的雙親委派模型:先找父親去加載,不行的話再由兒子來加載,這樣的話,可以避免多層級的加載器結構重複加載某些類。如下圖:
在這裏插入圖片描述

3、JVM中有哪些內存區域,分別用來幹嘛?

到底什麼是JVM的內存區域劃分?其實這個問題非常簡單,JVM在運行我們寫好的代碼時,它是必須使用多塊內存空間的,不同的內存空間用來放不同的數據,然後配合我們寫的代碼流程,才能讓我們的系統運行起來。舉個簡單的例子,比如咱們現在知道了JVM會加載類到內存裏來供後續運行,那麼這些類加載到內存以後,放到哪兒去了呢?所以JVM裏就必須有一塊內存區域,用來存放我們寫的那些類。如下圖:
在這裏插入圖片描述
繼續來看,我們的代碼運行起來時,是不是需要執行寫的一個一個的方法?那麼運行方法的時候,方法裏面有很多變量之類的東西,是不是需要放在某個內存區域裏?接着如果我們寫的代碼裏創建了一些對象,這些對象是不是也需要內存空間來存放?如下圖:
在這裏插入圖片描述
這就是爲什麼JVM中必須劃分出來不同的內存區域,它是爲了我們寫好的代碼在運行過程中根據需要來使用的。接下來,就依次看看JVM中有哪些內存區域。

(1)存放類的方法區

這個方法區是在JDK 1.8以前的版本里,代表JVM中的一塊區域,主要是放從“.class”文件里加載進來的類,還會有一些類似常量池的東西放在這個區域裏。但是在JDK 1.8以後,這塊區域的名字改了,叫做“Metaspace”,可以認爲是“元數據空間”這樣的意思。當然這裏主要還是存放我們自己寫的各種類相關的信息。舉個例子,假設有一個“Kafka.class”類和“ReplicaManager.class”類,如下圖:
在這裏插入圖片描述
這兩個類加載到JVM後,就會放在這個方法區中,如下圖:
在這裏插入圖片描述

(2)執行代碼指令用的程序計數器

假設代碼如下圖:
在這裏插入圖片描述
前面講過,實際上上面這段代碼首先會存在於“.java”後綴的文件裏,這個文件就是java源代碼文件。但是這個文件是面向我們程序員的,計算機它是看不懂這段代碼的。所以此時就得通過編譯器,把“.java”後綴的源代碼文件編譯爲“.class”後綴的字節碼文件。這個“.class”後綴的字節碼文件裏,存放的就是對寫出來的代碼編譯好的字節碼了。字節碼纔是計算器可以理解的一種語言,而不是我們寫出來的那一堆代碼。字節碼看起來大概是下面這樣的,跟上面的代碼無關,就是一個示例而已。
在這裏插入圖片描述
比如“0: aload_0”這樣的,就是“字節碼指令”,它對應了一條一條的機器指令,計算機只有讀到這種機器碼指令,才知道具體應該要幹什麼。比如字節碼指令可能會讓計算機從內存裏讀取某個數據,或者把某個數據寫入到內存裏去,都有可能,各種各樣的指令就會指示計算機去幹各種各樣的事情。現在Java代碼通過JVM跑起來的第一件事情就明確了, 首先Java代碼被編譯成字節碼指令,然後字節碼指令一定會被一條一條執行,這樣才能實現我們寫好的代碼執行的效果。所以當JVM加載類信息到內存之後,實際就會使用自己的字節碼執行引擎,去執行我們寫的代碼編譯出來的代碼指令。如下圖:
在這裏插入圖片描述
那麼在執行字節碼指令的時候,JVM裏就需要一個特殊的內存區域,那就是“程序計數器”,這個程序計數器就是用來記錄當前執行的字節碼指令的位置的,也就是記錄目前執行到了哪一條字節碼指令。如下圖:
在這裏插入圖片描述
大家都知道JVM是支持多線程的,所以其實寫好的代碼可能會開啓多個線程併發執行不同的代碼,所以就會有多個線程來併發的執行不同的代碼指令。因此每個線程都會有自己的一個程序計數器,專門記錄當前這個線程目前執行到了哪一條字節碼指令了。如下圖:
在這裏插入圖片描述

(3)Java虛擬機棧

Java代碼在執行的時候,一定是線程來執行某個方法中的代碼,哪怕就是下面的代碼,也會有一個main線程來執行main()方法裏的代碼。在main線程執行main()方法的代碼指令的時候,就會通過main線程對應的程序計數器記錄自己執行的指令位置。如下圖:
在這裏插入圖片描述
但是在方法裏,我們經常會定義一些方法內的局部變量,比如在上面的main()方法裏,其實就有一個“replicaManager”局部變量,它是引用一個ReplicaManager實例對象的,關於這個對象我們先不管它,先來看方法和局部變量。因此,JVM必須有一塊區域是來保存每個方法內的局部變量等數據的,這個區域就是Java虛擬機棧。每個線程都有自己的Java虛擬機棧,比如這裏的main線程就會有自己的一個Java虛擬機棧,用來存放自己執行的那些方法的局部變量。如果線程執行了一個方法,就會對這個方法調用創建對應的一個棧幀,棧幀裏就有這個方法的局部變量表 、操作數棧、動態鏈接、方法出口等東西。比如main線程執行了main()方法,那麼就會給這個main()方法創建一個棧幀,壓入main線程的Java虛擬機棧,同時在main()方法的棧幀裏,會存放對應的“replicaManager”局部變量。如下圖:
在這裏插入圖片描述
然後,假設main線程繼續執行ReplicaManager對象裏的方法,比如下面這樣,在“loadReplicasFromDisk”方法裏定義了一個局部變量:“hasFinishedLoad”。如下圖:
在這裏插入圖片描述
那麼main線程在執行上面的“loadReplicasFromDisk”方法時,就會爲“loadReplicasFromDisk”方法創建一個棧幀壓入線程自己的Java虛擬機棧裏面去。然後在棧幀的局部變量表裏就會有“hasFinishedLoad”這個局部變量。如下圖:
在這裏插入圖片描述
接着,如果“loadReplicasFromDisk”方法調用了另外一個“isLocalDataCorrupt()”方法 ,這個方法裏也有自己的局部變量,比如下面這樣的代碼。如下圖:
在這裏插入圖片描述
那麼這個時候會給“isLocalDataCorrupt”方法又創建一個棧幀,壓入線程的Java虛擬機棧裏。而且“isLocalDataCorrupt”方法的棧幀的局部變量表裏會有一個“isCorrupt”變量,這是“isLocalDataCorrupt”方法的局部變量,整個過程,如下圖:
在這裏插入圖片描述
接着,如果“isLocalDataCorrupt”方法執行完畢了,就會把“isLocalDataCorrupt”方法對應的棧幀從Java虛擬機棧裏給出棧。然後,如果“loadReplicasFromDisk”方法也執行完畢了,就會把“loadReplicasFromDisk”方法也從Java虛擬機棧裏出棧。

上述就是JVM中的“Java虛擬機棧”這個組件的作用:調用執行任何方法時,都會給方法創建棧幀然後入棧,在棧幀裏存放了這個方法對應的局部變量之類的數據,包括這個方法執行的其他相關的信息,方法執行完畢之後就出棧。如下圖:
在這裏插入圖片描述

(4)Java堆內存

接下來,看看JVM中的另外一個非常關鍵的區域,就是Java堆內存,這裏就是存放在代碼中創建的各種對象的,比如下面的代碼。如下圖:
在這裏插入圖片描述
上面的“new ReplicaManager()”這個代碼就是創建了一個ReplicaManager類的對象實例,這個對象實例裏面會包含一些數據,如下面的代碼所示。這個“ReplicaManager”類裏的“replicaCount”就是屬於這個對象實例的一個數據。類似ReplicaManager這樣的對象實例,就會存放在Java堆內存裏。如下圖:
在這裏插入圖片描述
Java堆內存區域裏會放入類似ReplicaManager的對象,然後因爲在main方法裏創建了ReplicaManager對象,那麼在線程執行main方法代碼的時候,就會在main方法對應的棧幀的局部變量表裏,讓一個引用類型的“replicaManager”局部變量來存放ReplicaManager對象的地址。相當於局部變量表裏的“replicaManager”指向了Java堆內存裏的ReplicaManager對象。如下圖:
在這裏插入圖片描述
把上面這個圖和下面這個總的大圖一起串起來看,再配合整體的代碼,捋一下整體的流程,看起來就會更加清晰。如下圖:
在這裏插入圖片描述
在這裏插入圖片描述
整體流程:

(1首先,JVM進程會啓動,就會先加載Kafka類到內存裏,然後有一個main線程,開始執行Kafka類中的main()方法;

(2main線程是關聯了一個程序計數器的,那麼它執行到哪一行指令,就會記錄在這裏。其次,就是main線程在執行main()方法的時候,會在main線程關聯的Java虛擬機棧裏,壓入一個main()方法的棧幀;

(3接着會發現需要創建一個ReplicaManager類的實例對象,此時會加載ReplicaManager類到內存裏來。然後會創建一個ReplicaManager的對象實例分配在Java堆內存裏,並且在main()方法的棧幀裏的局部變量表引入一個“replicaManager”變量,讓他引用ReplicaManager對象在Java堆內存中的地址;

(4接着,main線程開始執行ReplicaManager對象中的方法,會依次把自己執行到的方法對應的棧幀壓入自己的Java虛擬機棧,執行完方法之後再把方法對應的棧幀從Java虛擬機棧裏出棧。

(5)其它內存區域

其實在JDK很多底層API裏,比如IO相關的,NIO相關的,網絡Socket相關的,如果去看它內部的源碼,會發現很多地方都不是Java代碼了,而是走的native方法去調用本地操作系統裏面的一些方法,可能調用的都是c語言寫的方法,或者一些底層類庫,比如下面這樣的:public native int hashCode()。在調用這種native方法的時候,就會有線程對應的本地方法棧,這個裏面也是跟Java虛擬機棧類似的,也是存放各種native方法的局部變量表之類的信息。

還有一個區域,是不屬於JVM的,通過NIO中的allocateDirect這種API,可以在Java堆外分配內存空間。然後,通過Java虛擬機裏的DirectByteBuffer來引用和操作堆外內存空間。其實很多技術都會用這種方式,因爲有一些場景,堆外內存分配可以大大提升性能,這裏不做過多敘述。

4、JVM的垃圾回收機制用來幹嘛的?

在Java代碼中,一個方法執行完畢之後會怎麼樣?示例代碼如下圖:
在這裏插入圖片描述
現在有個問題,如果“replicaManager.load()”這行代碼執行結束了,此時會怎麼樣?前面說過,一旦方法裏的代碼執行完畢,那麼方法就執行完畢了,也就是說loadReplicasFromDisk()方法就執行完畢了。一旦loadReplicasFromDisk()方法執行完畢,此時就會把loadReplicasFromDisk()方法對應的棧幀從main線程的Java虛擬機棧裏出棧。如下圖:
在這裏插入圖片描述
此時,一旦loadReplicasFromDisk()方法的棧幀出棧,那麼會發現那個棧幀裏的局部變量,“replicaManager”,也就沒有了。也就是說,沒有任何一個變量指向Java堆內存裏的“ReplicaManager”實例對象了。核心點來了,此時發現了,Java堆內存裏的那個“ReplicaManager”實例對象已經沒有人引用它了。這個對象實際上已經沒用了,該乾的事兒都幹完了,現在還讓它留在內存裏幹啥呢?

一般來說,我們在一臺機器上啓動一個Java系統,機器的內存資源是有限的,比如就8個G的內存,然後我們啓動的Java系統本質就是一個JVM進程,它負責運行我們的系統的代碼。那麼這個JVM進程本身也是會佔用機器上的部分內存資源,比如佔用2G的內存資源。那麼,我們在JVM的Java堆內存中創建的對象,其實本質也是會佔用JVM的內存資源的,比如“ReplicaManager”實例對象,會佔用500字節的內存。如下圖:
在這裏插入圖片描述
繼續看上面這張圖,既然“ReplicaManager”對象實例是不需要繼續使用的,已經沒有任何方法的局部變量在引用這個實例對象了,而且它還空佔着內存資源,那麼應該怎麼處理呢?答案呼之欲出:JVM的垃圾回收機制。JVM本身是有垃圾回收機制的,它是一個後臺自動運行的線程。只要啓動一個JVM進程,它就會自帶這麼一個垃圾回收的後臺線程。這個線程會在後臺不斷檢查JVM堆內存中的各個實例對象。如下圖:
在這裏插入圖片描述
如果某個實例對象沒有任何一個方法的局部變量指向它,也沒有任何一個類的靜態變量,包括常量等在指向它,那麼這個垃圾回收線程,就會把這個沒人指向的“ReplicaManager”實例對象給回收掉,從內存裏清除掉,讓它不再佔用任何內存資源。這樣的話,這些不再被人指向的對象實例,即JVM中的“垃圾”,就會定期的被後臺垃圾回收線程清理掉,不斷釋放內存資源。如下圖:
在這裏插入圖片描述
到此爲止,相信大家就很清晰明瞭了,到底什麼是JVM中的“垃圾”?什麼又是JVM的“垃圾回收”?

5、JVM分代模型:年輕代、老年代、永久代

大家應該都知道一點,那就是我們在代碼裏創建的對象,都會進入到Java堆內存中,如下圖:
在這裏插入圖片描述這段代碼,稍微做了點改動,在main()方法裏,會週期性的執行loadReplicasFromDisk()方法,加載副本數據。首先,一旦執行main()方法,那麼就會把main()方法的棧幀壓入main線程的Java虛擬機棧,如下圖:
在這裏插入圖片描述
然後每次在while循環裏,調用loadReplicasFromDisk()方法,就會把loadReplicasFromDisk()方法的棧幀壓入自己的Java虛擬機棧,如下圖:
在這裏插入圖片描述
接着在執行loadReplicasFromDisk()方法的時候,會在Java堆內存裏創建一個ReplicaManager對象實例,而且loadReplicasFromDisk()方法的棧幀裏會有“replicaManager”局部變量去引用Java堆內存裏的ReplicaManager對象實例,如下圖:
在這裏插入圖片描述
然後就會執行ReplicaManager對象的load()方法。

大部分對象都是存活週期極短的

現在有一個問題,在上面代碼中,那個ReplicaManager對象,實際上屬於短暫存活的這麼一個對象,在loadReplicasFromDisk()方法中創建這個對象,然後執行ReplicaManager對象的load()方法,然後執行完畢之後,loadReplicasFromDisk()方法就會結束。一旦方法結束,那麼loadReplicasFromDisk()方法的棧幀就會出棧,如下圖:
在這裏插入圖片描述
此時一旦沒人引用這個ReplicaManager對象了,就將會被JVM的垃圾回收線程給回收,釋放內存空間,如下圖:
在這裏插入圖片描述
然後在main()方法的while循環裏,下一次循環再次執行loadReplicasFromDisk()方法的時候,又會走一遍上面那個過程,把loadReplicasFromDisk()方法的棧幀壓入Java虛擬機棧,然後構造一個ReplicaManager實例對象放在Java堆裏。一旦執行完ReplicaManager對象的load()方法之後,loadReplicasFromDisk()方法又會結束,再次出棧,然後垃圾回收釋放掉Java堆內存裏的ReplicaManager對象。

所以其實這個ReplicaManager對象,在上面的代碼中,是一個存活週期極爲短暫的對象,每次執行loadReplicasFromDisk()方法的時候,被創建出來,然後執行他的load()方法,接着可能1毫秒之後,就被垃圾回收掉了。從這段代碼就可以明顯看出來,大部分在代碼裏創建的對象,其實都是存活週期很短的。這種對象,其實在我們寫的Java代碼中,佔到絕大部分的比例。

少數對象是長期存活的

下面來看另外一段代碼,假如說用下面這種方式來實現同樣的功能,如下圖:
在這裏插入圖片描述
上面這段代碼的意思,就是給Kafka這個類定義一個靜態變量,也就是“replicaManager”,這個Kafka類是在JVM的方法區裏的。然後讓“replicaManager”引用了一個在Java堆內存裏創建的ReplicaManager實例對象,如下圖:
在這裏插入圖片描述
接着在main()方法中,就會在一個while循環裏,不停的調用ReplicaManager對象的load()方法,做成一個週期性運行的模式。這個時候,就要來思考一下,這個ReplicaManager實例對象,它是會一直被Kafka的靜態變量引用的,然後會一直駐留在Java堆內存裏,是不會被垃圾回收掉的。因爲這個實例對象它需要長期被使用,週期性的被調用load()方法,所以它就成爲了一個長時間存在的對象。

那麼類似這種被類的靜態變量長期引用的對象,它需要長期停留在Java堆內存裏,這種對象就是生存週期很長的對象,它是輕易不會被垃圾回收的,它需要長期存在,不停的去使用它。

JVM分代模型:年輕代和老年代

可以看到,根據寫代碼方式的不同,採用不同的方式來創建和使用對象,其實對象的生存週期是不同的。所以JVM將Java堆內存劃分爲了兩個區域,一個是年輕代,一個是老年代。其中年輕代,顧名思義,就是把第一種代碼示例中的那種,創建和使用完之後立馬就要回收的對象放在裏面;然後老年代呢,就是把第二種代碼示例中的那種,創建之後需要一直長期存在的對象放在裏面,如下圖:
在這裏插入圖片描述
比如下面的代碼,再次改造一下,再結合圖,能夠看得更加明確一些,如下圖:
在這裏插入圖片描述
上面這段代碼稍微複雜了點,稍微解釋一下,Kafka的靜態變量“fetcher”引用了ReplicaFetcher對象,這是長期需要駐留在內存裏使用的,這個對象會在年輕代裏駐留一段時間,但是最終會進入老年代,如下圖:
在這裏插入圖片描述
進入main()方法之後,會先調用loadReplicasFromDisk()方法,業務含義是系統啓動就從磁盤加載一次副本數據,這個方法的棧幀會入棧;然後在這個方法裏面創建了一個ReplicaManager對象,這個對象它是用完就會回收,所以是會放在年輕代裏,由棧幀裏的局部變量來引用,如下圖:
在這裏插入圖片描述
然後一旦loadReplicasFromDisk()方法執行完畢了,方法的棧幀就會出棧,對應的年輕代裏的ReplicaManager對象也會被回收掉,如下圖:
在這裏插入圖片描述
但是接着會執行一段while循環代碼,它會週期性地調用ReplicaFetcher的fetch()方法,去從遠程加載副本數據。所以ReplicaFetcher這個對象因爲被Kafka類的靜態變量fetcher給引用了,所以它會長期存在於老年代裏,持續被使用。

爲什麼要分成年輕代和老年代?

相信看到這裏,大家就一定看明白了,什麼樣的對象是短期存活的對象,什麼樣的對象是長期存在的對象,然後如何分別存在於年輕代和老年代裏。那麼爲什麼需要這麼區分呢?因爲這跟JVM的垃圾回收有關,對於年輕代裏的對象,它們的特點是創建之後很快就會被回收,所以需要用一種垃圾回收算法;對於老年代裏的對象,他們的特點是需要長期存在,所以需要另外一種垃圾回收算法,所以需要分成兩個區域來放不同的對象。

什麼是永久代?

很簡單,JVM裏的永久代簡單說就是方法區,上面那個圖裏的方法區,其實就是所謂的永久代,可以認爲永久代就是放一些類信息的。

文章主要內容摘自:
《從零開始帶你成爲JVM實戰高手》 ————阿里大神*救火隊隊長

花一包煙的錢,看到不一樣的技術世界:

共同學習,共同進步!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章