2萬(wàn)字長(zhǎng)文包教包會(huì) JVM 內(nèi)存結(jié)構(gòu)

JVM ?≠ Japanese Video's Man
寫這篇的主要原因呢,就是為了能在簡(jiǎn)歷上寫個(gè)“熟悉JVM底層結(jié)構(gòu)”,另一個(gè)原因就是能讓讀我文章的大家也寫上這句話,真是個(gè)助人為樂(lè)的帥小伙。。。。嗯,不單單只是面向面試學(xué)習(xí)哈,更重要的是構(gòu)建自己的JVM 知識(shí)體系,Javaer 們技術(shù)棧要有廣度,但是 JVM 的掌握必須有深度
點(diǎn)贊+收藏 就學(xué)會(huì)系列,文章收錄在 GitHub JavaKeeper ,N線互聯(lián)網(wǎng)開發(fā)必備技能兵器譜
直擊面試
反正我是帶著這些問(wèn)題往下讀的
- 說(shuō)一下 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)吧,都有哪些區(qū)?分別是干什么的?
- Java 8 的內(nèi)存分代改進(jìn)
- 舉例棧溢出的情況?
- 調(diào)整棧大小,就能保存不出現(xiàn)溢出嗎?
- 分配的棧內(nèi)存越大越好嗎?
- 垃圾回收是否會(huì)涉及到虛擬機(jī)棧?
- 方法中定義的局部變量是否線程安全?
內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和 CPU 的中間倉(cāng)庫(kù)及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行。JVM 內(nèi)存布局規(guī)定了 Java 在運(yùn)行過(guò)程中內(nèi)存申請(qǐng)、分配、管理的策略,保證了 JVM 的高效穩(wěn)定運(yùn)行。不同的 JVM 對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異。
下圖是 JVM 整體架構(gòu),中間部分就是 Java 虛擬機(jī)定義的各種運(yùn)行時(shí)數(shù)據(jù)區(qū)域。

jvm-framework
Java 虛擬機(jī)定義了若干種程序運(yùn)行期間會(huì)使用到的運(yùn)行時(shí)數(shù)據(jù)區(qū),其中有一些會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出而銷毀。另外一些則是與線程一一對(duì)應(yīng)的,這些與線程一一對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開始和結(jié)束而創(chuàng)建和銷毀。
- 線程私有:程序計(jì)數(shù)器、棧、本地棧
- 線程共享:堆、堆外內(nèi)存(永久代或元空間、代碼緩存)
下面我們就來(lái)一一解毒下這些內(nèi)存區(qū)域,先從最簡(jiǎn)單的入手
一、程序計(jì)數(shù)器
程序計(jì)數(shù)寄存器(Program Counter Register),Register 的命名源于 CPU 的寄存器,寄存器存儲(chǔ)指令相關(guān)的線程信息,CPU 只有把數(shù)據(jù)裝載到寄存器才能夠運(yùn)行。
這里,并非是廣義上所指的物理寄存器,叫程序計(jì)數(shù)器(或PC計(jì)數(shù)器或指令計(jì)數(shù)器)會(huì)更加貼切,并且也不容易引起一些不必要的誤會(huì)。JVM 中的 PC 寄存器是對(duì)物理 PC 寄存器的一種抽象模擬。
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
1.1 作用
PC 寄存器用來(lái)存儲(chǔ)指向下一條指令的地址,即將要執(zhí)行的指令代碼。由執(zhí)行引擎讀取下一條指令。

jvm-pc-counter
(分析:進(jìn)入class文件所在目錄,執(zhí)行javap -v xx.class反解析(或者通過(guò)IDEA插件Jclasslib直接查看,上圖),可以看到當(dāng)前類對(duì)應(yīng)的Code區(qū)(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等信息。)
1.2 概述
- 它是一塊很小的內(nèi)存空間,幾乎可以忽略不計(jì)。也是運(yùn)行速度最快的存儲(chǔ)區(qū)域
- 在 JVM 規(guī)范中,每個(gè)線程都有它自己的程序計(jì)數(shù)器,是線程私有的,生命周期與線程的生命周期一致
- 任何時(shí)間一個(gè)線程都只有一個(gè)方法在執(zhí)行,也就是所謂的當(dāng)前方法。如果當(dāng)前線程正在執(zhí)行的是 Java 方法,程序計(jì)數(shù)器記錄的是 JVM 字節(jié)碼指令地址,如果是執(zhí)行 natice 方法,則是未指定值(undefined)
- 它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成
- 字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令
- 它是唯一一個(gè)在 JVM 規(guī)范中沒(méi)有規(guī)定任何
OutOfMemoryError情況的區(qū)域
???:使用PC寄存器存儲(chǔ)字節(jié)碼指令地址有什么用呢?為什么使用PC寄存器記錄當(dāng)前線程的執(zhí)行地址呢?
??♂?:因?yàn)镃PU需要不停的切換各個(gè)線程,這時(shí)候切換回來(lái)以后,就得知道接著從哪開始繼續(xù)執(zhí)行。JVM的字節(jié)碼解釋器就需要通過(guò)改變PC寄存器的值來(lái)明確下一條應(yīng)該執(zhí)行什么樣的字節(jié)碼指令。
???:PC寄存器為什么會(huì)被設(shè)定為線程私有的?
??♂?:多線程在一個(gè)特定的時(shí)間段內(nèi)只會(huì)執(zhí)行其中某一個(gè)線程方法,CPU會(huì)不停的做任務(wù)切換,這樣必然會(huì)導(dǎo)致經(jīng)常中斷或恢復(fù)。為了能夠準(zhǔn)確的記錄各個(gè)線程正在執(zhí)行的當(dāng)前字節(jié)碼指令地址,所以為每個(gè)線程都分配了一個(gè)PC寄存器,每個(gè)線程都獨(dú)立計(jì)算,不會(huì)互相影響。
二、虛擬機(jī)棧
2.1 概述
Java 虛擬機(jī)棧(Java Virtual Machine Stacks),早期也叫 Java 棧。每個(gè)線程在創(chuàng)建的時(shí)候都會(huì)創(chuàng)建一個(gè)虛擬機(jī)棧,其內(nèi)部保存一個(gè)個(gè)的棧幀(Stack Frame),對(duì)應(yīng)著一次次 Java 方法調(diào)用,是線程私有的,生命周期和線程一致。
作用:主管 Java 程序的運(yùn)行,它保存方法的局部變量、部分結(jié)果,并參與方法的調(diào)用和返回。
特點(diǎn):
- 棧是一種快速有效的分配存儲(chǔ)方式,訪問(wèn)速度僅次于程序計(jì)數(shù)器
- JVM 直接對(duì)虛擬機(jī)棧的操作只有兩個(gè):每個(gè)方法執(zhí)行,伴隨著入棧(進(jìn)棧/壓棧),方法執(zhí)行結(jié)束出棧
- 棧不存在垃圾回收問(wèn)題
棧中可能出現(xiàn)的異常:
Java 虛擬機(jī)規(guī)范允許 Java虛擬機(jī)棧的大小是動(dòng)態(tài)的或者是固定不變的
- 如果采用固定大小的 Java 虛擬機(jī)棧,那每個(gè)線程的 Java 虛擬機(jī)棧容量可以在線程創(chuàng)建的時(shí)候獨(dú)立選定。如果線程請(qǐng)求分配的棧容量超過(guò) Java 虛擬機(jī)棧允許的最大容量,Java 虛擬機(jī)將會(huì)拋出一個(gè) StackOverflowError 異常
- 如果 Java 虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,并且在嘗試擴(kuò)展的時(shí)候無(wú)法申請(qǐng)到足夠的內(nèi)存,或者在創(chuàng)建新的線程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧,那 Java 虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常
可以通過(guò)參數(shù)-Xss來(lái)設(shè)置線程的最大棧空間,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度。
官方提供的參考工具,可查一些參數(shù)和操作:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BGBCIEFC
2.2 棧的存儲(chǔ)單位
棧中存儲(chǔ)什么?
- 每個(gè)線程都有自己的棧,棧中的數(shù)據(jù)都是以棧幀(Stack Frame)的格式存在
- 在這個(gè)線程上正在執(zhí)行的每個(gè)方法都各自有對(duì)應(yīng)的一個(gè)棧幀
- 棧幀是一個(gè)內(nèi)存區(qū)塊,是一個(gè)數(shù)據(jù)集,維系著方法執(zhí)行過(guò)程中的各種數(shù)據(jù)信息
2.3 棧運(yùn)行原理
JVM 直接對(duì) Java 棧的操作只有兩個(gè),對(duì)棧幀的壓棧和出棧,遵循“先進(jìn)后出/后進(jìn)先出”原則
在一條活動(dòng)線程中,一個(gè)時(shí)間點(diǎn)上,只會(huì)有一個(gè)活動(dòng)的棧幀。即只有當(dāng)前正在執(zhí)行的方法的棧幀(棧頂棧幀)是有效的,這個(gè)棧幀被稱為當(dāng)前棧幀(Current Frame),與當(dāng)前棧幀對(duì)應(yīng)的方法就是當(dāng)前方法(Current Method),定義這個(gè)方法的類就是當(dāng)前類(Current Class)
執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令只針對(duì)當(dāng)前棧幀進(jìn)行操作
如果在該方法中調(diào)用了其他方法,對(duì)應(yīng)的新的棧幀會(huì)被創(chuàng)建出來(lái),放在棧的頂端,稱為新的當(dāng)前棧幀
不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個(gè)棧幀中引用另外一個(gè)線程的棧幀
如果當(dāng)前方法調(diào)用了其他方法,方法返回之際,當(dāng)前棧幀會(huì)傳回此方法的執(zhí)行結(jié)果給前一個(gè)棧幀,接著,虛擬機(jī)會(huì)丟棄當(dāng)前棧幀,使得前一個(gè)棧幀重新成為當(dāng)前棧幀
Java 方法有兩種返回函數(shù)的方式,一種是正常的函數(shù)返回,使用 return 指令,另一種是拋出異常,不管用哪種方式,都會(huì)導(dǎo)致棧幀被彈出
IDEA 在 debug 時(shí)候,可以在 debug 窗口看到 Frames 中各種方法的壓棧和出棧情況

2.4 棧幀的內(nèi)部結(jié)構(gòu)
每個(gè)**棧幀(Stack Frame)**中存儲(chǔ)著:
- 局部變量表(Local Variables)
- 操作數(shù)棧(Operand Stack)(或稱為表達(dá)式棧)
- 動(dòng)態(tài)鏈接(Dynamic Linking):指向運(yùn)行時(shí)常量池的方法引用
- 方法返回地址(Return Address):方法正常退出或異常退出的地址
- 一些附加信息

jvm-stack-frame
繼續(xù)深拋棧幀中的五部分~~
2.4.1. 局部變量表
- 局部變量表也被稱為局部變量數(shù)組或者本地變量表
- 是一組變量值存儲(chǔ)空間,主要用于存儲(chǔ)方法參數(shù)和定義在方法體內(nèi)的局部變量,包括編譯器可知的各種 Java 虛擬機(jī)基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它并不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此相關(guān)的位置)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址,已被異常表取代)
- 由于局部變量表是建立在線程的棧上,是線程的私有數(shù)據(jù),因此不存在數(shù)據(jù)安全問(wèn)題
- 局部變量表所需要的容量大小是編譯期確定下來(lái)的,并保存在方法的 Code 屬性的
maximum local variables數(shù)據(jù)項(xiàng)中。在方法運(yùn)行期間是不會(huì)改變局部變量表的大小的 - 方法嵌套調(diào)用的次數(shù)由棧的大小決定。一般來(lái)說(shuō),棧越大,方法嵌套調(diào)用次數(shù)越多。對(duì)一個(gè)函數(shù)而言,它的參數(shù)和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調(diào)用所需傳遞的信息增大的需求。進(jìn)而函數(shù)調(diào)用就會(huì)占用更多的棧空間,導(dǎo)致其嵌套調(diào)用次數(shù)就會(huì)減少。
- 局部變量表中的變量只在當(dāng)前方法調(diào)用中有效。在方法執(zhí)行時(shí),虛擬機(jī)通過(guò)使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過(guò)程。當(dāng)方法調(diào)用結(jié)束后,隨著方法棧幀的銷毀,局部變量表也會(huì)隨之銷毀。
- 參數(shù)值的存放總是在局部變量數(shù)組的 index0 開始,到數(shù)組長(zhǎng)度 -1 的索引結(jié)束
槽 Slot
局部變量表最基本的存儲(chǔ)單元是Slot(變量槽)
在局部變量表中,32位以內(nèi)的類型只占用一個(gè)Slot(包括returnAddress類型),64位的類型(long和double)占用兩個(gè)連續(xù)的 Slot
- byte、short、char 在存儲(chǔ)前被轉(zhuǎn)換為int,boolean也被轉(zhuǎn)換為int,0 表示 false,非 0 表示 true
- long 和 double 則占據(jù)兩個(gè) Slot
JVM 會(huì)為局部變量表中的每一個(gè) Slot 都分配一個(gè)訪問(wèn)索引,通過(guò)這個(gè)索引即可成功訪問(wèn)到局部變量表中指定的局部變量值,索引值的范圍從 0 開始到局部變量表最大的 Slot 數(shù)量
當(dāng)一個(gè)實(shí)例方法被調(diào)用的時(shí)候,它的方法參數(shù)和方法體內(nèi)部定義的局部變量將會(huì)按照順序被復(fù)制到局部變量表中的每一個(gè) Slot 上
如果需要訪問(wèn)局部變量表中一個(gè)64bit的局部變量值時(shí),只需要使用前一個(gè)索引即可。(比如:訪問(wèn) long 或double 類型變量,不允許采用任何方式單獨(dú)訪問(wèn)其中的某一個(gè) Slot)
如果當(dāng)前幀是由構(gòu)造方法或?qū)嵗椒▌?chuàng)建的,那么該對(duì)象引用 this 將會(huì)存放在 index 為 0 的 Slot 處,其余的參數(shù)按照參數(shù)表順序繼續(xù)排列(這里就引出一個(gè)問(wèn)題:靜態(tài)方法中為什么不可以引用 this,就是因?yàn)閠his 變量不存在于當(dāng)前方法的局部變量表中)
棧幀中的局部變量表中的槽位是可以重用的,如果一個(gè)局部變量過(guò)了其作用域,那么在其作用域之后申明的新的局部變量就很有可能會(huì)復(fù)用過(guò)期局部變量的槽位,從而達(dá)到節(jié)省資源的目的。(下圖中,this、a、b、c 理論上應(yīng)該有 4 個(gè)變量,c 復(fù)用了 b 的槽)

- 在棧幀中,與性能調(diào)優(yōu)關(guān)系最為密切的就是局部變量表。在方法執(zhí)行時(shí),虛擬機(jī)使用局部變量表完成方法的傳遞
- 局部變量表中的變量也是重要的垃圾回收根節(jié)點(diǎn),只要被局部變量表中直接或間接引用的對(duì)象都不會(huì)被回收
2.4.2. 操作數(shù)棧
每個(gè)獨(dú)立的棧幀中除了包含局部變量表之外,還包含一個(gè)后進(jìn)先出(Last-In-First-Out)的操作數(shù)棧,也可以稱為表達(dá)式棧(Expression Stack)
操作數(shù)棧,在方法執(zhí)行過(guò)程中,根據(jù)字節(jié)碼指令,往操作數(shù)棧中寫入數(shù)據(jù)或提取數(shù)據(jù),即入棧(push)、出棧(pop)
某些字節(jié)碼指令將值壓入操作數(shù)棧,其余的字節(jié)碼指令將操作數(shù)取出棧。使用它們后再把結(jié)果壓入棧。比如,執(zhí)行復(fù)制、交換、求和等操作
概述
- 操作數(shù)棧,主要用于保存計(jì)算過(guò)程的中間結(jié)果,同時(shí)作為計(jì)算過(guò)程中變量臨時(shí)的存儲(chǔ)空間
- 操作數(shù)棧就是 JVM 執(zhí)行引擎的一個(gè)工作區(qū),當(dāng)一個(gè)方法剛開始執(zhí)行的時(shí)候,一個(gè)新的棧幀也會(huì)隨之被創(chuàng)建出來(lái),此時(shí)這個(gè)方法的操作數(shù)棧是空的
- 每一個(gè)操作數(shù)棧都會(huì)擁有一個(gè)明確的棧深度用于存儲(chǔ)數(shù)值,其所需的最大深度在編譯期就定義好了,保存在方法的 Code 屬性的
max_stack數(shù)據(jù)項(xiàng)中 - 棧中的任何一個(gè)元素都可以是任意的 Java 數(shù)據(jù)類型
- 32bit 的類型占用一個(gè)棧單位深度
- 64bit 的類型占用兩個(gè)棧單位深度
- 操作數(shù)棧并非采用訪問(wèn)索引的方式來(lái)進(jìn)行數(shù)據(jù)訪問(wèn)的,而是只能通過(guò)標(biāo)準(zhǔn)的入棧和出棧操作來(lái)完成一次數(shù)據(jù)訪問(wèn)
- 如果被調(diào)用的方法帶有返回值的話,其返回值將會(huì)被壓入當(dāng)前棧幀的操作數(shù)棧中,并更新PC寄存器中下一條需要執(zhí)行的字節(jié)碼指令
- 操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配,這由編譯器在編譯期間進(jìn)行驗(yàn)證,同時(shí)在類加載過(guò)程中的類檢驗(yàn)階段的數(shù)據(jù)流分析階段要再次驗(yàn)證
- 另外,我們說(shuō)Java虛擬機(jī)的解釋引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作數(shù)棧
棧頂緩存(Top-of-stack-Cashing)
HotSpot 的執(zhí)行引擎采用的并非是基于寄存器的架構(gòu),但這并不代表 HotSpot VM 的實(shí)現(xiàn)并沒(méi)有間接利用到寄存器資源。寄存器是物理 CPU 中的組成部分之一,它同時(shí)也是 CPU 中非常重要的高速存儲(chǔ)資源。一般來(lái)說(shuō),寄存器的讀/寫速度非常迅速,甚至可以比內(nèi)存的讀/寫速度快上幾十倍不止,不過(guò)寄存器資源卻非常有限,不同平臺(tái)下的CPU 寄存器數(shù)量是不同和不規(guī)律的。寄存器主要用于緩存本地機(jī)器指令、數(shù)值和下一條需要被執(zhí)行的指令地址等數(shù)據(jù)。
基于棧式架構(gòu)的虛擬機(jī)所使用的零地址指令更加緊湊,但完成一項(xiàng)操作的時(shí)候必然需要使用更多的入棧和出棧指令,這同時(shí)也就意味著將需要更多的指令分派(instruction dispatch)次數(shù)和內(nèi)存讀/寫次數(shù)。由于操作數(shù)是存儲(chǔ)在內(nèi)存中的,因此頻繁的執(zhí)行內(nèi)存讀/寫操作必然會(huì)影響執(zhí)行速度。為了解決這個(gè)問(wèn)題,HotSpot JVM設(shè)計(jì)者們提出了棧頂緩存技術(shù),將棧頂元素全部緩存在物理 CPU 的寄存器中,以此降低對(duì)內(nèi)存的讀/寫次數(shù),提升執(zhí)行引擎的執(zhí)行效率
2.4.3. 動(dòng)態(tài)鏈接(指向運(yùn)行時(shí)常量池的方法引用)
- 每一個(gè)棧幀內(nèi)部都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用。包含這個(gè)引用的目的就是為了支持當(dāng)前方法的代碼能夠?qū)崿F(xiàn)動(dòng)態(tài)鏈接(Dynamic Linking)。
- 在 Java 源文件被編譯到字節(jié)碼文件中時(shí),所有的變量和方法引用都作為符號(hào)引用(Symbolic Reference)保存在 Class 文件的常量池中。比如:描述一個(gè)方法調(diào)用了另外的其他方法時(shí),就是通過(guò)常量池中指向方法的符號(hào)引用來(lái)表示的,那么動(dòng)態(tài)鏈接的作用就是為了將這些符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用

jvm-dynamic-linking
JVM 是如何執(zhí)行方法調(diào)用的
方法調(diào)用不同于方法執(zhí)行,方法調(diào)用階段的唯一任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),暫時(shí)還不涉及方法內(nèi)部的具體運(yùn)行過(guò)程。Class 文件的編譯過(guò)程中不包括傳統(tǒng)編譯器中的連接步驟,一切方法調(diào)用在 Class文件里面存儲(chǔ)的都是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(直接引用)。也就是需要在類加載階段,甚至到運(yùn)行期才能確定目標(biāo)方法的直接引用。
【這一塊內(nèi)容,除了方法調(diào)用,還包括解析、分派(靜態(tài)分派、動(dòng)態(tài)分派、單分派與多分派),這里先不介紹,后續(xù)再挖】
在 JVM 中,將符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用與方法的綁定機(jī)制有關(guān)
- 靜態(tài)鏈接:當(dāng)一個(gè)字節(jié)碼文件被裝載進(jìn) JVM 內(nèi)部時(shí),如果被調(diào)用的目標(biāo)方法在編譯期可知,且運(yùn)行期保持不變時(shí)。這種情況下將調(diào)用方法的符號(hào)引用轉(zhuǎn)換為直接引用的過(guò)程稱之為靜態(tài)鏈接
- 動(dòng)態(tài)鏈接:如果被調(diào)用的方法在編譯期無(wú)法被確定下來(lái),也就是說(shuō),只能在程序運(yùn)行期將調(diào)用方法的符號(hào)引用轉(zhuǎn)換為直接引用,由于這種引用轉(zhuǎn)換過(guò)程具備動(dòng)態(tài)性,因此也就被稱之為動(dòng)態(tài)鏈接
對(duì)應(yīng)的方法的綁定機(jī)制為:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個(gè)字段、方法或者類在符號(hào)引用被替換為直接引用的過(guò)程,這僅僅發(fā)生一次。
- 早期綁定:早期綁定就是指被調(diào)用的目標(biāo)方法如果在編譯期可知,且運(yùn)行期保持不變時(shí),即可將這個(gè)方法與所屬的類型進(jìn)行綁定,這樣一來(lái),由于明確了被調(diào)用的目標(biāo)方法究竟是哪一個(gè),因此也就可以使用靜態(tài)鏈接的方式將符號(hào)引用轉(zhuǎn)換為直接引用。
- 晚期綁定:如果被調(diào)用的方法在編譯器無(wú)法被確定下來(lái),只能夠在程序運(yùn)行期根據(jù)實(shí)際的類型綁定相關(guān)的方法,這種綁定方式就被稱為晚期綁定。
虛方法和非虛方法
- 如果方法在編譯器就確定了具體的調(diào)用版本,這個(gè)版本在運(yùn)行時(shí)是不可變的。這樣的方法稱為非虛方法,比如靜態(tài)方法、私有方法、final方法、實(shí)例構(gòu)造器、父類方法都是非虛方法
- 其他方法稱為虛方法
虛方法表
在面向?qū)ο缶幊讨校瑫?huì)頻繁的使用到動(dòng)態(tài)分派,如果每次動(dòng)態(tài)分派都要重新在類的方法元數(shù)據(jù)中搜索合適的目標(biāo)有可能會(huì)影響到執(zhí)行效率。為了提高性能,JVM 采用在類的方法區(qū)建立一個(gè)虛方法表(virtual method table),使用索引表來(lái)代替查找。非虛方法不會(huì)出現(xiàn)在表中。
每個(gè)類中都有一個(gè)虛方法表,表中存放著各個(gè)方法的實(shí)際入口。
虛方法表會(huì)在類加載的連接階段被創(chuàng)建并開始初始化,類的變量初始值準(zhǔn)備完成之后,JVM 會(huì)把該類的方法表也初始化完畢。
2.4.4. 方法返回地址(return address)
用來(lái)存放調(diào)用該方法的 PC 寄存器的值。
一個(gè)方法的結(jié)束,有兩種方式
- 正常執(zhí)行完成
- 出現(xiàn)未處理的異常,非正常退出
無(wú)論通過(guò)哪種方式退出,在方法退出后都返回到該方法被調(diào)用的位置。方法正常退出時(shí),調(diào)用者的 PC 計(jì)數(shù)器的值作為返回地址,即調(diào)用該方法的指令的下一條指令的地址。而通過(guò)異常退出的,返回地址是要通過(guò)異常表來(lái)確定的,棧幀中一般不會(huì)保存這部分信息。
當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式可以退出這個(gè)方法:
執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,會(huì)有返回值傳遞給上層的方法調(diào)用者,簡(jiǎn)稱正常完成出口
一個(gè)方法的正常調(diào)用完成之后究竟需要使用哪一個(gè)返回指令還需要根據(jù)方法返回值的實(shí)際數(shù)據(jù)類型而定
在字節(jié)碼指令中,返回指令包含 ireturn(當(dāng)返回值是boolean、byte、char、short和int類型時(shí)使用)、lreturn、freturn、dreturn以及areturn,另外還有一個(gè) return 指令供聲明為 void 的方法、實(shí)例初始化方法、類和接口的初始化方法使用。
在方法執(zhí)行的過(guò)程中遇到了異常,并且這個(gè)異常沒(méi)有在方法內(nèi)進(jìn)行處理,也就是只要在本方法的異常表中沒(méi)有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出。簡(jiǎn)稱異常完成出口
方法執(zhí)行過(guò)程中拋出異常時(shí)的異常處理,存儲(chǔ)在一個(gè)異常處理表,方便在發(fā)生異常的時(shí)候找到處理異常的代碼。
本質(zhì)上,方法的退出就是當(dāng)前棧幀出棧的過(guò)程。此時(shí),需要恢復(fù)上層方法的局部變量表、操作數(shù)棧、將返回值壓入調(diào)用者棧幀的操作數(shù)棧、設(shè)置PC寄存器值等,讓調(diào)用者方法繼續(xù)執(zhí)行下去。
正常完成出口和異常完成出口的區(qū)別在于:通過(guò)異常完成出口退出的不會(huì)給他的上層調(diào)用者產(chǎn)生任何的返回值
2.4.5. 附加信息
棧幀中還允許攜帶與 Java 虛擬機(jī)實(shí)現(xiàn)相關(guān)的一些附加信息。例如,對(duì)程序調(diào)試提供支持的信息,但這些信息取決于具體的虛擬機(jī)實(shí)現(xiàn)。
三、本地方法棧
3.1 本地方法接口
簡(jiǎn)單的講,一個(gè) Native Method 就是一個(gè) Java 調(diào)用非 Java 代碼的接口。我們知道的 Unsafe 類就有很多本地方法。
為什么要使用本地方法(Native Method)?
Java 使用起來(lái)非常方便,然而有些層次的任務(wù)用 Java 實(shí)現(xiàn)起來(lái)也不容易,或者我們對(duì)程序的效率很在意時(shí),問(wèn)題就來(lái)了
- 與 Java 環(huán)境外交互:有時(shí) Java 應(yīng)用需要與 Java 外面的環(huán)境交互,這就是本地方法存在的原因。
- 與操作系統(tǒng)交互:JVM 支持 Java 語(yǔ)言本身和運(yùn)行時(shí)庫(kù),但是有時(shí)仍需要依賴一些底層系統(tǒng)的支持。通過(guò)本地方法,我們可以實(shí)現(xiàn)用 Java 與實(shí)現(xiàn)了 jre 的底層系統(tǒng)交互, JVM 的一些部分就是 C 語(yǔ)言寫的。
- Sun's Java:Sun的解釋器就是C實(shí)現(xiàn)的,這使得它能像一些普通的C一樣與外部交互。jre大部分都是用 Java 實(shí)現(xiàn)的,它也通過(guò)一些本地方法與外界交互。比如,類
java.lang.Thread的setPriority()的方法是用Java 實(shí)現(xiàn)的,但它實(shí)現(xiàn)調(diào)用的是該類的本地方法setPrioruty(),該方法是C實(shí)現(xiàn)的,并被植入 JVM 內(nèi)部。
3.2 本地方法棧(Native Method Stack)
Java 虛擬機(jī)棧用于管理 Java 方法的調(diào)用,而本地方法棧用于管理本地方法的調(diào)用
本地方法棧也是線程私有的
允許線程固定或者可動(dòng)態(tài)擴(kuò)展的內(nèi)存大小
- 如果線程請(qǐng)求分配的棧容量超過(guò)本地方法棧允許的最大容量,Java 虛擬機(jī)將會(huì)拋出一個(gè)
StackOverflowError異常 - 如果本地方法??梢詣?dòng)態(tài)擴(kuò)展,并且在嘗試擴(kuò)展的時(shí)候無(wú)法申請(qǐng)到足夠的內(nèi)存,或者在創(chuàng)建新的線程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的本地方法棧,那么 Java虛擬機(jī)將會(huì)拋出一個(gè)
OutofMemoryError異常
- 如果線程請(qǐng)求分配的棧容量超過(guò)本地方法棧允許的最大容量,Java 虛擬機(jī)將會(huì)拋出一個(gè)
本地方法是使用C語(yǔ)言實(shí)現(xiàn)的
它的具體做法是
Mative Method Stack中登記native方法,在Execution Engine執(zhí)行時(shí)加載本地方法庫(kù)當(dāng)某個(gè)線程調(diào)用一個(gè)本地方法時(shí),它就進(jìn)入了一個(gè)全新的并且不再受虛擬機(jī)限制的世界。它和虛擬機(jī)擁有同樣的權(quán)限。本地方法可以通過(guò)本地方法接口來(lái)訪問(wèn)虛擬機(jī)內(nèi)部的運(yùn)行時(shí)數(shù)據(jù)區(qū),它甚至可以直接使用本地處理器中的寄存器,直接從本地內(nèi)存的堆中分配任意數(shù)量的內(nèi)存
并不是所有 JVM 都支持本地方法。因?yàn)?Java 虛擬機(jī)規(guī)范并沒(méi)有明確要求本地方法棧的使用語(yǔ)言、具體實(shí)現(xiàn)方式、數(shù)據(jù)結(jié)構(gòu)等。如果 JVM 產(chǎn)品不打算支持 native 方法,也可以無(wú)需實(shí)現(xiàn)本地方法棧
在 Hotspot JVM 中,直接將本地方棧和虛擬機(jī)棧合二為一
棧是運(yùn)行時(shí)的單位,而堆是存儲(chǔ)的單位。
棧解決程序的運(yùn)行問(wèn)題,即程序如何執(zhí)行,或者說(shuō)如何處理數(shù)據(jù)。堆解決的是數(shù)據(jù)存儲(chǔ)的問(wèn)題,即數(shù)據(jù)怎么放、放在哪。
四、堆內(nèi)存
4.1 內(nèi)存劃分
對(duì)于大多數(shù)應(yīng)用,Java 堆是 Java 虛擬機(jī)管理的內(nèi)存中最大的一塊,被所有線程共享。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)據(jù)都在這里分配內(nèi)存。
為了進(jìn)行高效的垃圾回收,虛擬機(jī)把堆內(nèi)存邏輯上劃分成三塊區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):
- 新生帶(年輕代):新對(duì)象和沒(méi)達(dá)到一定年齡的對(duì)象都在新生代
- 老年代(養(yǎng)老區(qū)):被長(zhǎng)時(shí)間使用的對(duì)象,老年代的內(nèi)存空間應(yīng)該要比年輕代更大
- 元空間(JDK1.8之前叫永久代):像一些方法中的操作臨時(shí)對(duì)象等,JDK1.8之前是占用JVM內(nèi)存,JDK1.8之后直接使用物理內(nèi)存

JDK7
Java 虛擬機(jī)規(guī)范規(guī)定,Java 堆可以是處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,像磁盤空間一樣。實(shí)現(xiàn)時(shí),既可以是固定大小,也可以是可擴(kuò)展的,主流虛擬機(jī)都是可擴(kuò)展的(通過(guò) -Xmx 和 -Xms 控制),如果堆中沒(méi)有完成實(shí)例分配,并且堆無(wú)法再擴(kuò)展時(shí),就會(huì)拋出 OutOfMemoryError 異常。
年輕代 (Young Generation)
年輕代是所有新對(duì)象創(chuàng)建的地方。當(dāng)填充年輕代時(shí),執(zhí)行垃圾收集。這種垃圾收集稱為Minor GC。年輕一代被分為三個(gè)部分——伊甸園(Eden Memory)和兩個(gè)幸存區(qū)(Survivor Memory,被稱為from/to或s0/s1),默認(rèn)比例是8:1:1
- 大多數(shù)新創(chuàng)建的對(duì)象都位于 Eden 內(nèi)存空間中
- 當(dāng) Eden 空間被對(duì)象填充時(shí),執(zhí)行Minor GC,并將所有幸存者對(duì)象移動(dòng)到一個(gè)幸存者空間中
- Minor GC 檢查幸存者對(duì)象,并將它們移動(dòng)到另一個(gè)幸存者空間。所以每次,一個(gè)幸存者空間總是空的
- 經(jīng)過(guò)多次 GC 循環(huán)后存活下來(lái)的對(duì)象被移動(dòng)到老年代。通常,這是通過(guò)設(shè)置年輕一代對(duì)象的年齡閾值來(lái)實(shí)現(xiàn)的,然后他們才有資格提升到老一代
老年代(Old Generation)
舊的一代內(nèi)存包含那些經(jīng)過(guò)許多輪小型 GC 后仍然存活的對(duì)象。通常,垃圾收集是在老年代內(nèi)存滿時(shí)執(zhí)行的。老年代垃圾收集稱為主GC,通常需要更長(zhǎng)的時(shí)間。
大對(duì)象直接進(jìn)入老年代(大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象)。這樣做的目的是避免在Eden區(qū)和兩個(gè)Survivor區(qū)之間發(fā)生大量的內(nèi)存拷貝

元空間
不管是 JDK8 之前的永久代,還是 JDK8 及以后的元空間,都可以看作是 Java 虛擬機(jī)規(guī)范中方法區(qū)的實(shí)現(xiàn)。
雖然 Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開。
所以元空間放在后邊的方法區(qū)再說(shuō)。
4.2 設(shè)置堆內(nèi)存大小和 OOM
Java 堆用于存儲(chǔ) Java 對(duì)象實(shí)例,那么堆的大小在 JVM 啟動(dòng)的時(shí)候就確定了,我們可以通過(guò) -Xmx 和 -Xms 來(lái)設(shè)定
-Xmx用來(lái)表示堆的起始內(nèi)存,等價(jià)于-XX:InitialHeapSize-Xms用來(lái)表示堆的最大內(nèi)存,等價(jià)于-XX:MaxHeapSize
如果堆的內(nèi)存大小超過(guò) -Xms 設(shè)定的最大內(nèi)存, 就會(huì)拋出 OutOfMemoryError 異常。
我們通常會(huì)將 -Xmx 和 -Xms 兩個(gè)參數(shù)配置為相同的值,其目的是為了能夠在垃圾回收機(jī)制清理完堆區(qū)后不再需要重新分隔計(jì)算堆的大小,從而提高性能
默認(rèn)情況下,初始堆內(nèi)存大小為:電腦內(nèi)存大小/64
默認(rèn)情況下,最大堆內(nèi)存大小為:電腦內(nèi)存大小/4
可以通過(guò)代碼獲取到我們的設(shè)置值,當(dāng)然也可以模擬 OOM:
public?static?void?main(String[]?args)?{
??//返回?JVM?堆大小
??long?initalMemory?=?Runtime.getRuntime().totalMemory()?/?1024?/1024;
??//返回?JVM?堆的最大內(nèi)存
??long?maxMemory?=?Runtime.getRuntime().maxMemory()?/?1024?/1024;
??System.out.println("-Xms?:?"+initalMemory?+?"M");
??System.out.println("-Xmx?:?"+maxMemory?+?"M");
??System.out.println("系統(tǒng)內(nèi)存大小:"?+?initalMemory?*?64?/?1024?+?"G");
??System.out.println("系統(tǒng)內(nèi)存大?。??+?maxMemory?*?4?/?1024?+?"G");
}
查看 JVM 堆內(nèi)存分配
在默認(rèn)不配置 JVM 堆內(nèi)存大小的情況下,JVM 根據(jù)默認(rèn)值來(lái)配置當(dāng)前內(nèi)存大小
默認(rèn)情況下新生代和老年代的比例是 1:2,可以通過(guò)
–XX:NewRatio來(lái)配置
- 新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1,可以通過(guò)
-XX:SurvivorRatio來(lái)配置
若在JDK 7中開啟了 -XX:+UseAdaptiveSizePolicy,JVM 會(huì)動(dòng)態(tài)調(diào)整 JVM 堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡
此時(shí) –XX:NewRatio 和 -XX:SurvivorRatio ?將會(huì)失效,而 JDK 8 是默認(rèn)開啟-XX:+UseAdaptiveSizePolicy
在 JDK 8中,不要隨意關(guān)閉-XX:+UseAdaptiveSizePolicy,除非對(duì)堆內(nèi)存的劃分有明確的規(guī)劃
每次 GC 后都會(huì)重新計(jì)算 Eden、From Survivor、To Survivor 的大小
計(jì)算依據(jù)是GC過(guò)程中統(tǒng)計(jì)的GC時(shí)間、吞吐量、內(nèi)存占用量
java?-XX:+PrintFlagsFinal?-version?|?grep?HeapSize
????uintx?ErgoHeapSizeLimit?????????????????????????=?0???????????????????????????????????{product}
????uintx?HeapSizePerGCThread???????????????????????=?87241520????????????????????????????{product}
????uintx?InitialHeapSize??????????????????????????:=?134217728???????????????????????????{product}
????uintx?LargePageHeapSizeThreshold????????????????=?134217728???????????????????????????{product}
????uintx?MaxHeapSize??????????????????????????????:=?2147483648??????????????????????????{product}
java?version?"1.8.0_211"
Java(TM)?SE?Runtime?Environment?(build?1.8.0_211-b12)
Java?HotSpot(TM)?64-Bit?Server?VM?(build?25.211-b12,?mixed?mode)
$?jmap?-heap?進(jìn)程號(hào)
4.3 對(duì)象在堆中的生命周期
- 在 JVM 內(nèi)存模型的堆中,堆被劃分為新生代和老年代
- 新生代又被進(jìn)一步劃分為Eden區(qū)和Survivor區(qū),Survivor區(qū)由From Survivor和To Survivor組成
- 此時(shí) JVM 會(huì)給對(duì)象定義一個(gè)對(duì)象年輕計(jì)數(shù)器(
-XX:MaxTenuringThreshold)
- JVM 會(huì)把存活的對(duì)象轉(zhuǎn)移到 Survivor 中,并且對(duì)象年齡 +1
- 對(duì)象在 Survivor 中同樣也會(huì)經(jīng)歷 Minor GC,每經(jīng)歷一次 Minor GC,對(duì)象年齡都會(huì)+1
-XX:PetenureSizeThreshold,對(duì)象會(huì)直接被分配到老年代4.4 對(duì)象的分配過(guò)程
為對(duì)象分配內(nèi)存是一件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù),JVM 的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問(wèn)題,并且由于內(nèi)存分配算法和內(nèi)存回收算法密切相關(guān),所以還需要考慮 GC 執(zhí)行完內(nèi)存回收后是否會(huì)在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。
- new 的對(duì)象先放在伊甸園區(qū),此區(qū)有大小限制
- 當(dāng)伊甸園的空間填滿時(shí),程序又需要?jiǎng)?chuàng)建對(duì)象,JVM 的垃圾回收器將對(duì)伊甸園區(qū)進(jìn)行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷毀。再加載新的對(duì)象放到伊甸園區(qū)
- 然后將伊甸園中的剩余對(duì)象移動(dòng)到幸存者 0 區(qū)
- 如果再次觸發(fā)垃圾回收,此時(shí)上次幸存下來(lái)的放到幸存者 0 區(qū),如果沒(méi)有回收,就會(huì)放到幸存者 1 區(qū)
- 如果再次經(jīng)歷垃圾回收,此時(shí)會(huì)重新放回幸存者 0 區(qū),接著再去幸存者 1 區(qū)
- 什么時(shí)候才會(huì)去養(yǎng)老區(qū)呢? 默認(rèn)是 15 次回收標(biāo)記
- 在養(yǎng)老區(qū),相對(duì)悠閑。當(dāng)養(yǎng)老區(qū)內(nèi)存不足時(shí),再次觸發(fā) Major GC,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理
- 若養(yǎng)老區(qū)執(zhí)行了 Major GC ?之后發(fā)現(xiàn)依然無(wú)法進(jìn)行對(duì)象的保存,就會(huì)產(chǎn)生 OOM 異常
4.5 GC 垃圾回收簡(jiǎn)介
Minor GC、Major GC、Full GC
JVM 在進(jìn)行 GC 時(shí),并非每次都對(duì)堆內(nèi)存(新生代、老年代;方法區(qū))區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。
針對(duì) HotSpot VM 的實(shí)現(xiàn),它里面的 GC 按照回收區(qū)域又分為兩大類:部分收集(Partial GC),整堆收集(Full ?GC)
- 部分收集:不是完整收集整個(gè) Java 堆的垃圾收集。其中又分為:
- 目前只有 G1 GC 會(huì)有這種行為
- 目前,只有 CMS GC 會(huì)有單獨(dú)收集老年代的行為
- 很多時(shí)候 Major GC 會(huì)和 Full GC ?混合使用,需要具體分辨是老年代回收還是整堆回收
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 混合收集(Mixed GC):收集整個(gè)新生代以及部分老年代的垃圾收集
- 整堆收集(Full GC):收集整個(gè) Java 堆和方法區(qū)的垃圾
4.6 TLAB
什么是 TLAB (Thread Local Allocation Buffer)?
- 從內(nèi)存模型而不是垃圾回收的角度,對(duì) Eden 區(qū)域繼續(xù)進(jìn)行劃分,JVM 為每個(gè)線程分配了一個(gè)私有緩存區(qū)域,它包含在 Eden 空間內(nèi)
- 多線程同時(shí)分配內(nèi)存時(shí),使用 TLAB 可以避免一系列的非線程安全問(wèn)題,同時(shí)還能提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方式稱為快速分配策略
- OpenJDK 衍生出來(lái)的 JVM 大都提供了 TLAB 設(shè)計(jì)
為什么要有 TLAB ?
- 堆區(qū)是線程共享的,任何線程都可以訪問(wèn)到堆區(qū)中的共享數(shù)據(jù)
- 由于對(duì)象實(shí)例的創(chuàng)建在 JVM 中非常頻繁,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
- 為避免多個(gè)線程操作同一地址,需要使用加鎖等機(jī)制,進(jìn)而影響分配速度
盡管不是所有的對(duì)象實(shí)例都能夠在 TLAB 中成功分配內(nèi)存,但 JVM 確實(shí)是將 TLAB 作為內(nèi)存分配的首選。
在程序中,可以通過(guò) -XX:UseTLAB 設(shè)置是否開啟 TLAB 空間。
默認(rèn)情況下,TLAB 空間的內(nèi)存非常小,僅占有整個(gè) Eden 空間的 1%,我們可以通過(guò) -XX:TLABWasteTargetPercent 設(shè)置 TLAB 空間所占用 Eden 空間的百分比大小。
一旦對(duì)象在 TLAB 空間分配內(nèi)存失敗時(shí),JVM 就會(huì)嘗試著通過(guò)使用加鎖機(jī)制確保數(shù)據(jù)操作的原子性,從而直接在 Eden 空間中分配內(nèi)存。
4.7 堆是分配對(duì)象存儲(chǔ)的唯一選擇嗎
隨著 JIT 編譯期的發(fā)展和逃逸分析技術(shù)的逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化,所有的對(duì)象都分配到堆上也漸漸變得不那么“絕對(duì)”了。 ?——《深入理解 Java 虛擬機(jī)》
逃逸分析
逃逸分析(Escape Analysis)是目前 Java 虛擬機(jī)中比較前沿的優(yōu)化技術(shù)。這是一種可以有效減少 Java 程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。通過(guò)逃逸分析,Java Hotspot 編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:
- 當(dāng)一個(gè)對(duì)象在方法中被定義后,對(duì)象只在方法內(nèi)部使用,則認(rèn)為沒(méi)有發(fā)生逃逸。
- 當(dāng)一個(gè)對(duì)象在方法中被定義后,它被外部方法所引用,則認(rèn)為發(fā)生逃逸。例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。
例如:
public?static?StringBuffer?craeteStringBuffer(String?s1,?String?s2)?{
???StringBuffer?sb?=?new?StringBuffer();
???sb.append(s1);
???sb.append(s2);
???return?sb;
}
StringBuffer sb是一個(gè)方法內(nèi)部變量,上述代碼中直接將sb返回,這樣這個(gè) StringBuffer 有可能被其他方法所改變,這樣它的作用域就不只是在方法內(nèi)部,雖然它是一個(gè)局部變量,稱其逃逸到了方法外部。甚至還有可能被外部線程訪問(wèn)到,譬如賦值給類變量或可以在其他線程中訪問(wèn)的實(shí)例變量,稱為線程逃逸。
上述代碼如果想要 StringBuffer sb不逃出方法,可以這樣寫:
public?static?String?createStringBuffer(String?s1,?String?s2)?{
???StringBuffer?sb?=?new?StringBuffer();
???sb.append(s1);
???sb.append(s2);
???return?sb.toString();
}
不直接返回 StringBuffer,那么 StringBuffer 將不會(huì)逃逸出方法。
參數(shù)設(shè)置:
- 在 JDK 6u23版本之后,HotSpot 中默認(rèn)就已經(jīng)開啟了逃逸分析
- 如果使用較早版本,可以通過(guò)
-XX"+DoEscapeAnalysis顯式開啟
開發(fā)中使用局部變量,就不要在方法外定義。
使用逃逸分析,編譯器可以對(duì)代碼做優(yōu)化:
- 棧上分配:將堆分配轉(zhuǎn)化為棧分配。如果一個(gè)對(duì)象在子程序中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,對(duì)象可能是棧分配的候選,而不是堆分配
- 同步省略:如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問(wèn)到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步
- 分離對(duì)象或標(biāo)量替換:有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問(wèn)到,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而存儲(chǔ)在 CPU 寄存器
JIT 編譯器在編譯期間根據(jù)逃逸分析的結(jié)果,發(fā)現(xiàn)如果一個(gè)對(duì)象并沒(méi)有逃逸出方法的話,就可能被優(yōu)化成棧上分配。分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對(duì)象也被回收。這樣就無(wú)需進(jìn)行垃圾回收了。
常見(jiàn)棧上分配的場(chǎng)景:成員變量賦值、方法返回值、實(shí)例引用傳遞
代碼優(yōu)化之同步省略(消除)
- 線程同步的代價(jià)是相當(dāng)高的,同步的后果是降低并發(fā)性和性能
- 在動(dòng)態(tài)編譯同步塊的時(shí)候,JIT 編譯器可以借助逃逸分析來(lái)判斷同步塊所使用的鎖對(duì)象是否能夠被一個(gè)線程訪問(wèn)而沒(méi)有被發(fā)布到其他線程。如果沒(méi)有,那么 JIT 編譯器在編譯這個(gè)同步塊的時(shí)候就會(huì)取消對(duì)這個(gè)代碼的同步。這樣就能大大提高并發(fā)性和性能。這個(gè)取消同步的過(guò)程就叫做同步省略,也叫鎖消除。
public?void?keep()?{
??Object?keeper?=?new?Object();
??synchronized(keeper)?{
????System.out.println(keeper);
??}
}
如上代碼,代碼中對(duì) keeper 這個(gè)對(duì)象進(jìn)行加鎖,但是 keeper 對(duì)象的生命周期只在 keep()方法中,并不會(huì)被其他線程所訪問(wèn)到,所以在 JIT編譯階段就會(huì)被優(yōu)化掉。優(yōu)化成:
public?void?keep()?{
??Object?keeper?=?new?Object();
??System.out.println(keeper);
}
代碼優(yōu)化之標(biāo)量替換
標(biāo)量(Scalar)是指一個(gè)無(wú)法再分解成更小的數(shù)據(jù)的數(shù)據(jù)。Java 中的原始數(shù)據(jù)類型就是標(biāo)量。
相對(duì)的,那些的還可以分解的數(shù)據(jù)叫做聚合量(Aggregate),Java 中的對(duì)象就是聚合量,因?yàn)槠溥€可以分解成其他聚合量和標(biāo)量。
在 JIT 階段,通過(guò)逃逸分析確定該對(duì)象不會(huì)被外部訪問(wèn),并且對(duì)象可以被進(jìn)一步分解時(shí),JVM不會(huì)創(chuàng)建該對(duì)象,而會(huì)將該對(duì)象成員變量分解若干個(gè)被這個(gè)方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。這個(gè)過(guò)程就是標(biāo)量替換。
通過(guò) -XX:+EliminateAllocations 可以開啟標(biāo)量替換,-XX:+PrintEliminateAllocations 查看標(biāo)量替換情況。
public?static?void?main(String[]?args)?{
???alloc();
}
private?static?void?alloc()?{
???Point?point?=?new?Point(1,2);
???System.out.println("point.x="+point.x+";?point.y="+point.y);
}
class?Point{
????private?int?x;
????private?int?y;
}
以上代碼中,point 對(duì)象并沒(méi)有逃逸出alloc()方法,并且 point 對(duì)象是可以拆解成標(biāo)量的。那么,JIT 就不會(huì)直接創(chuàng)建 Point 對(duì)象,而是直接使用兩個(gè)標(biāo)量 int x ,int y 來(lái)替代 Point 對(duì)象。
private?static?void?alloc()?{
???int?x?=?1;
???int?y?=?2;
???System.out.println("point.x="+x+";?point.y="+y);
}
代碼優(yōu)化之棧上分配
我們通過(guò) JVM 內(nèi)存分配可以知道 JAVA 中的對(duì)象都是在堆上進(jìn)行分配,當(dāng)對(duì)象沒(méi)有被引用的時(shí)候,需要依靠 GC 進(jìn)行回收內(nèi)存,如果對(duì)象數(shù)量較多的時(shí)候,會(huì)給 GC 帶來(lái)較大壓力,也間接影響了應(yīng)用的性能。為了減少臨時(shí)對(duì)象在堆內(nèi)分配的數(shù)量,JVM 通過(guò)逃逸分析確定該對(duì)象不會(huì)被外部訪問(wèn)。那就通過(guò)標(biāo)量替換將該對(duì)象分解在棧上分配內(nèi)存,這樣該對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
總結(jié):
關(guān)于逃逸分析的論文在1999年就已經(jīng)發(fā)表了,但直到JDK 1.6才有實(shí)現(xiàn),而且這項(xiàng)技術(shù)到如今也并不是十分成熟的。
其根本原因就是無(wú)法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經(jīng)過(guò)逃逸分析可以做標(biāo)量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過(guò)程。
一個(gè)極端的例子,就是經(jīng)過(guò)逃逸分析之后,發(fā)現(xiàn)沒(méi)有一個(gè)對(duì)象是不逃逸的。那這個(gè)逃逸分析的過(guò)程就白白浪費(fèi)掉了。
雖然這項(xiàng)技術(shù)并不十分成熟,但是他也是即時(shí)編譯器優(yōu)化技術(shù)中一個(gè)十分重要的手段。
五、方法區(qū)
- 方法區(qū)(Method Area)與 Java 堆一樣,是所有線程共享的內(nèi)存區(qū)域。
- 雖然 Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開。
- 運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將類在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的是
String.intern()方法。受方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError 異常。 - 方法區(qū)的大小和堆空間一樣,可以選擇固定大小也可選擇可擴(kuò)展,方法區(qū)的大小決定了系統(tǒng)可以放多少個(gè)類,如果系統(tǒng)類太多,導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤
- JVM 關(guān)閉后方法區(qū)即被釋放
5.1 解惑
你是否也有看不同的參考資料,有的內(nèi)存結(jié)構(gòu)圖有方法區(qū),有的又是永久代,元數(shù)據(jù)區(qū),一臉懵逼的時(shí)候?
- 方法區(qū)(method area)只是JVM規(guī)范中定義的一個(gè)概念,用于存儲(chǔ)類信息、常量池、靜態(tài)變量、JIT編譯后的代碼等數(shù)據(jù),并沒(méi)有規(guī)定如何去實(shí)現(xiàn)它,不同的廠商有不同的實(shí)現(xiàn)。而永久代(PermGen)是 Hotspot 虛擬機(jī)特有的概念, Java8 的時(shí)候又被元空間取代了,永久代和元空間都可以理解為方法區(qū)的落地實(shí)現(xiàn)。
- 永久代物理是堆的一部分,和新生代,老年代地址是連續(xù)的(受垃圾回收器管理),而元空間存在于本地內(nèi)存(我們常說(shuō)的堆外內(nèi)存,不受垃圾回收器管理),這樣就不受 JVM 限制了,也比較難發(fā)生OOM(都會(huì)有溢出異常)
- Java7 中我們通過(guò)
-XX:PermSize和-xx:MaxPermSize來(lái)設(shè)置永久代參數(shù),Java8 之后,隨著永久代的取消,這些參數(shù)也就隨之失效了,改為通過(guò)-XX:MetaspaceSize和-XX:MaxMetaspaceSize用來(lái)設(shè)置元空間參數(shù) - 存儲(chǔ)內(nèi)容不同,元空間存儲(chǔ)類的元信息,靜態(tài)變量和常量池等并入堆中。相當(dāng)于永久代的數(shù)據(jù)被分到了堆和元空間中
- 如果方法區(qū)域中的內(nèi)存不能用于滿足分配請(qǐng)求,則 Java 虛擬機(jī)拋出
OutOfMemoryError - JVM 規(guī)范說(shuō)方法區(qū)在邏輯上是堆的一部分,但目前實(shí)際上是與 Java 堆分開的(Non-Heap)
所以對(duì)于方法區(qū),Java8 之后的變化:
- 移除了永久代(PermGen),替換為元空間(Metaspace);
- 永久代中的 class metadata 轉(zhuǎn)移到了 native memory(本地內(nèi)存,而不是虛擬機(jī));
- 永久代中的 interned Strings 和 class static variables 轉(zhuǎn)移到了 Java heap;
- 永久代參數(shù) (PermSize MaxPermSize) -> 元空間參數(shù)(MetaspaceSize MaxMetaspaceSize)
5.2 設(shè)置方法區(qū)內(nèi)存的大小
jdk8及以后:
- 元數(shù)據(jù)區(qū)大小可以使用參數(shù)
-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的兩個(gè)參數(shù) - 默認(rèn)值依賴于平臺(tái)。Windows 下,
-XX:MetaspaceSize是 21M,-XX:MaxMetaspacaSize的值是 -1,即沒(méi)有限制 - 與永久代不同,如果不指定大小,默認(rèn)情況下,虛擬機(jī)會(huì)耗盡所有的可用系統(tǒng)內(nèi)存。如果元數(shù)據(jù)發(fā)生溢出,虛擬機(jī)一樣會(huì)拋出異常
OutOfMemoryError:Metaspace -XX:MetaspaceSize:設(shè)置初始的元空間大小。對(duì)于一個(gè) 64 位的服務(wù)器端 JVM 來(lái)說(shuō),其默認(rèn)的-XX:MetaspaceSize的值為20.75MB,這就是初始的高水位線,一旦觸及這個(gè)水位線,F(xiàn)ull GC 將會(huì)被觸發(fā)并卸載沒(méi)用的類(即這些類對(duì)應(yīng)的類加載器不再存活),然后這個(gè)高水位線將會(huì)重置,新的高水位線的值取決于 GC 后釋放了多少元空間。如果釋放的空間不足,那么在不超過(guò)MaxMetaspaceSize時(shí),適當(dāng)提高該值。如果釋放空間過(guò)多,則適當(dāng)降低該值- 如果初始化的高水位線設(shè)置過(guò)低,上述高水位線調(diào)整情況會(huì)發(fā)生很多次,通過(guò)垃圾回收的日志可觀察到 Full GC 多次調(diào)用。為了避免頻繁 GC,建議將
-XX:MetaspaceSize設(shè)置為一個(gè)相對(duì)較高的值。
5.3 方法區(qū)內(nèi)部結(jié)構(gòu)
方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等。
類型信息
對(duì)每個(gè)加載的類型(類 class、接口 interface、枚舉 enum、注解 annotation),JVM 必須在方法區(qū)中存儲(chǔ)以下類型信息
- 這個(gè)類型的完整有效名稱(全名=包名.類名)
- 這個(gè)類型直接父類的完整有效名(對(duì)于 interface或是 java.lang.Object,都沒(méi)有父類)
- 這個(gè)類型的修飾符(public,abstract,final 的某個(gè)子集)
- 這個(gè)類型直接接口的一個(gè)有序列表
域(Field)信息
- JVM 必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序
- 域的相關(guān)信息包括:域名稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient 的某個(gè)子集)
方法(Method)信息
JVM 必須保存所有方法的
- 方法名稱
- 方法的返回類型
- 方法參數(shù)的數(shù)量和類型
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract 的一個(gè)子集)
- 方法的字符碼(bytecodes)、操作數(shù)棧、局部變量表及大?。╝bstract 和 native 方法除外)
- 異常表(abstract 和 native 方法除外)
- 每個(gè)異常處理的開始位置、結(jié)束位置、代碼處理在程序計(jì)數(shù)器中的偏移地址、被捕獲的異常類的常量池索引
棧、堆、方法區(qū)的交互關(guān)系

5.4 運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分,理解運(yùn)行時(shí)常量池的話,我們先來(lái)說(shuō)說(shuō)字節(jié)碼文件(Class 文件)中的常量池(常量池表)
常量池
一個(gè)有效的字節(jié)碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項(xiàng)信息那就是常量池表(Constant Pool Table),包含各種字面量和對(duì)類型、域和方法的符號(hào)引用。
為什么需要常量池?
一個(gè) java 源文件中的類、接口,編譯后產(chǎn)生一個(gè)字節(jié)碼文件。而 Java 中的字節(jié)碼需要數(shù)據(jù)支持,通常這種數(shù)據(jù)會(huì)很大以至于不能直接存到字節(jié)碼里,換另一種方式,可以存到常量池,這個(gè)字節(jié)碼包含了指向常量池的引用。在動(dòng)態(tài)鏈接的時(shí)候用到的就是運(yùn)行時(shí)常量池。
如下,我們通過(guò)jclasslib 查看一個(gè)只有 Main 方法的簡(jiǎn)單類,字節(jié)碼中的 #2 指向的就是 Constant Pool

常量池可以看作是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量等類型。
運(yùn)行時(shí)常量池
- 在加載類和結(jié)構(gòu)到虛擬機(jī)后,就會(huì)創(chuàng)建對(duì)應(yīng)的運(yùn)行時(shí)常量池
- 常量池表(Constant Pool Table)是 Class 文件的一部分,用于存儲(chǔ)編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中
- JVM 為每個(gè)已加載的類型(類或接口)都維護(hù)一個(gè)常量池。池中的數(shù)據(jù)項(xiàng)像數(shù)組項(xiàng)一樣,是通過(guò)索引訪問(wèn)的
- 運(yùn)行時(shí)常量池中包含各種不同的常量,包括編譯器就已經(jīng)明確的數(shù)值字面量,也包括到運(yùn)行期解析后才能夠獲得的方法或字段引用。此時(shí)不再是常量池中的符號(hào)地址了,這里換為真實(shí)地址
- 運(yùn)行時(shí)常量池,相對(duì)于 Class 文件常量池的另一個(gè)重要特征是:動(dòng)態(tài)性,Java 語(yǔ)言并不要求常量一定只有編譯期間才能產(chǎn)生,運(yùn)行期間也可以將新的常量放入池中,String 類的
intern()方法就是這樣的
- 運(yùn)行時(shí)常量池,相對(duì)于 Class 文件常量池的另一個(gè)重要特征是:動(dòng)態(tài)性,Java 語(yǔ)言并不要求常量一定只有編譯期間才能產(chǎn)生,運(yùn)行期間也可以將新的常量放入池中,String 類的
- 當(dāng)創(chuàng)建類或接口的運(yùn)行時(shí)常量池時(shí),如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過(guò)了方法區(qū)所能提供的最大值,則 JVM 會(huì)拋出 OutOfMemoryError 異常。
5.5 方法區(qū)在 JDK6、7、8中的演進(jìn)細(xì)節(jié)
只有 HotSpot 才有永久代的概念
jdk1.6及之前
有永久代,靜態(tài)變量存放在永久代上
jdk1.7 | 有永久代,但已經(jīng)逐步“去永久代”,字符串常量池、靜態(tài)變量移除,保存在堆中 |
jdk1.8及之后 | 取消永久代,類型信息、字段、方法、常量保存在本地內(nèi)存的元空間,但字符串常量池、靜態(tài)變量仍在堆中 |
移除永久代原因
http://openjdk.java.net/jeps/122

為永久代設(shè)置空間大小是很難確定的。
在某些場(chǎng)景下,如果動(dòng)態(tài)加載類過(guò)多,容易產(chǎn)生 Perm 區(qū)的 OOM。如果某個(gè)實(shí)際 Web 工程中,因?yàn)楣δ茳c(diǎn)比較多,在運(yùn)行過(guò)程中,要不斷動(dòng)態(tài)加載很多類,經(jīng)常出現(xiàn) OOM。而元空間和永久代最大的區(qū)別在于,元空間不在虛擬機(jī)中,而是使用本地內(nèi)存,所以默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制
對(duì)永久代進(jìn)行調(diào)優(yōu)較困難
5.6 方法區(qū)的垃圾回收
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類型。
先來(lái)說(shuō)說(shuō)方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號(hào)引用。字面量比較接近 java 語(yǔ)言層次的常量概念,如文本字符串、被聲明為 final 的常量值等。而符號(hào)引用則屬于編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
HotSpot 虛擬機(jī)對(duì)常量池的回收策略是很明確的,只要常量池中的常量沒(méi)有被任何地方引用,就可以被回收
判定一個(gè)類型是否屬于“不再被使用的類”,需要同時(shí)滿足三個(gè)條件:
- 該類所有的實(shí)例都已經(jīng)被回收,也就是 Java 堆中不存在該類及其任何派生子類的實(shí)例
- 加載該類的類加載器已經(jīng)被回收,這個(gè)條件除非是經(jīng)過(guò)精心設(shè)計(jì)的可替換類加載器的場(chǎng)景,如 OSGi、JSP 的重加載等,否則通常很難達(dá)成
- 該類對(duì)應(yīng)的 java.lang.Class 對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法
Java 虛擬機(jī)被允許堆滿足上述三個(gè)條件的無(wú)用類進(jìn)行回收,這里說(shuō)的僅僅是“被允許”,而并不是和對(duì)象一樣,不使用了就必然會(huì)回收。是否對(duì)類進(jìn)行回收,HotSpot 虛擬機(jī)提供了 -Xnoclassgc 參數(shù)進(jìn)行控制,還可以使用 -verbose:class 以及 -XX:+TraceClassLoading 、-XX:+TraceClassUnLoading 查看類加載和卸載信息。
在大量使用反射、動(dòng)態(tài)代理、CGLib 等 ByteCode 框架、動(dòng)態(tài)生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場(chǎng)景都需要虛擬機(jī)具備類卸載的功能,以保證永久代不會(huì)溢出。
參考與感謝
算是一篇學(xué)習(xí)筆記,共勉,主要來(lái)源:
宋紅康 JVM 教程
《深入理解 Java 虛擬機(jī) 第三版》
https://docs.oracle.com/javase/specs/index.html
https://www.cnblogs.com/wicfhwffg/p/9382677.html
https://www.cnblogs.com/hollischuang/p/12501950.html

喜歡就點(diǎn)個(gè)在看再走吧
