Java 虛擬機(jī)系列一:一文搞懂 JVM 架構(gòu)和運(yùn)行時數(shù)據(jù)區(qū)
Java 虛擬機(jī)系列二:垃圾收集機(jī)制詳解,動圖幫你理解
前言
之前寫博客一直比較隨性,主題也很隨意,就是想到什么寫什么,對什么感興趣就寫什么。雖然寫起來無拘無束,自在隨意,但也帶來了一些問題,每次寫完一篇后就要去糾結(jié)下一篇到底寫什么,看來選擇太多也不是好事兒,更重要的是不成體系的內(nèi)容對讀者也不夠友好。所以以后的博客盡量按系列來寫,不過偶爾也會穿插其他的內(nèi)容。接下來一段時間我會把寫博客的重點(diǎn)放在 JVM (Java Virtual Machine) 和 JUC (java util concurrent ) 上,對 Java 虛擬機(jī)和 Java 并發(fā)編程進(jìn)行一系列的介紹,歡迎關(guān)注。
了解 JVM 是對 Java 開發(fā)人員的基本要求,JVM 的相關(guān)內(nèi)容自然也成了現(xiàn)在 Java 程序員面試的重要考點(diǎn)。不過估計(jì)很多小伙伴和我一樣,長時間醉心于 CRUD,卻忘了去了解一下更底層、更基礎(chǔ)的東西,殊不知這些才是決定你能在這條路上走多遠(yuǎn)的關(guān)鍵因素,那接下來我們就一起來深入學(xué)習(xí)一下看似神秘的 JVM 吧。JVM 總體來看內(nèi)容還是很多的,我會把最重要的內(nèi)容介紹給大家,不過如果你有時間和精力的話,還是推薦你去看一下《深入理解Java虛擬機(jī)》這本書,確實(shí)是有口皆碑。本系列文章也會引用很多此書的內(nèi)容并加上我自己的理解,如果你堅(jiān)持看下去的話,相信會有很大的收獲。
首先對 JVM 做個簡單的介紹,JVM 是 JDK 的一部分,《Java 虛擬機(jī)規(guī)范》(The Java Virtual Machine Specification) 是平行于《Java 語言規(guī)范》(The Java Language Specification)的一套獨(dú)立的規(guī)范,不同的公司對其有不同的實(shí)現(xiàn) (類似于一個接口被不同的類實(shí)現(xiàn)),比較著名的 Java 虛擬機(jī)實(shí)現(xiàn)版本有 HotSpot、JRockit 和 J9 等。
本文分為兩大部分,將分別為大家介紹 JVM 的整體架構(gòu)和運(yùn)行時數(shù)據(jù)區(qū),這兩部分的依據(jù)均是《Java 虛擬機(jī)規(guī)范》,而不針對任何特定的 JVM 具體實(shí)現(xiàn)版本。
一、Java 虛擬機(jī)架構(gòu) (JVM Architecture)
在我看來,不管學(xué)習(xí)什么樣的知識或技術(shù),首先要做的就是從全局上去認(rèn)識它,這樣才能避免盲人摸象,事倍功半的情況發(fā)生。既然要學(xué)習(xí) JVM,就要先了解它的整體架構(gòu),于是我畫了個 JVM 架構(gòu)圖來幫助大家認(rèn)識它。

對 JVM 還不太了解的同學(xué)第一次看到這張花里胡哨的圖肯定會一臉懵逼,不用怕,其實(shí)我們只需要重點(diǎn)理解并掌握其中一部分 (同時也是面試重點(diǎn)) 就好了,比如運(yùn)行時數(shù)據(jù)區(qū)、垃圾收集器、內(nèi)存分配策略和類加載機(jī)制等,類文件結(jié)構(gòu)也可以學(xué)習(xí)一下,其他的稍作了解即可。既然本篇文章是要帶領(lǐng)大家認(rèn)識 JVM 架構(gòu)的,那就先把圖中各個部分都介紹一下吧 (注:本文只做介紹,讓各位先對 JVM 有個整體的認(rèn)識,本系列后續(xù)文章會做深入探討)。
1.1 Class 文件 (字節(jié)碼文件)
Java 之所以號稱“一次編寫,處處運(yùn)行”,就是得益于虛擬機(jī)和 Class 文件 (注:CLass 文件、字節(jié)碼文件和類文件是一個意思) 的組合機(jī)制。程序員并不需要自己去適配不同的操作系統(tǒng),大家都知道我們平時編寫的 java 代碼在編譯成 Class 文件后才能執(zhí)行,而 Class 文件可以在任何操作系統(tǒng)上的 JVM 上執(zhí)行,這樣就做到了“平臺無關(guān)性”。下面是一個最簡單的 HelloWorld 程序及其對應(yīng)的 Class 文件。

得益于 Class 文件,JVM 還可以做到“語言無關(guān)性”,也就是說不只有 Java 程序可以運(yùn)行于 JVM 之上,很多其他語言例如最近在安卓開發(fā)者中大火的 Kotlin 語言,還有 Scala、Groovy 等語言也都是基于 JVM 平臺的,這些語言的代碼都可以編譯成 Class 文件,然后在 JVM 上運(yùn)行。

1.2 類加載器子系統(tǒng) (ClassLoader Subsystem)
要執(zhí)行 Class 文件就需要先將其加載進(jìn)內(nèi)存,這一工作正是由類加載器 (ClassLoader) 完成的,系統(tǒng)為我們提供了三種類加載器,分別是啟動類加載器 (Bootstrap ClassLoader)、擴(kuò)展類加載器 (Extension ClassLoader) 和應(yīng)用程序類加載器 (Application ClassLoader),如果有必要,我們也可以加入自定義的類加載器。類加載過程如下:

類加載過程分為加載、連接和初始化三個階段,其中的連接階段又分為驗(yàn)證、準(zhǔn)備和解析三個階段 (詳細(xì)的類加載機(jī)制在后續(xù)文章中進(jìn)行介紹)。
1.3 Java 虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū) (JVM Runtime Data Area)
這部分內(nèi)容較多,放在本文第二部分單獨(dú)進(jìn)行介紹。
1.4 執(zhí)行引擎 (Execution Engine)
字節(jié)碼被加載進(jìn)運(yùn)行時數(shù)據(jù)區(qū)后,執(zhí)行引擎會進(jìn)行讀取并執(zhí)行,執(zhí)行引擎主要包含以下模塊:
- 解釋器 (Interpreter):相信大家很久以前就聽過“計(jì)算機(jī)只認(rèn)識0和1”這句話,時至今日,計(jì)算機(jī)依然只認(rèn)識0和1,所以任何編程語言的代碼最終都要轉(zhuǎn)化成機(jī)器碼 (二進(jìn)制代碼)才能執(zhí)行,Java 也不例外,而解釋器的工作正是將編譯得到的字節(jié)碼再轉(zhuǎn)化成機(jī)器碼,然后才能執(zhí)行。正因?yàn)槿绱?,Java 才被稱為解釋型語言,也正是因?yàn)檫吔忉屵厛?zhí)行的特點(diǎn),Java 程序在執(zhí)行時才會慢于 C++ 之類的編譯型語言。
- 即時編譯器 (JIT Compiler,just-in-time compiler):即時編譯器百度百科,為了彌補(bǔ)解釋執(zhí)行帶來的速度劣勢,JVM 引入了即時編譯器,它的作用就是把熱點(diǎn)代碼,比如重復(fù)調(diào)用的方法和循環(huán)代碼等,編譯成機(jī)器碼并存放在 code cache 中,這樣之后再用到這些代碼就不用重新解釋執(zhí)行了,可以提高程序運(yùn)行效率。
- 垃圾收集器 (Garbage Collector):Java 程序員可以不用手動釋放內(nèi)存,全是垃圾收集器的功勞,這也是 JVM 中尤其重要的內(nèi)容,后續(xù)會有多篇文章對其進(jìn)行介紹。
1.5 本地庫接口 (JNI,Java Native Interface)
如果你經(jīng)???JDK 源碼的話,一定會注意到 native 這個關(guān)鍵詞,被它修飾的方法是沒有方法體的,是因?yàn)樗{(diào)用了計(jì)算機(jī)本地的方法庫 (通常是 C 或 C++ 代碼)。JDK 源碼中有很多類的方法,特別是一些需要操作計(jì)算機(jī)硬件的方法,都調(diào)用了本地方法庫,畢竟與硬件打交道還是用 C 和 C++ 更方便,比如下面這些方法:
// 例一:這是 Thread 類中的 currentThread 方法,用于獲取當(dāng)前正在執(zhí)行的線程
public static native Thread currentThread();
// 例二:這是 FileInputStream 類中 open0 方法,用于打開指定文件
private native void open0(String name) throws FileNotFoundException;
1.6 本地方法庫 (Native Method Library)
本地庫接口所調(diào)用的對象正是位于這個庫中,一般是位于計(jì)算機(jī)本地的 C 或 C++ 語言代碼。
二、Java 虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)
Java 虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)是我們需要重點(diǎn)了解并熟悉的部分,因?yàn)檫@與我們寫的程序息息相關(guān),平時常見的 StackOverflowError 和 OutOfMemoryError 也幾乎都是來自這個區(qū)域。說“幾乎”是因?yàn)楫?dāng)本機(jī)直接內(nèi)存不夠用時也會拋出 OutOfMemoryError。如下圖所示,程序計(jì)數(shù)器、Java 虛擬機(jī)棧和本地方法棧是線程私有的,堆和方法區(qū)是線程共享的,其中方法區(qū)又包含了運(yùn)行時常量池。下面就對這個部分做個詳細(xì)的介紹吧 (注:本部分引用內(nèi)容來自《深入理解Java虛擬機(jī)》)。

2.1 程序計(jì)數(shù)器 (Program Counter Register)
怕有些小伙伴不清楚,提示一下:下面這樣的段落格式就是 Markdown 里的引用格式,,一般用于引用他人的文章或別處的內(nèi)容。
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在Java虛擬機(jī)的概念里,字節(jié)碼解釋器工作時就是通過改變這個計(jì)數(shù)器 的值來選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計(jì)數(shù)器來完成。
由于Java虛擬機(jī)的多線程是通過線程輪流切換、分配處理器執(zhí)行時間的方式來實(shí)現(xiàn)的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)都只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個Java方法,這個計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是本地 (Native) 方法,這個計(jì)數(shù)器值則應(yīng)為空 (Undefined)。此內(nèi)存區(qū)域是唯一一個在《Java虛擬機(jī)規(guī)范》中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。
這里引用了《深入理解Java虛擬機(jī)》書中的內(nèi)容,其實(shí)不難理解,程序計(jì)數(shù)器的作用就是保存線程的執(zhí)行狀態(tài),引用部分的第三段中說“如果線程正在執(zhí)行的是一個Java方法,這個計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址”,這個地址就是字節(jié)碼執(zhí)行到的位置。我們平時說的 Java 多線程上下文切換就需要程序計(jì)數(shù)器的輔助,當(dāng) CPU 從一個線程切換到另一個線程時,要從程序計(jì)數(shù)器中讀取線程執(zhí)行狀態(tài)從而恢復(fù)現(xiàn)場。后面又說“如果執(zhí)行的是本地 (Native)方法,這個計(jì)數(shù)器值為空(Undefined)”,這是為何呢?是因?yàn)楸镜胤椒▓?zhí)行的是 C / C++ 代碼,在原生平臺直接運(yùn)行,也就不存在 Java 虛擬機(jī)的概念,自然也無法保存字節(jié)碼指令地址,此時要想記錄代碼運(yùn)行狀態(tài)的話,只能使用原生 CPU 的 PC 寄存器。
2.2 Java 虛擬機(jī)棧 (JVM Stacks)
與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stack)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是 Java 方法執(zhí)行的線程內(nèi)存模型:每個方法被執(zhí)行的時候,Java 虛擬機(jī)都 會同步創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完畢的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型(boolean、byte、char、short、int、 float、long、double)、對象引用 (reference 類型,它并不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個代表對象的句柄或者其他與此對象相關(guān)的位置) 和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)。
這些數(shù)據(jù)類型在局部變量表中的存儲空間以局部變量槽 (Slot) 來表示,其中64位長度的 long 和 double 類型的數(shù)據(jù)會占用兩個變量槽,其余的數(shù)據(jù)類型只占用一個。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個方法時,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會改變局部變量表的大小。請讀者注意,這里說的“大小”是指變量槽的數(shù)量,虛擬機(jī)真正使用多大的內(nèi)存空間 (譬如按照1個變量槽占用32個比特、64個比特,或者更多)來實(shí)現(xiàn)一個變量槽,這是完全由具體的虛擬機(jī)實(shí)現(xiàn)自行決定的事情。
在《Java虛擬機(jī)規(guī)范》中,對這個內(nèi)存區(qū)域規(guī)定了兩類異常狀況:如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出 StackOverflowError 異常;如果 Java 虛擬機(jī)棧容量可以動態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時無法申請到足夠的內(nèi)存會拋出 OutOfMemoryError 異常。
Java 虛擬機(jī)棧的內(nèi)部結(jié)構(gòu)如下圖所示:

2.2.1 局部變量表
局部變量表是存放方法參數(shù)和局部變量的區(qū)域。 局部變量沒有準(zhǔn)備階段, 必須顯式初始化。如果是非靜態(tài)方法,則在 index[0] 位置上存儲的是方法所屬對象的實(shí)例引用,一個引用變量占 4 個字節(jié),隨后存儲的是參數(shù)和局部變量。
2.2.2 操作數(shù)棧
操作數(shù)棧是個初始狀態(tài)為空的桶式結(jié)構(gòu)棧。在方法執(zhí)行過程中, 會有各種指令往棧中寫入和提取信息。JVM 的執(zhí)行引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作數(shù)棧。字節(jié)碼指令集的定義都是基于棧類型的,棧的深度在方法元信息的 stack 屬性中。下面使用 i++ 和 ++i 的區(qū)別來幫助理解操作數(shù)棧:
i++ 和 ++i 的區(qū)別:
- i++:從局部變量表取出 i 并壓入操作棧,然后對局部變量表中的 i 自增 1,將操作棧棧頂值取出使用,最后,使用棧頂值更新局部變量表,如此線程從操作棧讀到的是自增之前的值。
- ++i:先對局部變量表的 i 自增 1,然后取出并壓入操作棧,再將操作棧棧頂值取出使用,最后,使用棧頂值更新局部變量表,線程從操作棧讀到的是自增之后的值。
之所以說 i++ 不是原子操作,即使使用 volatile 修飾也不是線程安全,就是因?yàn)?,可?i 被從局部變量表(內(nèi)存)取出,壓入操作棧(寄存器),操作棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內(nèi)存),其中分為 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另一個線程的 3 步打斷,產(chǎn)生數(shù)據(jù)互相覆蓋問題,從而導(dǎo)致 i 的值比預(yù)期的小。
2.2.3 動態(tài)連接
每個棧幀中包含一個在常量池中對當(dāng)前方法的引用, 目的是支持方法調(diào)用過程的動態(tài)連接。
2.2.4 方法出口
方法執(zhí)行時有兩種退出情況:
- 正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令,如 RETURN、IRETURN、ARETURN 等;
- 異常退出。
無論何種退出情況,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀,退出可能有三種方式:
- 返回值壓入上層調(diào)用棧幀。
- 異常信息拋給能夠處理的棧幀。
- 程序計(jì)數(shù)器指向方法調(diào)用后的下一條指令。
2.3 本地方法棧 (Native Method Stacks)
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法 (也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地 (Native) 方法服務(wù)。
《Java虛擬機(jī)規(guī)范》對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有任何強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,甚至有的Java虛擬機(jī) (譬如Hot-Spot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧也會在棧深度溢出或者棧擴(kuò)展失 敗時分別拋出 StackOverflowError 和OutOfMemoryError 異常。
這部分比較好理解,就不做解析了。
2.4 Java 堆 (Heap)
對于Java應(yīng)用程序來說,Java 堆 (Java Heap)是虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例,Java 世界里“幾乎”所有的對象實(shí)例都在這里分配內(nèi)存。Java 堆是垃圾收集器管理的內(nèi)存區(qū)域,因此也常被稱為“GC 堆”。
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的,這點(diǎn)就像我們用磁盤空間去存儲文件一樣,并不要求每個文件都連續(xù)存放。但對于大 對象(典型的如數(shù)組對象),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于實(shí)現(xiàn)簡單、存儲高效的考慮,很可能會要求連續(xù)的內(nèi)存空間。
Java 堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過參數(shù)-Xmx和-Xms設(shè)定)。如果在 Java 堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時,Java 虛擬機(jī)將會拋出 OutOfMemoryError 異常。
Java 堆的唯一作用就是存放對象實(shí)例,這也是垃圾收集器最關(guān)注的內(nèi)存區(qū)域,因?yàn)榇蠖鄶?shù)對象實(shí)例的存活時間都很短,比如在方法內(nèi)部創(chuàng)建的實(shí)例在方法執(zhí)行完之后就沒有存在價值了,所以這個區(qū)域的垃圾回收性價比最高。關(guān)于垃圾回收的詳細(xì)內(nèi)容,見后續(xù)文章。
2.5 方法區(qū) (Method Area)
方法區(qū) (Method Area)與 Java 堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載 的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫作“非堆”(Non-Heap),目的是與 Java 堆區(qū)分開來。
說到方法區(qū),不得不提一下“永久代”這個概念,尤其是在JDK 8以前,許多 Java 程序員都習(xí)慣在 HotSpot 虛擬機(jī)上開發(fā)、部署程序,很多人都更愿意把方法區(qū)稱呼為“永久代”(Permanent Generation),或?qū)烧呋鞛橐徽劇1举|(zhì)上這兩者并不是等價的,因?yàn)閮H僅是當(dāng)時的 HotSpot 虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把收集器的分代設(shè)計(jì)擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣使得 HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。但是對于其他虛擬機(jī)實(shí)現(xiàn),譬如 BEA JRockit、IBM J9 等來說,是不存在永久代的概念的。原則上如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受《Java虛擬機(jī)規(guī)范》管束,并不要求統(tǒng)一。但現(xiàn)在回頭來看,當(dāng)年使用永久代來實(shí)現(xiàn)方法區(qū)的決定并不是一個好主意,這種設(shè)計(jì)導(dǎo)致了 Java 應(yīng)用更容易遇到 內(nèi)存溢出的問題(永久代有-XX:M axPermSize 的上限,即使不設(shè)置也有默認(rèn)大小,而 J9 和 JRockit 只要沒有觸碰到進(jìn)程可用內(nèi)存的上限,例如32位系統(tǒng)中的4GB限制,就不會出問題 ),而且有極少數(shù)方法 (例如 String :: intern() ) 會因永久代的原因而導(dǎo)致不同虛擬機(jī)下有不同的表現(xiàn)。當(dāng) Oracle 收購 BEA 獲得了 JRockit 的所有權(quán)后,準(zhǔn)備把 JRockit 中的優(yōu)秀功能,譬如 Java Mission Control 管理工具,移植到 HotSpot 虛擬機(jī)時,但因?yàn)閮烧邔Ψ椒▍^(qū)實(shí)現(xiàn)的差異而面臨諸多困難。考慮到 HotSpot 未來的發(fā)展,在 JDK 6 的 時候 HotSpot 開發(fā)團(tuán)隊(duì)就有放棄永久代,逐步改為采用本地內(nèi)存 (Native Memory) 來實(shí)現(xiàn)方法區(qū)的計(jì)劃了,到了JDK 7 的 HotSpot,已經(jīng)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,而到了 JDK 8,終于完全廢棄了永久代的概念,改用與 JRockit、J9 一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Metaspace)來代替,把JDK 7中永久代還剩余的內(nèi)容(主要是類型信息)全部移到元空間中。
《Java虛擬機(jī)規(guī)范》對方法區(qū)的約束是非常寬松的,除了和 Java 堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,甚至還可以選擇不實(shí)現(xiàn)垃圾收集。相對而言,垃圾收集行為在這個區(qū)域的確是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載,一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收有時又確實(shí)是必要的。
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時,將拋出 OutOfMemoryError 異常。
這部分引用內(nèi)容對方法區(qū)的介紹十分全面,切記不要將方法區(qū)和永久代混為一談,從JDK 8 以后已經(jīng)沒有永久代的概念了。
2.6 運(yùn)行時常量池 (Runtime Constant Pool)
運(yùn)行時常量池 (Runtime Constant Pool) 是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表 (Constant Pool Table),用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時常量池中。
既然運(yùn)行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請到內(nèi)存 時會拋出OutOfMemoryError異常。
常量池是為了避免頻繁的創(chuàng)建和銷毀對象而影響系統(tǒng)性能,其實(shí)現(xiàn)了對象的共享。
總結(jié)
本文作為 Java 虛擬機(jī)系列的第一篇文章,為大家介紹了 Java 虛擬機(jī)的整體架構(gòu)和運(yùn)行時數(shù)據(jù)區(qū),相信大家對 JVM 已經(jīng)有了整體的認(rèn)識。但這還遠(yuǎn)遠(yuǎn)不夠,JVM 還有更多而內(nèi)容和細(xì)節(jié)等著我們?nèi)ヌ剿鳎罄m(xù)文章敬請期待。
最后是參考文章和文獻(xiàn):
- 周志明《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐》(強(qiáng)烈推薦)
- Java虛擬機(jī)規(guī)范
- Java內(nèi)存區(qū)域(運(yùn)行時數(shù)據(jù)區(qū)域)和內(nèi)存模型(JMM
- Architecture of JVM Java Virtual Machine
- 編譯型與解釋型的區(qū)別
- JVM總括三-字節(jié)碼、字節(jié)碼指令、JIT編譯執(zhí)行
- 徹底弄懂java中的常量池