《Java虛擬機(jī)規(guī)范》中明確說(shuō)明:“盡管所有的方法區(qū)在邏輯上是屬于堆的一部分电湘,但一些簡(jiǎn)單的實(shí)現(xiàn)可能不會(huì)選擇去進(jìn)行垃圾收集或者進(jìn)行壓縮。”但對(duì)于 HotSpotJVM 而言,方法區(qū)還有一個(gè)別名叫做Non-Heap(非堆)亡哄,目的就是要和堆分開。
所以布疙,方法區(qū)看作是一塊獨(dú)立于Java堆的內(nèi)存空間蚊惯。
方法區(qū)主要存放的是『Class』,而堆中主要存放的是『實(shí)例化的對(duì)象』
方法區(qū)(Method Area)與Java堆一樣灵临,是各個(gè)線程共享的內(nèi)存區(qū)域截型。
方法區(qū)在JVM啟動(dòng)的時(shí)候被創(chuàng)建,并且它的實(shí)際的物理內(nèi)存空間中和Java堆區(qū)一樣都可以是不連續(xù)的儒溉。
方法區(qū)的大小宦焦,跟堆空間一樣,可以選擇固定大小或者可擴(kuò)展顿涣。
方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類波闹,如果系統(tǒng)定義了太多的類,導(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)的生成反射類
關(guān)閉JVM就會(huì)釋放這個(gè)區(qū)域的內(nèi)存舔痪。
1.HotSpot中方法區(qū)的演進(jìn)
在jdk7及以前寓调,習(xí)慣上把方法區(qū)锌唾,稱為永久代。jdk8開始夺英,使用元空間取代了永久代晌涕。
JDK 1.8后,元空間存放在堆外內(nèi)存中痛悯。
本質(zhì)上余黎,方法區(qū)和永久代并不等價(jià)。僅是對(duì)hotspot而言的载萌【宀疲《Java虛擬機(jī)規(guī)范》對(duì)如何實(shí)現(xiàn)方法區(qū)巡扇,不做統(tǒng)一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念垮衷。
現(xiàn)在來(lái)看厅翔,當(dāng)年使用永久代,不是好的idea搀突。導(dǎo)致Java程序更容易o(hù)om(超過(guò)-XX:MaxPermsize上限)
而到了JDK8刀闷,終于完全廢棄了永久代的概念,改用與JRockit仰迁、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Metaspace)來(lái)代替:
元空間的本質(zhì)和永久代類似甸昏,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代最大的區(qū)別在于:元空間不在虛擬機(jī)設(shè)置的內(nèi)存中徐许,而是使用本地內(nèi)存施蜜。
永久代、元空間二者并不只是名字變了雌隅,內(nèi)部結(jié)構(gòu)也調(diào)整了花墩。
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無(wú)法滿足新的內(nèi)存分配需求時(shí)澄步,將拋出OOM異常冰蘑。
2.方法區(qū)內(nèi)部結(jié)構(gòu)
《深入理解Java虛擬機(jī)》書中對(duì)方法區(qū)(Method Area)存儲(chǔ)內(nèi)容描述如下:它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量村缸、靜態(tài)變量祠肥、即時(shí)編譯器編譯后的代碼緩存等。
這里我們重點(diǎn)關(guān)注掌握運(yùn)行時(shí)常量池
3.運(yùn)行時(shí)常量池
方法區(qū)梯皿,內(nèi)部包含了運(yùn)行時(shí)常量池
要弄清楚方法區(qū)的運(yùn)行時(shí)常量池仇箱,需要理解清楚classFile中的常量池
-
常量池
一個(gè)有效的字節(jié)碼文件中除了包含類的版本信息、字段东羹、方法以及接口等描述符信息外剂桥,還包含一項(xiàng)信息就是常量池表(Constant Pool Table),包括各種字面量和對(duì)類型属提、域和方法的符號(hào)引用
常量池权逗、可以看做是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類名冤议、方法名斟薇、參數(shù)類型、字面量 等類型恕酸。
-
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分堪滨。
常量池表(Constant Pool Table)是Class文件的一部分,用于存放編譯期生成的各種字面量與符號(hào)引用蕊温,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中袱箱。
運(yùn)行時(shí)常量池遏乔,在加載類和接口到虛擬機(jī)后,就會(huì)創(chuàng)建對(duì)應(yīng)的運(yùn)行時(shí)常量池发笔。
JVM為每個(gè)已加載的類型(類或接口)都維護(hù)一個(gè)常量池按灶。池中的數(shù)據(jù)項(xiàng)像數(shù)組項(xiàng)一樣,是通過(guò)索引訪問的筐咧。
運(yùn)行時(shí)常量池中包含多種不同的常量鸯旁,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運(yùn)行期解析后才能夠獲得的方法或者字段引用量蕊。此時(shí)不再是常量池中的符號(hào)地址了铺罢,這里換為真實(shí)地址。
運(yùn)行時(shí)常量池残炮,相對(duì)于Class文件常量池的另一重要特征是:具備動(dòng)態(tài)性韭赘。
運(yùn)行時(shí)常量池類似于傳統(tǒng)編程語(yǔ)言中的符號(hào)表(symboltable),但是它所包含的數(shù)據(jù)卻比符號(hào)表要更加豐富一些势就。
當(dāng)創(chuàng)建類或接口的運(yùn)行時(shí)常量池時(shí)泉瞻,如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過(guò)了方法區(qū)所能提供的最大值,則JVM會(huì)拋outofMemoryError異常苞冯。
4.方法區(qū)的演進(jìn)細(xì)節(jié)
首先明確:只有Hotspot才有永久代袖牙。BEA JRockit、IBM J9等來(lái)說(shuō)舅锄,是不存在永久代的概念的鞭达。原則上如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受《Java虛擬機(jī)規(guī)范》管束皇忿,并不要求統(tǒng)一畴蹭。
Hotspot中方法區(qū)的變化:
版本 | 內(nèi)容 |
---|---|
JDK1.6及以前 | 有永久代,靜態(tài)變量存儲(chǔ)在永久代上 |
JDK1.7 | 有永久代鳍烁,但已經(jīng)逐步 “去永久代”叨襟,字符串常量池,靜態(tài)變量移除幔荒,保存在堆中 |
JDK1.8 | 無(wú)永久代糊闽,類型信息,字段铺峭,方法墓怀,常量保存在本地內(nèi)存的元空間汽纠,但字符串常量池卫键、靜態(tài)變量仍然在堆中。 |
JDK6的時(shí)候:
JDK7的時(shí)候:
JDK8的時(shí)候虱朵,元空間大小只受物理內(nèi)存影響:
5.StringTable
StringTable叫做字符串常量池莉炉,用于存放字符串常量钓账,這樣當(dāng)我們使用相同的字符串對(duì)象時(shí),就可以直接從StringTable中獲取而不用重新創(chuàng)建對(duì)象絮宁。
StringTable為什么要調(diào)整位置
jdk7中將StringTable放到了堆空間中梆暮。因?yàn)橛谰么幕厥招屎艿停趂ull gc的時(shí)候才會(huì)觸發(fā)绍昂。而full gc是老年代的空間不足啦粹、永久代不足時(shí)才會(huì)觸發(fā)。
這就導(dǎo)致stringTable回收效率不高窘游。而我們開發(fā)中會(huì)有大量的字符串被創(chuàng)建唠椭,回收效率低,導(dǎo)致永久代內(nèi)存不足忍饰。放到堆里贪嫂,能及時(shí)回收內(nèi)存。
關(guān)于StringTable的深入講解艾蓝、垃圾回收力崇、性能調(diào)優(yōu)以及字符串的特點(diǎn)會(huì)結(jié)合視頻進(jìn)行講解發(fā)放
String相關(guān)的面試題如下:(可以自行思考)
<pre data-tool="mdnice編輯器" style="margin: 10px 0px; padding: 0px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 問
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 問,如果調(diào)換了【最后兩行代碼】的位置呢赢织,如果是jdk1.6呢
System.out.println(x1 == x2);` </pre>
6.JVM內(nèi)存相關(guān)的幾個(gè)核心參數(shù)
在JVM內(nèi)存分配中亮靴,有幾個(gè)參數(shù)是比較核心的,我們總結(jié)歸納在一起:
指令 | 描述 |
---|---|
-Xms | Java堆內(nèi)存的大小 |
-Xmx | Java堆內(nèi)存的最大大小 |
-Xmn | Java堆內(nèi)存中的新生代大小于置,扣除新生代剩下的就是老年代的內(nèi)存大小 |
-XX:PermSize | 永久代大小 |
-XX:MaxPermSize | 永久代最大大小 |
-Xss | 每個(gè)線程的棧內(nèi)存大小 |
7.系統(tǒng)啟動(dòng)的參數(shù)設(shè)置
我們可以在啟動(dòng)某個(gè)類或項(xiàng)目的時(shí)候進(jìn)行VM Options參數(shù)設(shè)置:
比如
運(yùn)行之前測(cè)試過(guò)的代碼台猴,結(jié)果跟設(shè)置一致:
總結(jié)
最后我們通過(guò)一個(gè)詳細(xì)圖對(duì)整個(gè)JVM的內(nèi)存結(jié)構(gòu)做一個(gè)總結(jié):