一. 前言
深入學(xué)習(xí)Jvm的第二篇文章, 依然作為總結(jié), 可能會有點兒乏味, 但卻是面試及虛擬機調(diào)優(yōu)的必備知識. 話不多說, 進入正題.
二. Jvm內(nèi)存模型大致劃分
先直接來張圖:

相信這張圖很多人都不陌生, 也可能有部分同學(xué)只聽說過堆和棧的. 不過沒關(guān)系, 聽在下娓娓道來即可, 先簡單給大伙兒描述一下上圖中每個區(qū)域大概是用來干什么的(會刻意省略掉部分復(fù)雜的東西, 目的是先有個印象, 便于后面知識的理解):
- 棧: 通常用來存放我們的局部變量表, 如果是基礎(chǔ)數(shù)據(jù)類型的變量, 放在棧中. 如果是對象類型的變量, 則實際保存在堆中, 棧中只存放該對象的引用地址.
- 堆: 存放對象
- 方法棧: 給本地方法用的棧, 也就是native方法, 如果你看過Thread的類的start()方法, 就會發(fā)現(xiàn)跟蹤到底, 調(diào)用了一個帶有native修飾符的start0()方法, 這意味著它是跨語言調(diào)用, 通常是調(diào)用C或C++的函數(shù)庫.
- 方法區(qū): 我們的常量, 靜態(tài)變量, 以及類元信息都在方法區(qū), 這個區(qū)也叫元空間. 什么類元信息? 如果看過在下的上一篇文章, 你可能會有所了解. 這個類元信息就是類加載到j(luò)vm內(nèi)存后所產(chǎn)生的、關(guān)于該類的所有信息. 需要注意的是, 這塊區(qū)域使用的是直接內(nèi)存, 而不是劃分給虛擬機的內(nèi)存.
- 程序計數(shù)器: 存放Java字節(jié)碼執(zhí)行到哪兒了, 可以粗劣的理解成我們debug時的行號. 這個東西可也是至關(guān)重要的, 試想一下多線程的情況你就明白了, 這么多的線程, 涉及到掛起和線程的上下文切換, 每個線程如何知道被喚醒后下一行該執(zhí)行哪行代碼呢?
- 類裝載子系統(tǒng): 用來將類加載到Jvm的.
- 字節(jié)碼執(zhí)行引擎: 顧名思義, 執(zhí)行字節(jié)碼的嘛.
相信大家注意到那兩個色塊兒了, 線程獨有和線程共享, 什么意思呢?
線程獨有: 會給每個線程單獨分配一小塊內(nèi)存空間
線程共享: 所有線程共享一塊內(nèi)存
這樣說可能不是很理解, 沒關(guān)系, 請繼續(xù)看下文, 因為很多知識需要看完之后加到一塊兒才能理解.
三. 細說Jvm棧
先來個簡單的代碼:
/**
* 簡單的java程序, 用于說明棧的關(guān)系
*
* @Author: deadline
* @Date: 2021-02-27 18:33
*/
public class JvmTestForStack {
public int count() {
int a = 1;
int b = 2;
int c = a + b;
return c;
}
public static void main(String[] args) {
JvmTestForStack jvmTestForStack = new JvmTestForStack();
jvmTestForStack.count();
}
}
再來張圖:

代碼和圖呢, 它們是一伙兒的, 把它們結(jié)合起來仔細看看, 我想你應(yīng)該已經(jīng)理解的差不多了, 不過我還是打算再多講講:
可能你聽說過棧這種數(shù)據(jù)接口, 先入后出嘛, Jvm內(nèi)存中的棧也是先入后出的. 之前說過棧是每個線程都單獨具備的, 意思是, 每個線程都會分配到一小塊棧內(nèi)存空間, 如上圖. 說到這里, 又不得不說棧幀, 什么是棧幀呢? 每個方法被調(diào)用時, 就會壓入一小塊兒內(nèi)存空間到該線程的棧內(nèi)存中, 這一小塊兒內(nèi)存空間的名字就叫做棧幀. 這個壓入的動作就像往彈夾中壓入子彈一樣, 先壓入的最后擊發(fā). 根據(jù)上述代碼, main()方法是最先入棧的, 所以它的棧幀在棧底, 隨后是count()方法, 當count()方法執(zhí)行完畢, 分配給它的棧幀會立馬銷毀, 這個動作叫做出棧.
結(jié)合上述, 圖中的大部分內(nèi)容我相信你已經(jīng)看懂了, 不過作為總結(jié)...這顯然不夠, 所以我要再寫寫操作數(shù)棧, 以及動態(tài)鏈接
說到這里, 就得提一個java指令: javap, 這是一個用來反編譯class字節(jié)碼的指令, 使用方式: javap -v xxx.class, 下面是使用該指令反編譯上述代碼的部分代碼:
Constant pool:
#1 = Methodref #5.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // jvm/JvmTestForStack
#3 = Methodref #2.#27 // jvm/JvmTestForStack."<init>":()V
#4 = Methodref #2.#29 // jvm/JvmTestForStack.count:()I
#5 = Class #30 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Ljvm/JvmTestForStack;
#13 = Utf8 count
#14 = Utf8 ()I
#15 = Utf8 a
#16 = Utf8 I
#17 = Utf8 b
#18 = Utf8 c
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 jvmTestForStack
#24 = Utf8 MethodParameters
#25 = Utf8 SourceFile
#26 = Utf8 JvmTestForStack.java
#27 = NameAndType #6:#7 // "<init>":()V
#28 = Utf8 jvm/JvmTestForStack
#29 = NameAndType #13:#14 // count:()I
#30 = Utf8 java/lang/Object
這個被稱之為常量池, 可以看處, 它似乎把我們的java代碼分解成了一個一個的符號, 比如#19的main符號, 以及#7的()V符號. 這些符號是存放在方法區(qū)中的, 再看下方反編譯后的代碼:
public int count();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: iload_3
9: ireturn
這就是count()方法反編譯后的代碼, 我們從 0: iconst_1 這行代碼開始看, 結(jié)合上方的棧內(nèi)存圖, 就可以看出count()方法在底層究竟是怎么做的.
有以上鋪墊, 現(xiàn)在可以詳細說說了:
動態(tài)鏈接: 認真觀察一下上面的常量池反編譯代碼的 #4 = Methodref #2.#29 這一行, 這個#2.#29是什么意思呢? 這個就是符號鏈接, #2鏈接著#28, #28 = jvm/JvmTestForStack; #29鏈接著#13和#14, #13 = count, #14 = ()I; 那么鏈接起來, 就變成了 jvm/JvmTestForStack.count()I 這行代碼, 之前說過, jvm執(zhí)行到這行代碼仍然不知道count()方法具體有哪些字節(jié)碼呀, 所以底層執(zhí)行時, 還需要轉(zhuǎn)換一次鏈接, 也就是把這些個符號鏈接, 轉(zhuǎn)換為直接鏈接, 讓jvm知道走到這行代碼時, 應(yīng)該去內(nèi)存中的哪個位置拿到可執(zhí)行的jvm字節(jié)碼.
操作數(shù)棧: 經(jīng)過上面說的動態(tài)鏈接, jvm字節(jié)碼執(zhí)行引擎找到了count()方法具體的字節(jié)碼, 這些字節(jié)碼jvm可以看懂, 并會根據(jù)規(guī)則再次轉(zhuǎn)成計算機可以看懂的匯編語言, 然后執(zhí)行. 而我們的計算機它只認識0和1, 所以就有了上面棧內(nèi)存圖中的...將操作數(shù)壓棧出棧運算等操作.
再次強調(diào): 對于基本數(shù)據(jù)類型, 也就是int, double, boolean等類型, 它們的值是直接存放在棧中的. 而對于對象, 大多數(shù)時候, 棧中存放的僅僅是一個引用, 真正的值存放在堆中.
四. 細說Jvm堆
仍然是先上圖:

我們的堆內(nèi)存區(qū)域被劃分成了兩段: 分別是年輕代和老年代, 年輕代占整個堆內(nèi)存區(qū)域的1/3, 老年代占2/3. 年輕代又別劃分為Eden區(qū)和幸存區(qū)(Survivor區(qū)), Eden區(qū)占年輕代的8/10, 兩個Survivor各占1/10;
一般情況下, 新new的對象會被放到Eden區(qū), Eden區(qū)放滿則執(zhí)行minor gc, 進行垃圾回收, 那什么樣的對象會被視為垃圾對象呢? 其實就是沒有任何引用的對象, 無法再通過任何變量訪問的對象. 整個過程呢, 圖中已經(jīng)畫的很清晰了, 不過多解釋了. 需要注意的是, 除了圖中提到的對象動態(tài)年齡判斷機制, 還有很多種情況會將對象移動到老年代, 會在下一篇文章中細說. 當老年代被放滿, 則會觸發(fā)full gc.
full gc為什么那么慢?
在進行full gc的時候, Jvm會執(zhí)行一個STW的機制, 全稱stop the world. 停止所有用戶線程, 進行full gc. 為什么要有stw機制? gc的算法有很多種, 但都需要標記出哪些是垃圾對象或非垃圾對象, 所以我的猜測是, 如果一邊進行垃圾回收, 一邊又有新的垃圾對象產(chǎn)生...就像遍歷一個集合時, 又有其它線程不停的在新增或者修改集合中的值, 那么將會產(chǎn)生很嚴重的后果, 可能永遠都遍歷不完, 也可能發(fā)生線程安全問題等等等等.
五. Jvm內(nèi)存參數(shù)配置
一一一然是先上圖:

上圖即為Jvm中各個區(qū)域的內(nèi)存配置參數(shù), 上圖中的值僅是示例值, 具體大小請根據(jù)自身系統(tǒng)的業(yè)務(wù)情況來定, 不知道該配置多大的同學(xué)也可以不配置, java默認的大小已經(jīng)足夠大多數(shù)系統(tǒng)使用了, 想要了解或?qū)W習(xí)如何合理的配置Jvm內(nèi)存各個區(qū)域的大小, 請期待在下的下一篇文章.
完整的參數(shù)示例: java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar test.jar
此外還有一個需要注意的地方, 相信大家注意到我反復(fù)的再提方法區(qū)使用的是直接內(nèi)存. 方法區(qū)的默認大小是21M, 當放滿之后, 會執(zhí)行full gc. 然后根據(jù)gc的結(jié)果, 動態(tài)的水平擴縮容該區(qū)域的大小, 如果gc釋放的量較大, 則縮小該空間; 如果釋放的量較小, 則放大該空間(不會大于MaxMetaspaceSize設(shè)置的值); 如果沒有設(shè)置該值, 則沒有限制, 且程序啟動時, 就會執(zhí)行好幾次full gc
對了, 根據(jù)本文所述, 所以模擬棧溢出, 無限遞歸調(diào)用方法即可, 模擬堆溢出, 弄個集合不停new對象即可, 嘿嘿...^^
今天的總結(jié)和分享就到這里, 如果有說的不對的地方, 還請大家不吝賜教.