JVM的基礎(chǔ)知識(shí)點(diǎn)Java的內(nèi)存模型
閱讀文本大概需要3分鐘。
? ? ? Java虛擬機(jī)是Java工程師必學(xué)的進(jìn)階功課,這段時(shí)間開(kāi)始死磕JVM。今天梳理一下JVM的基礎(chǔ)知識(shí)點(diǎn)Java的內(nèi)存模型!

程序計(jì)數(shù)器
是什么:程序計(jì)數(shù)器是很小的一塊內(nèi)存空間,它是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
有什么用:解釋器通過(guò)這個(gè)計(jì)數(shù)器來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令。
存儲(chǔ)什么內(nèi)容:如果線(xiàn)程執(zhí)行的是Java方法,存儲(chǔ)的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果是native方法,計(jì)數(shù)器值為空(undefined)。
為什么是線(xiàn)程私有的:多線(xiàn)程是線(xiàn)程輪流切換并分配處理器執(zhí)行時(shí)間片的方式來(lái)實(shí)現(xiàn)的,在任何確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)就是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線(xiàn)程,所以,為了線(xiàn)程在切換后能恢復(fù)到正確的執(zhí)行位置,每個(gè)線(xiàn)程應(yīng)該獨(dú)立擁有一個(gè)程序計(jì)數(shù)器。
會(huì)出現(xiàn)什么異常情況:唯一一個(gè)無(wú)內(nèi)存溢出異常的區(qū)域。
Java虛擬機(jī)棧
是什么:虛擬機(jī)棧是Java方法的內(nèi)存模型,每一個(gè)Java方法從調(diào)用到執(zhí)行完成就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中的入棧和出棧。
存儲(chǔ)什么內(nèi)容:每個(gè)方法的執(zhí)行就會(huì)創(chuàng)建一個(gè)棧幀,這個(gè)棧幀會(huì)存儲(chǔ)這個(gè)Java方法的局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等信息。
為什么是線(xiàn)程私有的:每個(gè)線(xiàn)程所執(zhí)行的方法可能是不一樣的。
會(huì)出現(xiàn)什么異常情況:如果線(xiàn)程請(qǐng)求的棧深度>虛擬機(jī)允許的深度,拋出棧溢出異常;如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,拋出內(nèi)存溢出異常。
本地方法棧
是什么:本地方法棧的作用和虛擬機(jī)棧非常像是,只不過(guò)本地方法棧是native方法的內(nèi)存模型,每一個(gè)native方法從調(diào)用到執(zhí)行完成就對(duì)應(yīng)著一個(gè)棧幀在本地方法棧中的入棧和出棧。
存儲(chǔ)什么內(nèi)容:同虛擬機(jī)棧。
為什么是線(xiàn)程私有的:同虛擬機(jī)棧。
會(huì)出現(xiàn)什么異常情況:同虛擬機(jī)棧。
Java堆
是什么:Java堆是Java虛擬機(jī)管理的內(nèi)存中最大的一塊,Java堆是在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建的。
存儲(chǔ)什么內(nèi)容:存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這個(gè)內(nèi)存區(qū)域分配內(nèi)存。
為什么是線(xiàn)程共享的:所有的線(xiàn)程都可以訪(fǎng)問(wèn)不同的對(duì)象。其實(shí)從內(nèi)存分配的角度來(lái)看,線(xiàn)程共享的Java堆可能其實(shí)是多個(gè)線(xiàn)程私有的分配緩沖區(qū),不同的線(xiàn)程將各自的對(duì)象實(shí)例放在看似共享的Java堆的各自的緩沖區(qū)上,這樣劃分可以更好的回收內(nèi)存,也可以更好點(diǎn)分配內(nèi)存。
會(huì)出現(xiàn)什么異常情況:Java堆可以處于物理上不連續(xù)的內(nèi)存空間上,但邏輯上一定是連續(xù)的,在堆中沒(méi)有內(nèi)存可以完成對(duì)象實(shí)例的分配,且無(wú)法再擴(kuò)展時(shí),會(huì)拋出內(nèi)存溢出異常。
方法區(qū)
是什么:和堆一樣,是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域。很多人把方法區(qū)稱(chēng)為永久代,但是本質(zhì)上這兩個(gè)不等價(jià),Java虛擬機(jī)將GC分代收集擴(kuò)展至方法區(qū),使用永久代來(lái)實(shí)現(xiàn)方法區(qū),這樣GC收集器就能像管理Java堆一樣管理方法區(qū)而不需要再寫(xiě)一套GC收集來(lái)管理方法區(qū)。當(dāng)然在方法區(qū)里也可以設(shè)置不進(jìn)行GC收集。
存儲(chǔ)什么內(nèi)容:已被虛擬機(jī)加載的類(lèi)信息,類(lèi)常量,類(lèi)的靜態(tài)變量,即時(shí)編譯器編譯后的代碼等。運(yùn)行時(shí)常量池也是方法區(qū)的一部分。
為什么是線(xiàn)程共享的:各個(gè)線(xiàn)程都可以訪(fǎng)問(wèn)虛擬機(jī)加載的類(lèi)。
會(huì)出現(xiàn)什么異常情況:內(nèi)存溢出異常。
直接內(nèi)存
是什么:直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)定義的內(nèi)存區(qū)域,但也經(jīng)常被使用。JDK1.4加入了NIO類(lèi),一種基于通道與緩沖區(qū)的新I/O方式,NIO可以使用native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為直接內(nèi)存的引用來(lái)操作直接內(nèi)存,這樣可以避免在Java堆和native堆來(lái)回復(fù)制數(shù)據(jù),從而提高了性能。
會(huì)出現(xiàn)什么異常情況:受機(jī)器總內(nèi)存的影響,會(huì)出現(xiàn)內(nèi)存溢出異常。
Java虛擬機(jī)中描述了兩種異常:
如果線(xiàn)程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常;
如果在虛擬機(jī)中無(wú)法申請(qǐng)到足夠多的內(nèi)存空間,將拋出OutOfMemoryError異常。
我們都知道Java虛擬機(jī)各個(gè)內(nèi)存區(qū)域(除了程序計(jì)數(shù)器)都有發(fā)生內(nèi)存溢出的可能,但到底什么樣的操作或程序才會(huì)導(dǎo)致內(nèi)存溢出或棧溢出的異常呢?我們分不同的內(nèi)存區(qū)域來(lái)解釋這個(gè)問(wèn)題。
0x01、對(duì)于Java堆內(nèi)存區(qū)域
Java堆中只會(huì)產(chǎn)生OutOfMemoryError異常。
先搞清楚Java堆內(nèi)存放的是什么,還不清楚的可以回顧下這篇文章《死磕JVM-Java內(nèi)存模型》,從這篇文章里我們知道Java堆內(nèi)存存放的是對(duì)象實(shí)例,所以原理上只要我們不斷創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來(lái)避免垃圾回收機(jī)制清楚這些對(duì)象,也就是說(shuō)當(dāng)Eden區(qū)滿(mǎn)的時(shí)候,GC被觸發(fā)時(shí),讓GC誤以為內(nèi)存中的對(duì)象還存活著,那么在對(duì)象數(shù)量達(dá)到最大堆容量限制的時(shí)候就會(huì)產(chǎn)生內(nèi)存溢出的異常。如下代碼就會(huì)產(chǎn)生內(nèi)存溢出的異常:
public?class?堆溢出{
?????static?class?OOMError{}
?????public?static?void?main(String[]?args){
??????????List?list?=new?ArrayList ();
??????????while(true){
???????????????list.add(newOOMError());
??????????}
?????}}
運(yùn)行結(jié)果:
Exceptionin?thread?"main"?java.lang.OutOfMemoryError:Java?heap?space
?????at?java.util.Arrays.copyOf(Arrays.java:3210)
?????at?java.util.Arrays.copyOf(Arrays.java:3181)
?????at?java.util.ArrayList.grow(ArrayList.java:261)
?????at?java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
?????at?java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
?????at?java.util.ArrayList.add(ArrayList.java:458)
?????at?com.intelligentler.jvm.堆溢出.main(堆溢出.java:13)“Java heap space”提示著產(chǎn)生OutOfMemoryError異常的Java虛擬機(jī)的內(nèi)存區(qū)域,也就是Java堆內(nèi)存。
如何解決發(fā)生在Java堆內(nèi)存的OutOfMemoryError異常呢?
首先我們要分清楚產(chǎn)生OutOfMemoryError異常的原因是內(nèi)存泄露還是內(nèi)存溢出,如果內(nèi)存中的對(duì)象確實(shí)都必須存活著而不像上面那樣不斷地創(chuàng)建對(duì)象實(shí)例卻不使用該對(duì)象,則是內(nèi)存溢出,而像上面代碼中的情況則是內(nèi)存泄露。
如果是內(nèi)存泄露,我們可以通過(guò)一些內(nèi)存查看工具來(lái)查看泄露對(duì)象到GC Roots的引用鏈,找到泄露對(duì)象是通過(guò)怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致GC無(wú)法自動(dòng)回收這些泄露對(duì)象,掌握了這些信息,我們就能比較準(zhǔn)確地定位出泄露代碼的位置。
如果不是內(nèi)存泄露,也就是說(shuō)內(nèi)存中的對(duì)象確實(shí)都還必須存活,那么應(yīng)該檢查虛擬機(jī)的堆參數(shù),看看是否還可以將機(jī)器物理內(nèi)存調(diào)大,同時(shí)在代碼上檢查是否存在某些對(duì)象生命周期過(guò)長(zhǎng)、持有狀態(tài)時(shí)間過(guò)長(zhǎng)的情況。
0x02、對(duì)于虛擬機(jī)棧和本地方法棧
在這一部分內(nèi)存區(qū)域,可能產(chǎn)生OutOfMemoryError異常和StackOverflowError異常。
如果定義大量的本地變量,增大此方法幀中本地變量表的長(zhǎng)度或者設(shè)置-Xss參數(shù)減少棧內(nèi)存容量,這兩種操作都會(huì)拋出StackOverflowError異常,如下面的代碼:
public?class?棧溢出{
?????privateint?stackLength?=1;
?????publicvoid?addStackLength(){
??????????stackLength++;
??????????addStackLength();
?????}
?????public?static?void?main(String[]?args)throws?Throwable{
??????????棧溢出?oom?=new?棧溢出();
??????????try{
???????????????oom.addStackLength();
??????????}catch(Throwable?e){
???????????????System.out.println("stack?length:"+?oom.stackLength);
???????????????throw?e;
??????????}
?????}
}運(yùn)行結(jié)果:
stack?length:18388Exceptionin?thread?"main"?java.lang.StackOverflowError
?????at?com.intelligentler.jvm.棧溢出.addStackLength(棧溢出.java:9)
?????at?com.intelligentler.jvm.棧溢出.addStackLength(棧溢出.java:9)
?????at?com.intelligentler.jvm.棧溢出.addStackLength(棧溢出.java:9)所以,如果在單線(xiàn)程的情況下,無(wú)論是棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無(wú)法再分配的時(shí)候,虛擬機(jī)拋出的是StackOverflowError異常。
如果在多線(xiàn)程下,不斷地建立線(xiàn)程可能會(huì)產(chǎn)生OutOfMemoryError異常。
0x03、對(duì)于方法區(qū)
方法區(qū)中只會(huì)產(chǎn)生OutOfMemoryError異常。
由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,我們可以通過(guò)String.intern()方法來(lái)構(gòu)建一個(gè)運(yùn)行時(shí)常量池的OutOfMemoryError異常。
String.intern()是一個(gè)Native方法,它的作用是:如果字符串常量池中已經(jīng)包含了一個(gè)等于該String對(duì)象的字符串,則返回這個(gè)String對(duì)象,否則,將此String對(duì)象包含的字符串添加到常量池中,并返回這個(gè)字符串的String對(duì)象的引用。如下面代碼:
public?class?方法區(qū)溢出{
?????public?static?void?main(String[]?args){
??????????List?list?=newArrayList();
??????????int?i?=0;
??????????while(true){
???????????????list.add(String.valueOf(i++).intern());
??????????}
?????}
} 運(yùn)行結(jié)果:
Exceptionin?thread?"main"?java.lang.OutOfMemoryError:PermGen?space
????at?java.lang.String.intern(NativeMethod)PermGen space的全稱(chēng)是Permanent Generation space,是指內(nèi)存的永久保存區(qū)域,也就是說(shuō)運(yùn)行時(shí)常量池屬于方法區(qū)(也就是虛擬機(jī)永久代)中的一部分。
另外,方法區(qū)是存放Class的相關(guān)信息的,運(yùn)行時(shí)如果有大量的類(lèi)來(lái)填滿(mǎn)方法區(qū),就會(huì)產(chǎn)生OutOfMemoryError異常。
☆
往期精彩
☆
01?Sentinel如何進(jìn)行流量監(jiān)控
02?Nacos源碼編譯
03?基于Apache Curator框架的ZooKeeper使用詳解
關(guān)注我
每天進(jìn)步一點(diǎn)點(diǎn)
