hello 我是寶哥 , 接上一篇文章,我們聊到了
有面試官會(huì)讓你解釋一下Java的內(nèi)存模型,有些人解釋對了,結(jié)果面試官說不對,應(yīng)該是堆啊、棧啊、方法區(qū)什么的(遇到這種面試官,就是你裝逼的時(shí)刻了..)
看完本篇文章你將了解:
- 1.JVM內(nèi)存結(jié)構(gòu)
- 2.JVM棧幀剖析
- 3.方法區(qū)在JDK1.7和1.8中的區(qū)別
- 4.堆分代結(jié)構(gòu)
建議收藏!
JVM內(nèi)存結(jié)構(gòu)
首先JVM內(nèi)存結(jié)構(gòu)
和JAVA內(nèi)存模型
是兩個(gè)概念.
JVM內(nèi)存結(jié)構(gòu):
Class文件通過類加載機(jī)制 加載到內(nèi)存空間,JVM內(nèi)存結(jié)構(gòu)
就是上圖中內(nèi)存空間,Java內(nèi)存模型,則是另外的一個(gè)概念.
根據(jù)《Java 虛擬機(jī)規(guī)范(Java SE 7 版)》規(guī)定,Java 虛擬機(jī)所管理的內(nèi)存如下圖所示。
從上圖可看出,運(yùn)行時(shí)數(shù)據(jù)區(qū)分為
五大區(qū)域
。這些區(qū)域各有各的用途,其中方法區(qū)和堆是所有線程共享
的,棧,本地方法棧和程序計(jì)數(shù)器則為線程私有
的。
1.程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊很小的內(nèi)存空間,它是線程私有的,可以認(rèn)作為當(dāng)前線程的行號(hào)指示器。
為什么要程序計(jì)數(shù)器呢
因?yàn)镃PU會(huì)在多個(gè)線程中切換上下文,需要使用程序計(jì)數(shù)器紀(jì)錄當(dāng)前線程運(yùn)行到哪一行了
,等待線程重新獲取到運(yùn)行時(shí)間時(shí),繼續(xù)從計(jì)數(shù)的位置往下執(zhí)行
.至于它是線程私有的,是因?yàn)槊總€(gè)線程都需要獨(dú)立計(jì)數(shù),各個(gè)線程之間不會(huì)產(chǎn)生影響.
面試題:程序計(jì)數(shù)器會(huì)發(fā)生OutOfMemoryError嗎?
答:這塊內(nèi)存區(qū)域是虛擬機(jī)規(guī)范中唯一沒有OutOfMemoryError的區(qū)域。如果線程執(zhí)行的是個(gè)java方法,那么計(jì)數(shù)器記錄虛擬機(jī)字節(jié)碼指令的地址。如果為native方法,那么計(jì)數(shù)器為空。
2.Java棧(JVM線程棧)
首先我們要記住,棧描述的是方法執(zhí)行的內(nèi)存模型,它是線程私有的.
每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表,操作棧,動(dòng)態(tài)鏈接,方法出口等信息。每一個(gè)方法被調(diào)用的過程就對應(yīng)一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程,如下圖:
面試題:Java虛擬機(jī)棧可能出現(xiàn)哪兩種類型的異常?
- 1 虛擬機(jī)棧是一個(gè)棧,當(dāng)我們的棧幀超過最大深度時(shí),會(huì)拋出StackOverflowError
- 2 棧無法申請到足夠的空間時(shí),拋出OutOfMemoryError異常
棧幀(Stack Frame)
每一個(gè)方法從調(diào)用至執(zhí)行完成的過程,都對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里從入棧到出棧的過程,那么一個(gè)棧幀包含什么?
局部變量表(Local Variable Table)
也叫本地變量表
,它所需要的內(nèi)存空間在編譯期完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法在棧中需要分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變,它的最小單位為Slot
,一個(gè)Slot可以存放一個(gè)32位以內(nèi)的數(shù)據(jù)類型
.虛擬機(jī)通過索引定位的方法查找相應(yīng)的局部變量,索引的范圍是從0~局部變量表最大容量
。如果Slot是32位的,則遇到一個(gè)64位數(shù)據(jù)類型的變量(如long或double型),則會(huì)使用兩個(gè)連續(xù)的Slot
來存儲(chǔ)操作數(shù)棧(Operand Stack)
也常稱為操作棧
,它是一個(gè)后入先出棧(LIFO)。當(dāng)一個(gè)方法剛剛開始執(zhí)行時(shí),其操作數(shù)棧是空的,隨著方法執(zhí)行和字節(jié)碼指令的執(zhí)行,會(huì)從局部變量表或?qū)ο髮?shí)例的字段中復(fù)制常量或變量寫入到操作數(shù)棧,再隨著計(jì)算的進(jìn)行將棧中元素出棧到局部變量表或者返回給方法調(diào)用者,也就是出棧/入棧操作。一個(gè)完整的方法執(zhí)行期間往往包含多個(gè)這樣出棧/入棧的過程。動(dòng)態(tài)連接(Dynamic Linking)
在一個(gè)class文件中,一個(gè)方法要調(diào)用其他方法,需要將這些方法的符號(hào)引用轉(zhuǎn)化為其在內(nèi)存地址中的直接引用,而符號(hào)引用存在于方法區(qū)中的運(yùn)行時(shí)常量池。
Java虛擬機(jī)棧中,每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧所屬方法的符號(hào)引用,持有這個(gè)引用的目的是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)。
這些符號(hào)引用一部分會(huì)在類加載階段或者第一次使用時(shí)就直接轉(zhuǎn)化為直接引用,這類轉(zhuǎn)化稱為靜態(tài)解析
。另一部分將在每次運(yùn)行期間轉(zhuǎn)化為直接引用,這類轉(zhuǎn)化稱為動(dòng)態(tài)連接
。-
方法返回地址(Return address)
一般方法執(zhí)行時(shí),有2種方式會(huì)退出該方法- 1.
正常退出
正常退出指方法正常完成并退出,沒有拋出任何異常,當(dāng)前方法正常完成,則根據(jù)當(dāng)前方法返回的字節(jié)碼指令,這時(shí)有可能會(huì)有返回值傳遞給方法調(diào)用者(調(diào)用它的方法)
,或者無返回值(void)
。具體是否有返回值以及返回值的數(shù)據(jù)類型將根據(jù)該方法返回的字節(jié)碼指令確定。 - 2.
異常退出
方法執(zhí)行過程中遇到異常,并且這個(gè)異常在方法體內(nèi)部沒有得到處理,導(dǎo)致方法退出,只要在本方法的異常表中沒有搜索到相應(yīng)的異常處理器,就會(huì)導(dǎo)致方法退出。
方法退出過程實(shí)際上就等同于把當(dāng)前棧幀出棧,因此退出可以執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓如調(diào)用者的操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后的下一條指令。
一般來說,方法正常退出時(shí),調(diào)用者的PC計(jì)數(shù)值可以作為返回地址,棧幀中可能保存此計(jì)數(shù)值。而方法異常退出時(shí),返回地址是通過異常處理器表確定的,棧幀中一般不會(huì)保存此部分信息 - 1.
附加信息
指的是在虛擬機(jī)實(shí)現(xiàn)中加入了一些規(guī)范里沒有描述的信息到棧幀之中,例如與調(diào)試相關(guān)的信息。
本地方法棧
本地方法棧是與虛擬機(jī)棧發(fā)揮的作用十分相似,區(qū)別是虛擬機(jī)棧執(zhí)行的是Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的native方法服務(wù),可能底層調(diào)用的c或者c++,我們打開jdk安裝目錄可以看到也有很多用c編寫的文件,可能就是native方法所調(diào)用的c代碼。
java是跨平臺(tái)的語言,既然是跨了平臺(tái),所付出的代價(jià)就是犧牲一些對底層的控制,而java要實(shí)現(xiàn)對底層的控制,就需要一些其他語言的幫助,這個(gè)就是native的作用了.
堆
它是所有線程共享的,它的目的是存放對象實(shí)例。同時(shí)它也是GC所管理的主要區(qū)域,因此常被稱為GC堆,現(xiàn)在收集器常使用分代算法進(jìn)行垃圾回收,這里是垃圾回收重災(zāi)區(qū).
堆分代結(jié)構(gòu)
堆被劃分成兩個(gè)不同的區(qū)域:新生代 ( Young )、老年代 ( Old )。
新生代 ( Young ) 又被劃分為三個(gè)區(qū)域:Eden、From Survivor、To Survivor。它們的默認(rèn)比例關(guān)系如下圖:
新生代 (Young Generation)
新生成的對象優(yōu)先存放在新生代中,新生代對象朝生夕死,存活率很低,在新生代中常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70%~95%的空間,回收效率很高幸存者區(qū) (Survivor)
幸存者區(qū)有2塊, From Survivor區(qū), To Survivor區(qū),也稱S0,S1區(qū).被面試官問了不要懵,GC進(jìn)行時(shí),Eden區(qū)中所有存活的對象都會(huì)被復(fù)制到 To Survivor區(qū),而在FromSurvivor區(qū)中,仍存活的對象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閥值(默認(rèn)為15,新生代中的對象毎熬過一輪垃圾回收,年齡值就加1,GC分代年齡存儲(chǔ)在對象的 header中)的對象會(huì)被移到老年代中,沒有達(dá)到閥值的對象會(huì)被復(fù)制到 To Survivor區(qū)。接著清空Eden區(qū)和 From Survivor區(qū),新生代中存活的對象都在 To Survivor區(qū)。接著,F(xiàn)rom Survivor區(qū)和 To Survivor區(qū)會(huì)交換它們的角色,GC時(shí)當(dāng) To Survivor區(qū)沒有足夠的空間存放上一次新生代收集下來的存活對象時(shí),需要依賴?yán)夏甏M(jìn)行分配擔(dān)保,將這些對象存放在老年代中.老年代(Old)
在新生代中經(jīng)歷了多次(具體看虛擬機(jī)配置的值)GC后仍然存活下來的對象會(huì)進(jìn)入老年代中。老年代中的對象生命周期較長,存活率比較高,在老年代中進(jìn)行GC的頻率相對而言較低,而且回收的速度也比較慢
常見面試題:
-
何時(shí)發(fā)生Minor GC/Young GC ?
新生成的對象在Eden區(qū)分配(大對象除外,大對象直接進(jìn)入老年代),當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC
也叫Young GC
-
為什么分代?
將對象根據(jù)存活概率進(jìn)行分類,對存活時(shí)間長的對象,放到固定區(qū),從而減少掃描垃圾時(shí)間及GC頻率。針對分類進(jìn)行不同的垃圾回收算法,對算法揚(yáng)長避短
方法區(qū)
從這張圖可以看到JDK1.8和JDK1.7相比最大的區(qū)別是:
元空間區(qū)取代了永久代
,永久代原本主要存放Class
和Meta
的信息。而元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存
。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。
上圖中我們可以看到,JDK1.7和1.8對于運(yùn)行時(shí)數(shù)據(jù)區(qū)和堆中的方法區(qū)都做了調(diào)整,jdk8中引入了一個(gè)新的內(nèi)存區(qū)域叫metaspace。并不是所有的jvm中都有永久代,IBM的J9,oracle的JRocket都沒有永久代,永久代是實(shí)現(xiàn)層面的東西,永久代里面存的東西基本上就是方法區(qū)規(guī)定的那些東西。
面試題
: 方法區(qū)、永久代、元空間的區(qū)別?
-
方法區(qū)
:
是JVM的規(guī)范,所有虛擬機(jī)必須遵守的。是JVM 所有線程共享的、用于存儲(chǔ)類的信息、常量池、方法數(shù)據(jù)、方法代碼等。 -
永久代
:
全稱Permanent Generation space ,是指內(nèi)存的永久保存區(qū)域。是 HotSpot 虛擬機(jī)基于JVM規(guī)范
對方法區(qū)的一個(gè)落地實(shí)現(xiàn),并且只有 HotSpot 才有 PermGen space,在JDK8被移除了. -
元空間
:
元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。它也是基于JVM規(guī)范
對方法區(qū)的一個(gè)落地實(shí)現(xiàn)
JDK6、JDK7 時(shí),方法區(qū) 就是 PermGen(永久代)。
JDK8 時(shí),方法區(qū)就是 Metaspace(元空間)
在不同版本的JDK中
堆溢出的實(shí)例:
面試題
:為什么去除了永久代:
1)字符串存在永久代中,容易出現(xiàn)性能問題和內(nèi)存溢出。
2)類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出。
3)永久代會(huì)為 GC 帶來不必要的復(fù)雜度,并且回收效率偏低。
結(jié)語
經(jīng)過本篇學(xué)習(xí),我們已經(jīng)知道JVM對于內(nèi)存的管理,以及它的結(jié)構(gòu),
千萬不要搞混 JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)和JMM(Java memory modle)的關(guān)系
而JAVA對象布局在我的一篇文章中有講過他們是不同的概念
JAVA對象布局
下一篇我們來研究JMM(java的內(nèi)存模型)和Java中的逃逸分析,以及多線程編程延伸.
請持續(xù)關(guān)注公眾號(hào):JAVA寶典
關(guān)注公眾號(hào):java寶典