深入理解Java虛擬機(jī):方法區(qū)詳解
前言
本節(jié)主要講的是運(yùn)行時(shí)數(shù)據(jù)區(qū)(方法區(qū)),也就是下圖這部分,它是在類(lèi)加載完成后的階段:
圖片
- 每個(gè)線(xiàn)程:獨(dú)立包括程序計(jì)數(shù)器、棧、本地棧
- 線(xiàn)程間共享:堆、堆外內(nèi)存(永久代或元空間、代碼緩存)
當(dāng)我們通過(guò)前面的:類(lèi)的加載-> 驗(yàn)證 -> 準(zhǔn)備 -> 解析 -> 初始化 這幾個(gè)階段完成后,就會(huì)用到執(zhí)行引擎對(duì)我們的類(lèi)進(jìn)行使用,同時(shí)執(zhí)行引擎將會(huì)使用到我們運(yùn)行時(shí)數(shù)據(jù)區(qū)。
內(nèi)存是非常重要的系統(tǒng)資源,是硬盤(pán)和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ī)制存在著部分差異。
正文
我們通過(guò)磁盤(pán)或者網(wǎng)絡(luò)IO得到的數(shù)據(jù),都需要先加載到內(nèi)存中,然后CPU從內(nèi)存中獲取數(shù)據(jù)進(jìn)行讀取,也就是說(shuō)內(nèi)存充當(dāng)了CPU和磁盤(pán)之間的橋梁。
圖片
線(xiàn)程
線(xiàn)程是一個(gè)程序里的運(yùn)行單元。JVM允許一個(gè)應(yīng)用有多個(gè)線(xiàn)程并行的執(zhí)行。在Hotspot JVM里,每個(gè)線(xiàn)程都與操作系統(tǒng)的本地線(xiàn)程直接映射。
當(dāng)一個(gè)Java線(xiàn)程準(zhǔn)備好執(zhí)行以后,此時(shí)一個(gè)操作系統(tǒng)的本地線(xiàn)程也同時(shí)創(chuàng)建。Java線(xiàn)程執(zhí)行終止后,本地線(xiàn)程也會(huì)回收。
操作系統(tǒng)負(fù)責(zé)所有線(xiàn)程的安排調(diào)度到任何一個(gè)可用的CPU上。一旦本地線(xiàn)程初始化成功,它就會(huì)調(diào)用Java線(xiàn)程中的run()方法。
JVM系統(tǒng)線(xiàn)程:
- 虛擬機(jī)線(xiàn)程:需要JVM達(dá)到安全點(diǎn)才會(huì)出現(xiàn)。這些操作必須在不同的線(xiàn)程中發(fā)生的,原因是他們都需要JVM達(dá)到安全點(diǎn),這樣堆才不會(huì)變化。這種線(xiàn)程的執(zhí)行類(lèi)型包括stop-the-world的垃圾收集,線(xiàn)程棧收集,線(xiàn)程掛起以及偏向鎖撤銷(xiāo)。
- 周期任務(wù)線(xiàn)程:這種線(xiàn)程是時(shí)間周期事件的體現(xiàn)(比如中斷),他們一般用于周期性操作的調(diào)度執(zhí)行。
- GC線(xiàn)程:這種線(xiàn)程對(duì)在JVM里不同種類(lèi)的垃圾收集行為提供了支持。
- 編譯線(xiàn)程:這種線(xiàn)程在運(yùn)行時(shí)會(huì)將字節(jié)碼編譯成到本地代碼。
- 信號(hào)調(diào)度線(xiàn)程:這種線(xiàn)程接收信號(hào)并發(fā)送給JVM,在它內(nèi)部通過(guò)調(diào)用適當(dāng)?shù)姆椒ㄟM(jìn)行處理。
方法區(qū)
棧、堆、方法區(qū)的交互關(guān)系
圖片
盡管所有的方法區(qū)在邏輯上是屬于堆的一部分,但一些簡(jiǎn)單的實(shí)現(xiàn)可能不會(huì)選擇去進(jìn)行垃圾收集或者進(jìn)行壓縮。但對(duì)于HotSpotJVM而言,方法區(qū)還有一個(gè)別名叫做Non-Heap(非堆),目的就是要和堆分開(kāi),所以方法區(qū)看作是一塊獨(dú)立于Java堆的內(nèi)存空間。
方法區(qū)基本理解
- 方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域。
- 方法區(qū)在JVM啟動(dòng)的時(shí)候被創(chuàng)建,并且它的實(shí)際的物理內(nèi)存空間中和Java堆區(qū)一樣都可以是不連續(xù)的。
- 方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或者可擴(kuò)展。
- 方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類(lèi),如果系統(tǒng)定義了太多的類(lèi),導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤:java.lang.OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace
- 加載大量的第三方的jar包;Tomcat部署的工程過(guò)多(30~50個(gè));大量動(dòng)態(tài)的生成反射類(lèi)
- 關(guān)閉JVM就會(huì)釋放這個(gè)區(qū)域的內(nèi)存。
方法區(qū)的演進(jìn)
在jdk7及以前,習(xí)慣上把方法區(qū),稱(chēng)為永久代。jdk8開(kāi)始,使用元空間取代了永久代
圖片
JDK8完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Metaspace)來(lái)代替
圖片
元空間的本質(zhì)和永久代類(lèi)似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代最大的區(qū)別在于:元空間不在虛擬機(jī)設(shè)置的內(nèi)存中,而是使用本地內(nèi)存。
設(shè)置方法區(qū)內(nèi)存的大小
jdk7及以前:
- 通過(guò)-XX:Permsize來(lái)設(shè)置永久代初始分配空間。默認(rèn)值是20.75M
- 通過(guò)-XX:MaxPermsize來(lái)設(shè)定永久代最大可分配空間。32位機(jī)器默認(rèn)是64M,64位機(jī)器模式是82M
圖片
jdk8及以后:
- 元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
- -XX:MetaspaceSize設(shè)置初始的元空間大小。對(duì)于一個(gè)64位的服務(wù)器端JVM來(lái)說(shuō),其默認(rèn)的-XX:MetaspaceSize值為21MB,這就是初始的高水位線(xiàn),一旦觸及這個(gè)水位線(xiàn),F(xiàn)ull GC將會(huì)被觸發(fā)并卸載沒(méi)用的類(lèi)(即這些類(lèi)對(duì)應(yīng)的類(lèi)加載器不再存活),然后這個(gè)高水位線(xiàn)將會(huì)重置。新的高水位線(xiàn)的值取決于GC后釋放了多少元空間。如果釋放的空間不足,那么在不超過(guò)MaxMetaspaceSize時(shí),適當(dāng)提高該值。如果釋放空間過(guò)多,則適當(dāng)降低該值。
方法區(qū)的內(nèi)部結(jié)構(gòu)
圖片
方法區(qū)存儲(chǔ)什么
它用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等
圖片
類(lèi)型信息,對(duì)每個(gè)加載的類(lèi)型(類(lèi)class、接口interface、枚舉enum、注解annotation),JVM必須在方法區(qū)中存儲(chǔ)以下類(lèi)型信息:
- 這個(gè)類(lèi)型的完整有效名稱(chēng)(全名=包名.類(lèi)名)
- 這個(gè)類(lèi)型直接父類(lèi)的完整有效名(對(duì)于interface或java.lang.Object,都沒(méi)有父類(lèi))
- 這個(gè)類(lèi)型的修飾符(public,abstract,final的某個(gè)子集)
- 這個(gè)類(lèi)型直接接口的一個(gè)有序列表
域信息,JVM必須在方法區(qū)中保存類(lèi)型的所有域的相關(guān)信息以及域的聲明順序:
- 域的相關(guān)信息包括:域名稱(chēng)、域類(lèi)型、域修飾符(public,private,protected,static,final,volatile,transient的某個(gè)子集)
方法信息,JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
- 方法名稱(chēng)
- 方法的返回類(lèi)型(或void)
- 方法參數(shù)的數(shù)量和類(lèi)型(按順序)
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個(gè)子集)
- 方法的字節(jié)碼(bytecodes)、操作數(shù)棧、局部變量表及大小(abstract和native方法除外)
- 異常表(abstract和native方法除外)
每個(gè)異常處理的開(kāi)始位置、結(jié)束位置、代碼處理在程序計(jì)數(shù)器中的偏移地址、被捕獲的異常類(lèi)的常量池索引
類(lèi)變量:
- 靜態(tài)變量和類(lèi)關(guān)聯(lián)在一起,隨著類(lèi)的加載而加載,他們成為類(lèi)數(shù)據(jù)在邏輯上的一部分
- 類(lèi)變量被類(lèi)的所有實(shí)例共享,即使沒(méi)有類(lèi)實(shí)例時(shí),你也可以訪(fǎng)問(wèn)它
全局常量:
- 被聲明為final的類(lèi)變量的處理方法則不同,每個(gè)全局常量在編譯的時(shí)候就會(huì)被分配了
常量池
- 字節(jié)碼文件,內(nèi)部包含了常量池(數(shù)量值、字符串值、類(lèi)引用、字段引用、方法引用)
圖片
一個(gè)有效的字節(jié)碼文件中除了包含類(lèi)的版本信息、字段、方法以及接口等描述符信息外,還包含一項(xiàng)信息就是常量池表(Constant Pool Table),包括各種字面量和對(duì)類(lèi)型、域和方法的符號(hào)引用。
一個(gè)Java源文件中的類(lèi)、接口,編譯后產(chǎn)生一個(gè)字節(jié)碼文件。而Java中的字節(jié)碼需要數(shù)據(jù)支持,通常這種數(shù)據(jù)會(huì)很大以至于不能直接存到字節(jié)碼里,換另一種方式,可以存到常量池,這個(gè)字節(jié)碼包含了指向常量池的引用,在動(dòng)態(tài)鏈接的時(shí)候會(huì)用到運(yùn)行時(shí)常量池。
常量池可以看做是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類(lèi)名、方法名、參數(shù)類(lèi)型、字面量等類(lèi)型。
運(yùn)行時(shí)常量池
- 運(yùn)行時(shí)常量池是方法區(qū)的一部分。
- 常量池表是Class文件的一部分,用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
- 運(yùn)行時(shí)常量池,在加載類(lèi)和接口到虛擬機(jī)后,就會(huì)創(chuàng)建對(duì)應(yīng)的運(yùn)行時(shí)常量池。
- JVM為每個(gè)已加載的類(lèi)型(類(lèi)或接口)都維護(hù)一個(gè)常量池。池中的數(shù)據(jù)項(xiàng)像數(shù)組項(xiàng)一樣,是通過(guò)索引訪(fǎng)問(wèn)的。
- 運(yùn)行時(shí)常量池中包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運(yùn)行期解析后才能夠獲得的方法或者字段引用。此時(shí)不再是常量池中的符號(hào)地址了,這里換為真實(shí)地址。
- 運(yùn)行時(shí)常量池,相對(duì)于Class文件常量池的另一重要特征是:具備動(dòng)態(tài)性。
- 運(yùn)行時(shí)常量池類(lèi)似于傳統(tǒng)編程語(yǔ)言中的符號(hào)表(symboltable),但是它所包含的數(shù)據(jù)卻比符號(hào)表要更加豐富一些。
- 當(dāng)創(chuàng)建類(lèi)或接口的運(yùn)行時(shí)常量池時(shí),如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過(guò)了方法區(qū)所能提供的最大值,則JVM會(huì)拋OutOfMemoryError異常。
方法區(qū)使用舉例
public class MethodAreaDemo {
public static void main(String args[]) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a+b);
}
}
圖片
詳細(xì)執(zhí)行過(guò)程
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
圖片
方法區(qū)的演進(jìn)細(xì)節(jié)
jdk1.6:
圖片
jdk1.7:
圖片
jdk1.8:
圖片
StringTable為什么要調(diào)整位置
jdk7中將StringTable放到了堆空間中。因?yàn)橛谰么幕厥招屎艿?,在full gc的時(shí)候才會(huì)觸發(fā)。而full gc是老年代的空間不足、永久代不足時(shí)才會(huì)觸發(fā)。
這就導(dǎo)致StringTable回收效率不高。而我們開(kāi)發(fā)中會(huì)有大量的字符串被創(chuàng)建,回收效率低,導(dǎo)致永久代內(nèi)存不足。放到堆里,能及時(shí)回收內(nèi)存。