Java內(nèi)存模型

面試中問到“內(nèi)存模型”,通常是考察Java內(nèi)存結(jié)構(gòu)和GC,而不是Happens-Before等更深入、細(xì)致的內(nèi)容。內(nèi)存模型是考察coder對(duì)一門語言的理解能力,從而進(jìn)一步延伸到對(duì)JVM優(yōu)化,和平時(shí)學(xué)習(xí)的深度上,是Java面試中最重要的一部分。這里整理了內(nèi)存結(jié)構(gòu)和GC的知識(shí)點(diǎn),Happens-Before模型預(yù)計(jì)在以后學(xué)習(xí)過JVM過再來整理。

如果把內(nèi)存模型看做一個(gè)數(shù)據(jù)結(jié)構(gòu),那么面試中考察的重點(diǎn)分為內(nèi)存結(jié)構(gòu)和GC,不過有時(shí)候會(huì)單獨(dú)問到GC,另外大問題分解為小問題也方便理解。
·

內(nèi)存結(jié)構(gòu)

內(nèi)存結(jié)構(gòu)簡介

JVM的內(nèi)存結(jié)構(gòu)大概分為:

  1. 堆(heap):線程共享,所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配?;厥掌髦饕芾淼膶?duì)象。
  2. 方法區(qū)(MEATHOD AREA):線程共享,存儲(chǔ)類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼。
  3. 方法棧(JVM Stack):線程私有、存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口,對(duì)象指針。
  4. 本地方法棧(NATIVE METHOD STACK):線程私有。為虛擬機(jī)使用到的Native 方法服務(wù)。如Java使用c或者c++編寫的接口服務(wù)時(shí),代碼在此區(qū)運(yùn)行。
  5. PC寄存器(PC Register):線程私有。指向下一條要執(zhí)行的指令。
image.png

各區(qū)域詳細(xì)說明

在Java的內(nèi)存結(jié)構(gòu)中,我們重點(diǎn)關(guān)注的是堆和方法區(qū)。

image.png

堆的作用是存放對(duì)象實(shí)例和數(shù)組。從結(jié)構(gòu)上來分,可以分為新生代和老生代。而新生代又可以分為Eden 空間、From Survivor 空間(s0)、To Survivor 空間(s1)。 所有新生成的對(duì)象首先都是放在年輕代的。需要注意,Survivor的兩個(gè)區(qū)是對(duì)稱的,沒先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過來 對(duì)象,和從前一個(gè)Survivor復(fù)制過來的對(duì)象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor去過來的對(duì)象。而且,Survivor區(qū)總有一個(gè)是空的。

控制參數(shù)

-Xms設(shè)置堆的最小空間大小。-Xmx設(shè)置堆的最大空間大小。-XX:NewSize設(shè)置新生代最小空間大小。-XX:MaxNewSize設(shè)置新生代最小空間大小。

垃圾回收

此區(qū)域是垃圾回收的主要操作區(qū)域。

異常情況

如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError 異常。

方法區(qū)

方法區(qū)(Method Area)與Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。雖然Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫做Non-Heap(非堆),目的應(yīng)該是與Java 堆區(qū)分開來。

很多人愿意把方法區(qū)稱為“永久代”(Permanent Generation),本質(zhì)上兩者并不等價(jià),僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC 分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已。對(duì)于其他虛擬機(jī)(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。在Java8中永生代徹底消失了。

控制參數(shù)

-XX:PermSize 設(shè)置最小空間 -XX:MaxPermSize 設(shè)置最大空間。

垃圾回收

對(duì)此區(qū)域會(huì)涉及但是很少進(jìn)行垃圾回收。這個(gè)區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,一般來說這個(gè)區(qū)域的回收“成績”比較難以令人滿意。

異常情況

根據(jù)Java 虛擬機(jī)規(guī)范的規(guī)定, 當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError。

方法棧

每個(gè)線程會(huì)有一個(gè)私有的棧。每個(gè)線程中方法的調(diào)用又會(huì)在本棧中創(chuàng)建一個(gè)棧幀。在方法棧中會(huì)存放編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference 類型,它不等同于對(duì)象本身。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。

控制參數(shù)

-Xss控制每個(gè)線程棧的大小。

異常情況

在Java 虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:

  1. StackOverflowError: 異常線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度時(shí)拋出;
  2. OutOfMemoryError 異常: 虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出。

本地方法棧

本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其
區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則
是為虛擬機(jī)使用到的Native 方法服務(wù)。

控制參數(shù)

在Sun JDK中本地方法棧和方法棧是同一個(gè),因此也可以用-Xss控制每個(gè)線程的大小。

異常情況

與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出StackOverflowError 和OutOfMemoryError
異常。

PC計(jì)數(shù)器

它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。

異常情況

此內(nèi)存區(qū)域是唯一一個(gè)在Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError 情況的區(qū)域。

參考:
Java虛擬機(jī)詳解02----JVM內(nèi)存結(jié)構(gòu)
JVM內(nèi)存結(jié)構(gòu)

GC

簡言之,Java程序內(nèi)存主要(這里強(qiáng)調(diào)主要二字)分兩部分,堆和非堆。大家一般new的對(duì)象和數(shù)組都是在堆中的,而GC主要回收的內(nèi)存也是這塊堆內(nèi)存。

復(fù)習(xí)堆內(nèi)存模型

既然重點(diǎn)是堆內(nèi)存,我們就再看看堆的內(nèi)存模型。

堆內(nèi)存由垃圾回收器的自動(dòng)內(nèi)存管理系統(tǒng)回收。
堆內(nèi)存分為兩大部分:新生代和老年代。比例為1:2。
老年代主要存放應(yīng)用程序中生命周期長的存活對(duì)象。
新生代又分為三個(gè)部分:一個(gè)Eden區(qū)和兩個(gè)Survivor區(qū),比例為8:1:1。
Eden區(qū)存放新生的對(duì)象。
Survivor存放每次垃圾回收后存活的對(duì)象。

image.png

關(guān)注這幾個(gè)問題:

  1. 為什么要分新生代和老年代?
  2. 新生代為什么分一個(gè)Eden區(qū)和兩個(gè)Survivor區(qū)?
  3. 一個(gè)Eden區(qū)和兩個(gè)Survivor區(qū)的比例為什么是8:1:1?

這幾個(gè)問題都是垃圾回收機(jī)制所采用的算法決定的。所以問題轉(zhuǎn)化為,是何種算法?為什么要采用此種算法?

判定可回收對(duì)象

在進(jìn)行垃圾回收之前,我們需要清除一個(gè)問題——什么樣的對(duì)象是垃圾(無用對(duì)象),需要被回收?

目前最常見的有兩種算法用來判定一個(gè)對(duì)象是否為垃圾。

引用計(jì)數(shù)算法

給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的。

image.png

優(yōu)點(diǎn)是簡單,高效,現(xiàn)在的objective-c用的就是這種算法。

缺點(diǎn)是很難處理循環(huán)引用,比如圖中相互引用的兩個(gè)對(duì)象則無法釋放。

這個(gè)缺點(diǎn)很致命,有人可能會(huì)問,那objective-c不是用的好好的嗎?我個(gè)人并沒有覺得objective-c好好的處理了這個(gè)循環(huán)引用問題,它其實(shí)是把這個(gè)問題拋給了開發(fā)者。

可達(dá)性分析算法(根搜索算法)

為了解決上面的循環(huán)引用問題,Java采用了一種新的算法:可達(dá)性分析算法。

從GC Roots(每種具體實(shí)現(xiàn)對(duì)GC Roots有不同的定義)作為起點(diǎn),向下搜索它們引用的對(duì)象,可以生成一棵引用樹,樹的節(jié)點(diǎn)視為可達(dá)對(duì)象,反之視為不可達(dá)。

image.png

OK,即使循環(huán)引用了,只要沒有被GC Roots引用了依然會(huì)被回收,完美!

但是,這個(gè)GC Roots的定義就要考究了,Java語言定義了如下GC Roots對(duì)象:

  1. 虛擬機(jī)棧(幀棧中的本地變量表)中引用的對(duì)象。
  2. 方法區(qū)中靜態(tài)屬性引用的對(duì)象。
  3. 方法區(qū)中常量引用的對(duì)象。
  4. 本地方法棧中JNI引用的對(duì)象。

Stop The World

有了上面的垃圾對(duì)象的判定,我們還要考慮一個(gè)問題,請(qǐng)大家做好心里準(zhǔn)備,那就是Stop The World。

因?yàn)槔厥盏臅r(shí)候,需要整個(gè)的引用狀態(tài)保持不變,否則判定是判定垃圾,等我稍后回收的時(shí)候它又被引用了,這就全亂套了。所以,GC的時(shí)候,其他所有的程序執(zhí)行處于暫停狀態(tài),卡住了。

幸運(yùn)的是,這個(gè)卡頓是非常短(尤其是新生代),對(duì)程序的影響微乎其微 (關(guān)于其他GC比如并發(fā)GC之類的,在此不討論)。

所以GC的卡頓問題由此而來,也是情有可原,暫時(shí)無可避免。

垃圾回收

已經(jīng)知道哪些是垃圾對(duì)象了,怎么回收呢?

目前主流有以下幾種算法,目前JVM采用的是分代回收算法,而分代回收算法正是從這幾種算法發(fā)展而來。

標(biāo)記清除算法 (Mark-Sweep)

標(biāo)記-清除算法分為兩個(gè)階段:標(biāo)記階段和清除階段。標(biāo)記階段的任務(wù)是標(biāo)記出所有需要被回收的對(duì)象,清除階段就是回收被標(biāo)記的對(duì)象所占用的空間。

image.png

優(yōu)點(diǎn):簡單實(shí)現(xiàn)。

缺點(diǎn):容易產(chǎn)生內(nèi)存碎片(碎片太多可能會(huì)導(dǎo)致后續(xù)過程中需要為大對(duì)象分配空間時(shí)無法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動(dòng)作)。

復(fù)制算法 (Copying)

復(fù)制算法將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。

image.png

優(yōu)點(diǎn):實(shí)現(xiàn)簡單,運(yùn)行高效且不容易產(chǎn)生內(nèi)存碎片。

缺點(diǎn):對(duì)內(nèi)存空間的使用做出了高昂的代價(jià),因?yàn)槟軌蚴褂玫膬?nèi)存縮減到原來的一半。

從算法原理我們可以看出,復(fù)制算法算法的效率跟存活對(duì)象的數(shù)目多少有很大的關(guān)系,如果存活對(duì)象很多,那么復(fù)制算法算法的效率將會(huì)大大降低。

標(biāo)記整理算法 (Mark-Compact)

該算法標(biāo)記階段和Mark-Sweep一樣,但是在完成標(biāo)記之后,它不是直接清理可回收對(duì)象,而是將存活對(duì)象都向一端移動(dòng),然后清理掉端邊界以外的內(nèi)存。

image.png

優(yōu)點(diǎn):實(shí)現(xiàn)簡單,不容易產(chǎn)生內(nèi)存碎片,內(nèi)存使用高效。

缺點(diǎn):效率非常低。

所以,特別適用于存活對(duì)象多,回收對(duì)象少的情況下。

分代回收算法

以上幾種算法都有各自的優(yōu)點(diǎn)和缺點(diǎn),適用于不同的內(nèi)存情景。而分代回收算法根據(jù)Java的語言特性,將復(fù)制算法和標(biāo)記整理算法的的特點(diǎn)相結(jié)合,針對(duì)不同的內(nèi)存情景使用不同的回收算法。

這里重復(fù)一下兩種老算法的適用場(chǎng)景:

復(fù)制算法:適用于存活對(duì)象很少。回收對(duì)象多
標(biāo)記整理算法: 適用用于存活對(duì)象多,回收對(duì)象少

兩種算法剛好互補(bǔ),不同類型的對(duì)象生命周期決定了更適合采用哪種算法。

于是,我們根據(jù)對(duì)象存活的生命周期將內(nèi)存劃分為若干個(gè)不同的區(qū)域。一般情況下將堆區(qū)劃分為老年代(Old Generation)和新生代(Young Generation),老年代的特點(diǎn)是每次垃圾收集時(shí)只有少量對(duì)象需要被回收,使用標(biāo)記整理算法,而新生代的特點(diǎn)是每次垃圾回收時(shí)都有大量的對(duì)象需要被回收,復(fù)制算法,那么就可以根據(jù)不同代的特點(diǎn)采取最適合的收集算法。

現(xiàn)在回頭去看堆內(nèi)存為什么要?jiǎng)澐中律屠夏甏?,是不是覺得如此的清晰和自然了?

具體來看:

  1. 對(duì)于新生代,雖然采取的是復(fù)制算法,但是,實(shí)際中并不是按照上面算法中說的1:1的比例來劃分新生代的空間,而是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,比例為8:1:1。為什么?下一節(jié)深入分析。
  2. 老年代的特點(diǎn)是每次回收都只回收少量對(duì)象,這符合一個(gè)穩(wěn)定系統(tǒng)的主要特征——超過一半的對(duì)象會(huì)長期駐留在內(nèi)存中。所以老年代的比例要大于新生代,默認(rèn)的新生代:老年代的比例為1:2。

這就是分代回收算法。

深入理解分代回收算法

對(duì)于這個(gè)算法,我相信很多人還是有疑問的,我們來各個(gè)擊破,說清楚了就很簡單。

為什么不是一塊Survivor空間而是兩塊?

這里涉及到一個(gè)新生代和老年代的存活周期的問題,比如一個(gè)對(duì)象在新生代經(jīng)歷15次(僅供參考)GC,就可以移到老年代了。問題來了,當(dāng)我們第一次GC的時(shí)候,我們可以把Eden區(qū)的存活對(duì)象放到Survivor A空間,但是第二次GC的時(shí)候,Survivor A空間的存活對(duì)象也需要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時(shí),又把Survivor B空間的存活對(duì)象復(fù)制到Survivor A空間,如此反復(fù)。

所以,這里就需要兩塊Survivor空間來回倒騰。

為什么Eden空間這么大而Survivor空間要分的少一點(diǎn)?

新創(chuàng)建的對(duì)象都是放在Eden空間,這是很頻繁的,尤其是大量的局部變量產(chǎn)生的臨時(shí)對(duì)象,這些對(duì)象絕大部分都應(yīng)該馬上被回收,能存活下來被轉(zhuǎn)移到survivor空間的往往不多。所以,設(shè)置較大的Eden空間和較小的Survivor空間是合理的,大大提高了內(nèi)存的使用率,緩解了Copying算法的缺點(diǎn)。

我看8:1:1就挺好的,當(dāng)然這個(gè)比例是可以調(diào)整的,包括上面的新生代和老年代的1:2的比例也是可以調(diào)整的。

新的問題又來了,從Eden空間往Survivor空間轉(zhuǎn)移的時(shí)候Survivor空間不夠了怎么辦?直接放到老年代去。

Eden空間和兩塊Survivor空間的工作流程

這里本來簡單的Copying算法被劃分為三部分后很多朋友一時(shí)理解不了,也確實(shí)不好描述,下面我來演示一下Eden空間和兩塊Survivor空間的工作流程。

現(xiàn)在假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。

// 分配了一個(gè)又一個(gè)對(duì)象
放到Eden區(qū)
// 不好,Eden區(qū)滿了,只能GC(新生代GC:Minor GC)了
把Eden區(qū)的存活對(duì)象copy到Survivor A區(qū),然后清空Eden區(qū)(本來Survivor B區(qū)也需要清空的,不過本來就是空的)
// 又分配了一個(gè)又一個(gè)對(duì)象
放到Eden區(qū)
// 不好,Eden區(qū)又滿了,只能GC(新生代GC:Minor GC)了
把Eden區(qū)和Survivor A區(qū)的存活對(duì)象copy到Survivor B區(qū),然后清空Eden區(qū)和Survivor A區(qū)
// 又分配了一個(gè)又一個(gè)對(duì)象
放到Eden區(qū)
// 不好,Eden區(qū)又滿了,只能GC(新生代GC:Minor GC)了
把Eden區(qū)和Survivor B區(qū)的存活對(duì)象copy到Survivor A區(qū),然后清空Eden區(qū)和Survivor B區(qū)
// ...
// 有的對(duì)象來回在Survivor A區(qū)或者B區(qū)呆了比如15次,就被分配到老年代Old區(qū)
// 有的對(duì)象太大,超過了Eden區(qū),直接被分配在Old區(qū)
// 有的存活對(duì)象,放不下Survivor區(qū),也被分配到Old區(qū)
// ...
// 在某次Minor GC的過程中突然發(fā)現(xiàn):
// 不好,老年代Old區(qū)也滿了,這是一次大GC(老年代GC:Major GC)
Old區(qū)慢慢的整理一番,空間又夠了
// 繼續(xù)Minor GC
// ...
// ...

觸發(fā)GC的類型

了解這些是為了解決實(shí)際問題,Java虛擬機(jī)會(huì)把每次觸發(fā)GC的信息打印出來來幫助我們分析問題,所以掌握觸發(fā)GC的類型是分析日志的基礎(chǔ)。

  • GC_FOR_MALLOC: 表示是在堆上分配對(duì)象時(shí)內(nèi)存不足觸發(fā)的GC。
  • GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存達(dá)到一定量,或者可以理解為快要滿的時(shí)候,系統(tǒng)會(huì)自動(dòng)觸發(fā)GC操作來釋放內(nèi)存。
  • GC_EXPLICIT: 表示是應(yīng)用程序調(diào)用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號(hào)時(shí)觸發(fā)的GC。
  • GC_BEFORE_OOM: 表示是在準(zhǔn)備拋OOM異常之前進(jìn)行的最后努力而觸發(fā)的GC。

參考:


本文鏈接:Java內(nèi)存模型
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識(shí)共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近學(xué)習(xí)Python的GC機(jī)制時(shí),想到了java的GC,忘得差不多了,(⊙﹏⊙)b??!這里便做一下回顧總結(jié)。推薦周...
    廿陸小生閱讀 1,265評(píng)論 0 0
  • http://www.infoq.com/cn/articles/java-memory-model-1http:...
    xiaofudeng閱讀 212評(píng)論 0 0
  • 方法區(qū)和對(duì)所有線程共享的內(nèi)存區(qū)域;java棧、本地方法棧和程序計(jì)數(shù)器運(yùn)行是線程私有的內(nèi)存區(qū)域 java堆:是j...
    過去今天和未來閱讀 576評(píng)論 0 0
  • JVM架構(gòu) 當(dāng)一個(gè)程序啟動(dòng)之前,它的class會(huì)被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的...
    cocohaifang閱讀 1,834評(píng)論 0 7
  • 一 、java虛擬機(jī)底層結(jié)構(gòu)詳解 我們知道,一個(gè)JVM實(shí)例的行為不光是它自己的事,還涉及到它的子系統(tǒng)、存儲(chǔ)區(qū)域、...
    葡萄喃喃囈語閱讀 1,582評(píng)論 0 4

友情鏈接更多精彩內(nèi)容