本課時我們主要講解 JVM 的內存劃分以及棧上的執行過程。這塊內容在面試中主要涉及以下這 3 個面試題:
* JVM 是如何進行內存區域劃分的?
* JVM 如何高效進行內存管理?
* 為什么需要有元空間,它又涉及什么問題?
帶著這 3 個問題,我們開始今天的學習,關于內存劃分的知識我希望在本課時你能夠理解就可以,不需要死記硬背,因為在后面的課時我們會經常使用到本課時學習的內容,也會結合工作中的場景具體問題具體分析,這樣你可以對 JVM 的內存獲得更深刻的認識。
首先,第一個問題:**JVM的內存區域是怎么高效劃分的**?這也是一個高頻的面試題。很多同學可能通過死記硬背的方式來應對這個問題,這樣不僅對知識沒有融會貫通在面試中還很容易忘記答案。
為什么要問到 JVM 的內存區域劃分呢?因為 Java 引以為豪的就是它的自動內存管理機制。相比于 C++的手動內存管理、復雜難以理解的指針等,Java 程序寫起來就方便的多。
然而這種呼之即來揮之即去的內存申請和釋放方式,自然也有它的代價。為了管理這些快速的內存申請釋放操作,就必須引入一個池子來延遲這些內存區域的回收操作。
我們常說的內存回收,就是針對這個池子的操作。我們把上面說的這個池子,叫作堆,可以暫時把它看成一個整體。
### JVM 內存布局
程序想要運行,就需要數據。有了數據,就需要在內存上存儲。那你可以回想一下,我們的 C++ 程序是怎么運行的?是不是也是這樣?
Java 程序的數據結構是非常豐富的。其中的內容,舉一些例子:
* 靜態成員變量
* 動態成員變量
* 區域變量
* 短小緊湊的對象聲明
* 龐大復雜的內存申請
這么多不同的數據結構,到底是在什么地方存儲的,它們之間又是怎么進行交互的呢?是不是經常在面試的時候被問到這些問題?
我們先看一下 JVM 的內存布局。隨著 Java 的發展,內存布局一直在調整之中。比如,Java 8 及之后的版本,徹底移除了持久代,而使用 Metaspace 來進行替代。這也表示著 -XX:PermSize 和 -XX:MaxPermSize 等參數調優,已經沒有了意義。但大體上,比較重要的內存區域是固定的。

JVM 內存區域劃分如圖所示,從圖中我們可以看出:
* JVM 堆中的數據是共享的,是占用內存最大的一塊區域。
* 可以執行字節碼的模塊叫作執行引擎。
* 執行引擎在線程切換時怎么恢復?依靠的就是程序計數器。
* JVM 的內存劃分與多線程是息息相關的。像我們程序中運行時用到的棧,以及本地方法棧,它們的維度都是線程。
* 本地內存包含元數據區和一些直接內存。
一般情況下,只要你能答出上面這些主要的區域,面試官都會滿意的點頭。但如果深挖下去,可能就有同學就比較頭疼了。下面我們就詳細看下這個過程。
### 虛擬機棧

棧是什么樣的數據結構?你可以想象一下子彈上膛的這個過程,后進的子彈最先射出,最上面的子彈就相當于棧頂。
我們在上面提到,Java 虛擬機棧是基于線程的。哪怕你只有一個 main() 方法,也是以線程的方式運行的。在線程的生命周期中,參與計算的數據會頻繁地入棧和出棧,棧的生命周期是和線程一樣的。
棧里的每條數據,就是棧幀。在每個 Java 方法被調用的時候,都會創建一個棧幀,并入棧。一旦完成相應的調用,則出棧。所有的棧幀都出棧后,線程也就結束了。每個棧幀,都包含四個區域:
* 局部變量表
* 操作數棧
* 動態連接
* 返回地址
我們的應用程序,就是在不斷操作這些內存空間中完成的。

本地方法棧是和虛擬機棧非常相似的一個區域,它服務的對象是 native 方法。你甚至可以認為虛擬機棧和本地方法棧是同一個區域,這并不影響我們對 JVM 的了解。
這里有一個比較特殊的數據類型叫作 returnAdress。因為這種類型只存在于字節碼層面,所以我們平常打交道的比較少。對于 JVM 來說,程序就是存儲在方法區的字節碼指令,而 returnAddress 類型的值就是指向特定指令內存地址的指針。

這部分有兩個比較有意思的內容,面試中說出來會讓面試官眼前一亮。
* 這里有一個兩層的棧。第一層是棧幀,對應著方法;第二層是方法的執行,對應著操作數。注意千萬不要搞混了。
* 你可以看到,所有的字節碼指令,其實都會抽象成對棧的入棧出棧操作。執行引擎只需要傻瓜式的按順序執行,就可以保證它的正確性。
這一點很神奇,也是基礎。我們接下來從線程角度看一下里面的內容。
### 程序計數器
那么你設想一下,如果我們的程序在線程之間進行切換,憑什么能夠知道這個線程已經執行到什么地方呢?
既然是線程,就代表它在獲取 CPU 時間片上,是不可預知的,需要有一個地方,對線程正在運行的點位進行緩沖記錄,以便在獲取 CPU 時間片時能夠快速恢復。
就好比你停下手中的工作,倒了杯茶,然后如何繼續之前的工作?
程序計數器是一塊較小的內存空間,它的作用可以看作是當前線程所執行的字節碼的行號指示器。這里面存的,就是當前線程執行的進度。下面這張圖,能夠加深大家對這個過程的理解。

可以看到,程序計數器也是因為線程而產生的,與虛擬機棧配合完成計算操作。程序計數器還存儲了當前正在運行的流程,包括正在執行的指令、跳轉、分支、循環、異常處理等。
我們可以看一下程序計數器里面的具體內容。下面這張圖,就是使用 javap 命令輸出的字節碼。大家可以看到在每個 opcode 前面,都有一個序號。就是圖中紅框中的偏移地址,你可以認為它們是程序計數器的內容。

### 堆

堆是 JVM 上最大的內存區域,我們申請的幾乎所有的對象,都是在這里存儲的。我們常說的垃圾回收,操作的對象就是堆。
堆空間一般是程序啟動時,就申請了,但是并不一定會全部使用。
隨著對象的頻繁創建,堆空間占用的越來越多,就需要不定期的對不再使用的對象進行回收。這個在 Java 中,就叫作 GC(Garbage Collection)。
由于對象的大小不一,在長時間運行后,堆空間會被許多細小的碎片占滿,造成空間浪費。所以,僅僅銷毀對象是不夠的,還需要堆空間整理。這個過程非常的復雜,我們會在后面有專門的課時進行介紹。
那一個對象創建的時候,到底是在堆上分配,還是在棧上分配呢?這和兩個方面有關:對象的類型和在 Java 類中存在的位置。
Java 的對象可以分為基本數據類型和普通對象。
對于普通對象來說,JVM 會首先在堆上創建對象,然后在其他地方使用的其實是它的引用。比如,把這個引用保存在虛擬機棧的局部變量表中。
對于基本數據類型來說(byte、short、int、long、float、double、char),有兩種情況。
我們上面提到,每個線程擁有一個虛擬機棧。當你在方法體內聲明了基本數據類型的對象,它就會在棧上直接分配。其他情況,都是在堆上分配。
注意,像 int[] 數組這樣的內容,是在堆上分配的。數組并不是基本數據類型。

這就是 JVM 的基本的內存分配策略。而堆是所有線程共享的,如果是多個線程訪問,會涉及數據同步問題。這同樣是個大話題,我們在這里先留下一個懸念。
### 元空間
關于元空間,我們還是以一個非常高頻的面試題開始:“為什么有 Metaspace 區域?它有什么問題?”
說到這里,你應該回想一下類與對象的區別。對象是一個活生生的個體,可以參與到程序的運行中;類更像是一個模版,定義了一系列屬性和操作。那么你可以設想一下。我們前面生成的 A.class,是放在 JVM 的哪個區域的?
想要問答這個問題,就不得不提下 Java 的歷史。在 Java 8 之前,這些類的信息是放在一個叫 Perm 區的內存里面的。更早版本,甚至 String.intern 相關的運行時常量池也放在這里。這個區域有大小限制,很容易造成 JVM 內存溢出,從而造成 JVM 崩潰。
Perm 區在 Java 8 中已經被徹底廢除,取而代之的是 Metaspace。原來的 Perm 區是在堆上的,現在的元空間是在非堆上的,這是背景。關于它們的對比,可以看下這張圖。

然后,元空間的好處也是它的壞處。使用非堆可以使用操作系統的內存,JVM 不會再出現方法區的內存溢出;但是,無限制的使用會造成操作系統的死亡。所以,一般也會使用參數 -XX:MaxMetaspaceSize 來控制大小。
方法區,作為一個概念,依然存在。它的物理存儲的容器,就是 Metaspace。我們將在后面的課時中,再次遇到它。現在,你只需要了解到,這個區域存儲的內容,包括:類的信息、常量池、方法數據、方法代碼就可以了。
### 小結
好了,到這里本課時的基本內容就講完了,針對這塊的內容在面試中還經常會遇到下面這兩個問題。
* 我們常說的字符串常量,存放在哪呢?
由于常量池,在 Java 7 之后,放到了堆中,我們創建的字符串,將會在堆上分配。
>[info]備注:JVM中存在多個常量池。把第一個改成字符串常量池就比較好理解了。
1、字符串常量池,已經移動到堆上(jdk8之前是perm區),也就是執行intern方法后存的地方。
2、類文件常量池,constant_pool,是每個類每個接口所擁有的,第四節字節碼中“#n”的那些都是。這部分數據在方法區,也就是元數據區。而運行時常量池是在類加載后的一個內存區域,它們都在元空間。
* 堆、非堆、本地內存,有什么關系?
關于它們的關系,我們可以看一張圖。在我的感覺里,堆是軟綿綿的,松散而有彈性;而非堆是冰冷生硬的,內存非常緊湊。

大家都知道,JVM 在運行時,會從操作系統申請大塊的堆內內存,進行數據的存儲。但是,堆外內存也就是申請后操作系統剩余的內存,也會有部分受到 JVM 的控制。比較典型的就是一些 native 關鍵詞修飾的方法,以及對內存的申請和處理。
在 Linux 機器上,使用 top 或者 ps 命令,在大多數情況下,能夠看到 RSS 段(實際的內存占用),是大于給 JVM 分配的堆內存的。
如果你申請了一臺系統內存為 2GB 的主機,可能 JVM 能用的就只有 1GB,這便是一個限制。
### 總結
JVM 的運行時區域是棧,而存儲區域是堆。很多變量,其實在編譯期就已經固定了。.class 文件的字節碼,由于助記符的作用,理解起來并不是那么吃力,我們將在課程最后幾個課時,從字節碼層面看一下多線程的特性。
JVM 的運行時特性,以及字節碼,是比較偏底層的知識。本課時屬于初步介紹,有些部分并未深入講解。希望你應該能夠在腦海里建立一個 Java 程序怎么運行的概念,以便我們在后面的課時中,提到相應的內存區域時,有個整體的印象。
#### 課后問答
* 1、被final修飾的成員變量,會被gc嗎?
答:對象是否被GC,和是否是final變量沒有關系。
* 2、類加載器是加載字節碼變成機器碼給執行引擎去執行的,那么類加載器是誰來加載的?
答:啟動類加載器,就是最上面那一個,是c代碼實現的,沒有繼承classloader類。它就是一段native邏輯,所以沒有加載這種概念。它的實現參考${openjdk}\hotspot\src\share\vm\classfile 目錄下的 classLoader.cpp 與classLoader.hpp
* 3、JMM 保證了 read、load、assign、use、store 和 write 六個操作具有原子性,可以認為除了 long 和 double 類型以外,對其他基本數據類型所對應的內存單元的訪問讀寫都是原子的。long和double沒有原子性?
答:目前大多數機器是64位的,你可以認為是原子的。這是因為,在32位操作系統上對64位的數據的讀寫要分兩步完成,每一步取32位數據。隨著時間推移,這種知識點會越來越冷。
* 4、字節碼的執行流程可以這樣理解嗎?字節碼在Java虛擬機棧中被執行,每一項內容都可以看作是一個棧幀,棧幀的結構包括局部變量表、操作數棧、鏈接、返回地址。這時候就很明了了,棧幀的執行流程就是字節碼的執行流程了。類中變量會被解析到局部變量表,然后對操作數棧進行入棧出棧的操作,在此期間有可能引用到動態或靜態鏈接,最后把計算結果的引用地址返回。不知道這個理解是否正確,麻煩老師指正,謝謝?
答:正確
* 5、“它只是自定義的加載器順序不同,但對于頂層來說,還是一樣的。”這句話是什么意思呢?為什么自己寫的ArrayList不會被加載?
答:loadClass的邏輯是可以非常靈活的,以下代碼來自tomcat-9.0.30。
```
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
```
第一步就是嘗試從javabase加載哦,加載不到才走其他邏輯。感興趣可以參照WebappClassLoaderBase.java文件。
* 6、這里有一個兩層的棧。第一層是棧幀,對應著方法;第二層是方法的執行,對應著操作數棧;這里是說棧幀是說具體的Java方法,而真正的調用,是在棧幀中里面還建了一個操作數棧對嗎?為什么要這么做呀?
答:線程方法棧(棧)->棧幀(元素)=>方法級別的操作。
棧幀里的操作數棧(棧)->操作數(元素)=> 字節碼指令級的操作。
主管的功能不同,層次也不同。
* 7、“由于常量池,在 Java 7 之后,放到了堆中,我們創建的字符串,將會在堆上分配”。但是您上文也說了,JAVA8開始,metasapce是非堆區域,而且文中也提到了該區域包含的內容是類的信息、常量池、方法數據、方法代碼。那么java字符串常量,就不應該在堆上創建了啊。勞煩您解釋下,添麻煩了,謝謝。
答案:JVM中存在多個常量池。把第一個改成字符串常量池就比較好理解了。
1、字符串常量池,已經移動到堆上(jdk8之前是perm區),也就是執行intern方法后存的地方。
2、類文件常量池,constant_pool,是每個類每個接口所擁有的,第四節字節碼中“#n”的那些都是。這部分數據在方法區,也就是元數據區。而運行時常量池是在類加載后的一個內存區域,它們都在元空間。
* 8、大家都知道,JVM 在運行時,會從操作系統申請大塊的堆內內存,進行數據的存儲。但是,堆外內存也就是申請后操作系統剩余的內存,也會有部分受到 JVM 的控制。比較典型的就是一些 native 關鍵詞修飾的方法,以及對內存的申請和處理 這句話是jvm去申請了一塊操作系統的堆內內存,那圖上怎么jvm申請的內存包括了堆內存和非堆內存,有點疑惑。就是jvm申請的內存其實有這兩個?
答案:可以這樣理解:
操作系統有8G。-Xmx分配了4G(堆內內存),Metaspace使用了256M(堆外內存)
剩下的 8G-4G-256M ,就是操作系統剩下的本地內存。具體有沒有可能變成堆外內存,要看情況。
比如:
(1)netty的direct buffer使用了額外的120MB內存,那么現在JVM占用的堆外內存就有 256M+120M
(2)使用了jni或者jna,直接申請了內存2GB,那么現在JVM占用的堆外內存就有256M+120M+2GB
(3)網絡socket連接等,占用了操作系統的50MB內存
這個時候,留給操作系統的就只剩下了:8GB-4GB-256M-120M-2GB-50M。具體“堆和堆外”一共用了多少,可以top命令,看RSS段。
* 9、字符串常量池,已經移動到堆上(jdk8之前是perm區),jdk8之前 的 perm區 不就是Perm Gen所在的位置嗎?圖上Perm Gen在堆內部,那不就是【字符串常量池一直位于堆上嗎?】難道perm區不在堆內部嗎?
答案:你這么說也沒錯。我覺得你只需要記憶jdk8之后的就可以了,因為這個常量池經過了多次變更。
<jdk7: 處于perm區,屬于堆,但空間單獨管理>=jdk7 處于堆,此堆非彼堆,空間上限也不同 :)>jdk8: perm區沒了,它又不在元空間,也只能說堆了。
* 10、對于基本類型的包裝類也是在棧上分配嗎?
答案:包裝類屬于引用類型,它們不屬于基本類型,所以是在堆上分配的。
* 11、請問虛擬機棧的數據一定是線程安全的嗎?
答案:答案是肯定的,在同一虛擬機棧的數據不需要做同步。建議了解一下“線程封閉”這個概念。主要提到了Ad-hoc、棧封閉、ThreadLocal等。
* 12、上文中提到jdk8之后的版本,廢棄了perm 取而代之的是產生了非堆區matespace,這個區是方法區的物理存儲形式,常量池也存在其中,,下文又說常量池是在堆中創建,請問常量池的存儲形式具體是在哪個區,還是我的理解有誤呢,
答案:JVM中存在多個常量池。把第一個改成字符串常量池就比較好理解了。
1、字符串常量池,已經移動到堆上(jdk8之前是perm區),也就是執行intern方法后存的地方。
2、類文件常量池,constant\_pool,是每個類每個接口所擁有的,第四節字節碼中“#n”的那些都是。這部分數據在方法區,也就是元數據區。而運行時常量池是在類加載后的一個內存區域,它們都在元空間。
* 13、非堆指的就是方法區吧,看您圖中在堆中還畫了一個永久帶(Perm),非堆和永久帶是不是有點重復?在我理解是一回事吧?
答案:你概念好像搞混了。圖中有兩種情況,Java8之前的Perm是屬于堆的,包括里面的方法區;課程默認是Java8及其以后,沒有Perm,此時方法區是在metaspace非堆。另外,非堆也不僅僅只是方法區,更詳細的排查可以參考第13課時。
* 14、運行時常量池和字符串常量池什么區別?string常量池是在堆中還是元空間?
答案:JVM中存在多個常量池。1、字符串常量池,已經移動到堆上(jdk8之前是perm區),也就是執行intern方法后存的地方。2、類文件常量池,constant_pool,是每個類每個接口所擁有的,第四節字節碼中“#n”的那些都是。這部分數據在方法區,也就是元數據區。而運行時常量池是在類加載后的一個內存區域,它們都在元空間。
* 15、請教一個問題,就是我在main主線程中新建了兩個線程,其中一個線程發生了oom,但是我發現發生oom后,有問題的那個線程把占用的內存都釋放了,其他的線程也沒受到影響,繼續運行。我的疑惑是:我們分配的堆空間不是對整個進程有效嗎?為什么其中一個線程發生了oom,內存會釋放掉呢?且不影響其他線程呢?
答案:你說的這種情況確實存在,少量線程模擬下可以復現,因為GC線程和用戶線程是并行執行的,線程溢出的空間能夠被及時釋放。但是系統一般都是高速運行,有很多線程在運行和并行申請內存,在實際中很難復現,都是雪崩式直接退出。還有很多情況是GC線程瘋狂運轉,直到系統異常,也就是我們后面說的GC線程占用cpu 100%
* 16、請教一個問題,應該是道面試題:創建一個100M的數組,程序OOM,但是分析日志發現 堆內存還大于100M,造成這個問題有哪些情況?這個問題和咱們這篇文章的最后有點像,老師說用top命令觀察 RSS段(實際占用內存)一般是大于分配的堆內存的,文中說“堆外內存也就是申請后操作系統剩余的內存,也會有部分受到 JVM 的控制。比較典型的就是一些 native 關鍵詞修飾的方法,以及對內存的申請和處理”,這塊沒怎么看明白,自己也解釋不通上面的那道題,希望老師幫忙指點下。
答案:這個OOM描述不太清楚,OOM在會發生在很多區域。第101、13小節具體講解了堆外內存的排查,希望對你有所幫助。另外,一些JVM配置參數也會造成此種狀況,比如CMS的預留空間大小。
* 17、文中說除了基本類型,其他都是在堆上分配的。之前粗略在哪個地方看到過jvm會判斷對象是否存在線程逃逸,如果不存在就直接在棧上分配對象。這種在棧上創建對象的情況是怎樣的呢。
答案:你說的沒錯,這種情況在第6小節已聊到了。不過它是分層編譯的優化手段,所以我們在后面JIT小節還會碰到它。
* 18、大家都知道,JVM 在運行時,會從操作系統申請大塊的堆內內存,進行數據的存儲。但是,堆外內存也就是申請后操作系統剩余的內存,也會有部分受到 JVM 的控制。比較典型的就是一些 native 關鍵詞修飾的方法,以及對內存的申請和處理 這句話是jvm去申請了一塊操作系統的堆內內存,那圖上怎么jvm申請的內存包括了堆內存和非堆內存,有點疑惑。就是jvm申請的內存其實有這兩個?
答案:可以這樣理解:
操作系統有8G。-Xmx分配了4G(堆內內存),Metaspace使用了256M(堆外內存)
剩下的 8G-4G-256M ,就是操作系統剩下的本地內存。具體有沒有可能變成堆外內存,要看情況。
比如:
(1)netty的direct buffer使用了額外的120MB內存,那么現在JVM占用的堆外內存就有 256M+120M
(2)使用了jni或者jna,直接申請了內存2GB,那么現在JVM占用的堆外內存就有256M+120M+2GB
(3)網絡socket連接等,占用了操作系統的50MB內存
這個時候,留給操作系統的就只剩下了:8GB-4GB-256M-120M-2GB-50M。具體“堆和堆外”一共用了多少,可以top命令,看RSS段。
* 19、老師,枚舉類的內存模型是啥?
答案:java類和字節碼,沒有內存模型這個概念。Java的內存模型,指的是JMM,與多線程協作有關。像堆、虛擬機棧這些劃分,也不叫內存模型,叫內存布局。這個千萬別搞混了。
你應該是說enum的字節碼表現形式。可以使用javac E.java && javap -v -p E看一下輸出。
```
public enum E{
A,
B,
C,
D
}
```
javap輸出:
```
public final class E extends java.lang.Enum<E>
```
可以看到enum只是一種語法上的便捷方式,繼承的是Enum類。
* 20、完整的項目代碼
答案:https://gitee.com/xjjdog/jvm-lagou-res
* 21、“如果幸存區中相同年齡對象大小的和,大于幸存區的一半,大于或等于 age 的對象將會直接進入老年代”應該為小于等于某一年齡的對象大小總和?
答案:感謝提醒,參考代碼share/gc/shared/ageTable.cpp中的compute_tenuring_threshold函數,重新表述如下:從年齡最小的對象開始累加,如果累加的對象大小,大于幸存區的一半,則講當前的對象age將作為新的閾值,年齡大于此閾值的對象直接進入老年代。
* 22、另外“幸存區的一半”最好提一下 TargetSurvivorRatio 這個參數。
答案:值的注意的是。使用“grep -rn -i --color TargetSurvivorRatio .”搜索(jdk13),可以看到這個參數只影響serial和G1收集器,還稍微影響PLAB緩沖區的大小。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充