1、背景引入
我們從下面這段代碼開始今天的內容:
有沒有很眼熟呀,跟前面我們的代碼差不多。只是這裏稍微調整了一下,在main() 方法中,會循環調用 loadReplicasFormDisk() 方法。
我們來用內存分配的角度來用圖示來描述一下上面的代碼怎麼運行的。
首先,一旦 main() 方法執行,那麼就會把 main() 方法的棧楨壓入 main 線程的Java 虛擬機棧中,如圖:
接着,進入循環體中,會調用 loadReplicasFormDisk() 方法,就會把 loadReplicasFormDisk() 方法的棧楨也壓入到 main 線程的Java 虛擬機裏面去,然後圖示就像下面這樣:
然後,在 loadReplicasFormDisk()方法中,每次都會去堆裏面創建 一個 ReplicaManger 對象的實例,然後通過 loadReplicasFormDisk() 棧楨中的局部變量表中的 “replicaManager” 變量去引用堆中創建的那個 ReplicaManger 對象的實例,看下圖:
2、大部分對象存活時間都是很短的
上圖中,處於 Java 堆裏面的那個 ReplicaManager 對象的實例其實就是一個短暫存活的對象。
爲什麼這麼說呢?我們繼續分析上面的代碼。
大家可以再看一下代碼。你會發現,我們是在 main 方法中,用循環的方式去調用 loadReplicasFormDisk() 方法,loadReplicasFormDisk() 裏面去創建的。
那麼這裏每一次循環結束,loadReplicasFormDisk() 方法的棧楨就從 main 線程的Java 虛擬機棧中出棧了。也就意味着不再有變量再去引用處於 Java 堆中的那個 ReplicaManager 對象的實例了。
根據我們上週的垃圾回收的知識,那麼這裏沒有被引用的這個 處於Java 堆中的 ReplicaManager 對象實例,就會很快被垃圾回收掉的。
下一次循環的時候,繼續走上面的流程,把 loadReplicasFormDisk() 方法的棧楨加入到 main 線程的 Java虛擬機棧中,然後在Java 堆中創建一個ReplicaManager 對象,並且讓 loadReplicasFormDisk() 棧楨中的局部變量 replicaManager來引用這個對象。
當 replicaManager.load() 執行完成後,loadReplicasFormDisk() 方法結束,loadReplicasFormDisk 虛擬機棧出棧。後面的每次循環都是這個操作。
所以每次在 Java 堆中創建的 ReplicaManager 對象的實例存活週期其實都是很短的。調用 loadReplicasFormDisk() 方法的時候被創建,執行 replicaManager.load() 方法後被回收。
大家也可以想想自己平常寫的代碼是不是大多數時候都是這樣去寫的。
3、少數部分對象是可以長期存活的
我們調整一下上面的代碼,把創建 ReplicaManager 對象的操作放到類上面來作爲Kafka類的一個 ,loadReplicasFormDisk 中只做 load() 操作。看下面代碼:
上面的代碼給 Kafka 這個類定義了一個叫 “replicaManager” 的靜態變量,這個變量處於 JVM 的方法區中。然後去引用了處於Java 堆裏面的 ReplicaManager 對象實例,看起來圖示就是下面這樣子:
此時在main 方法中,就是週期性的調用 ReplicaManager 對象的 load() 方法。
現在這個處於 Java 堆中的 ReplicaManager 對象,是被 Kafka 的靜態變量 replicaManager 所引用的,他會長期駐留在哪中,是不會被垃圾回收掉的。
因爲它長期駐留在內存中,還週期性的被調用,所以它也就成爲了一個長時間存活的對象了。
4、JVM 的分代模型:年輕代和老年代
大家通過上面的兩段代碼可以發現,我們不同代碼的編寫方式,決定了這些對象的生存週期。
所以JVM 將Java 堆內存劃分爲了 兩個區域,一個是年輕代,一個是老年代。
年輕代就是存放那種使用完就立馬回收的對象。
而老年代則用來存放那些長期駐留在內存中的對象。
用上面的兩段代碼融合來看一下整個流程:
首先 Kafka 中的靜態變量 fetcher 引用了 ReplicaFetcher 對象,這是長期需要駐留在內存中的,會放到老年代中。這裏其實還是會短暫的先放到年輕代裏面的,知識最終會放到老年代區,至於爲什麼,後面會細講,這裏你只需要認爲它會被放到老年代中去就行了。
接着開始執行 main 方法,進去後會先執行loadReplicasFormDisk() 方法,此時會創建一個 loadReplicasFormDisk() 方法的棧楨壓入 main 線程的 Java 虛擬機棧中。
loadReplicasFormDisk() 方法中會使用一個局部變量表中的 “replicaManager” 變量去指向在Java 堆中年輕代裏創建的一個 ReplicaManger 對象。因爲 loadReplicasFormDisk() 方法調用完成以後,就沒有變量在指向 這個 ReplicaManger 對象了。所以丟在年輕代裏面的,如下圖:
當 loadReplicasFormDisk() 執行完畢以後,loadReplicasFormDisk()方法的棧幀就會出棧,對應年輕代中的 ReplicaManger 對象也會被回收掉,回收後入下圖:
接着會執行 while 循環代碼,會週期性的去調用 ReplicaFetcher的 fetch() 方法。
但是 ReplicaFetcher 這個對象是被 kafka 的靜態變量 fathcer 給引用了,所以他會存在於 老年代中,持續被使用。
5、爲什麼要分成年輕代和老年代
從上面的分析,我們可以看到哪些代碼會在年輕代?存活時間短的。哪些代碼對象會在老年代?存活時間長的。
但是爲什麼要這麼分呢?
其實這裏還跟垃圾回收的機制有關係,對於存活時間短的和長期存活的對象,回收的算法是不一致的。
關於垃圾回收算法,會在第三週中詳細講到的。這裏我們就關注 JVM 的劃分就好。
6、永久代
其實我們這裏說的 JVM 中的永久代,就是上面圖中的方法區。暫時可以任務它就是來存放類的信息的。
7、最後
今天我們結合了實際的代碼例子來講述了 JVM的內存模型劃分,也通過圖示說明了哪些對象是放到哪塊區域的,大家可以好好理解一下。
後面一篇會帶來實戰第二週的學習總結:《你的Java對象在JVM中是怎麼分配和流轉的》,將說明對象第一次是分配在哪裏?什麼時候會觸發垃圾回收,新生代的回收,老年代的回收?等等...