JVM之內(nèi)存布局超詳細(xì)整理

??內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和CPU的中間倉(cāng)庫(kù)及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行敬察。JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過程中內(nèi)存申請(qǐng)、分配尔当、管理的策略莲祸,保證了JVM的高效穩(wěn)定運(yùn)行。不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異椭迎。結(jié)合JVM虛擬機(jī)規(guī)范锐帜,來學(xué)習(xí)一 下經(jīng)典的JVM內(nèi)存布局。

經(jīng)典jvm內(nèi)存布局(jdk8之后)

??話不多說侠碧,先來一圖(截圖來至阿里的<碼出高效:java開發(fā)手冊(cè)>)抹估。上圖就是jdk8之后的jvm經(jīng)典布局缠黍,接下來主要詳細(xì)分析各個(gè)分區(qū)的功能及作用弄兜。

Stacks(虛擬機(jī)棧)

虛擬機(jī)棧)

??棧( Stack )是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),就像子彈的彈夾,最后壓入的子彈先發(fā)射瓷式,壓在底部的子彈最后發(fā)射替饿,撞針只能訪問位于頂部的那一顆子彈。相對(duì)于基于寄存器的運(yùn)行環(huán)境來說贸典,JVM 是基于棧結(jié)構(gòu)的運(yùn)行環(huán)境视卢。棧結(jié)構(gòu)移植性更好,可控性更強(qiáng)廊驼。JVM中的虛擬機(jī)棧是描述Java方法執(zhí)行的內(nèi)存區(qū)域据过,它是線程私有的(每一條Java虛擬機(jī)線程都有自己私有的Java虛擬機(jī)棧,這個(gè)棧與線程同時(shí)創(chuàng)建)妒挎。

??棧中的元素用于支持虛擬機(jī)進(jìn)行方法調(diào)用绳锅,每個(gè)方法從開始調(diào)用到執(zhí)行完成的過程,就是棧幀從入棧到出棧的過程酝掩。在活動(dòng)線程中鳞芙,只有位于棧頂?shù)膸攀怯行У模Q為當(dāng)前棧幀。正在執(zhí)行的方法稱為當(dāng)前方法原朝,棧幀是方法運(yùn)行的基本結(jié)構(gòu)驯嘱。在執(zhí)行引擎運(yùn)行時(shí),所有指令都只能針對(duì)當(dāng)前棧幀進(jìn)行操作喳坠。StackOverflowError表示請(qǐng)求的棧溢出鞠评,導(dǎo)致內(nèi)存耗盡,通常出現(xiàn)在遞歸方法中丙笋。JVM能夠橫掃千軍, 虛擬機(jī)棧就是它的心腹大將谢澈,當(dāng)前方法的棧幀,都是正在戰(zhàn)斗的戰(zhàn)場(chǎng)御板,其中的操作棧是參與戰(zhàn)斗的士兵锥忿。

  • 局部表量表

??每個(gè)棧幀(見上圖)內(nèi)部都包含一組稱為局部變量表的變量列表。棧幀中局部變量表的長(zhǎng)度由編譯期決定怠肋,并且存儲(chǔ)于類或接口的二進(jìn)制表示之中敬鬓,即通過方法的code屬性保存及提供給棧幀使用。

??一個(gè)局部量可以保存一個(gè)類型為boolean笙各、byte钉答、char、short杈抢、int数尿、float、reference或returnAddress的數(shù)據(jù)惶楼。兩個(gè)局部變量可以保存一個(gè)類型為long或double的數(shù)據(jù)右蹦。

??局部變量使用索引來進(jìn)行定位訪問。首個(gè)局部變量的索引值為0歼捐。局部變量的索引值是個(gè)整數(shù)何陆,它大于等于0,且小于局部變量表的長(zhǎng)度豹储。

??long和double類型的數(shù)據(jù)占用兩個(gè)連續(xù)的局部變量贷盲,這兩種類型的數(shù)據(jù)值采用兩個(gè)局部變量中較小的索引值來定位。例如剥扣,將一個(gè)double類型的值存儲(chǔ)在索引值為n的局部變量中巩剖,實(shí)際上的意思是索引值為n和n+1的兩個(gè)局部變量都用來存儲(chǔ)這個(gè)值。然而钠怯,索引值為n+1的局部變量是無法直接讀取的佳魔,但是可能會(huì)被寫人。不過呻疹,如果進(jìn)行了這種操作,那將會(huì)導(dǎo)致局部變量n的內(nèi)容失效吃引。前面提及的局部變量索引值n并不要求一定是偶數(shù)筹陵,Java虛擬機(jī)也不要求double和long類型數(shù)據(jù)采用64位對(duì)齊的方式連續(xù)地存儲(chǔ)在局部變量表中。虛擬機(jī)實(shí)現(xiàn)者可以自由地選擇適當(dāng)?shù)姆绞侥鞒撸ㄟ^兩個(gè)局部變量來存儲(chǔ)一個(gè)double或long類型的值朦佩。

??Java虛擬機(jī)使用局部變量表來完成方法調(diào)用時(shí)的參數(shù)傳遞。當(dāng)調(diào)用類方法時(shí)庐氮,它的參數(shù)將會(huì)依次傳遞到局部變量表中從0開始的連續(xù)位置上语稠。當(dāng)調(diào)用實(shí)例方法時(shí),第0個(gè)局部變量一定用來存儲(chǔ)該實(shí)例方法所在對(duì)象的引用(即Java語言中的this關(guān)鍵字)弄砍。后續(xù)的其他參數(shù)將會(huì)傳遞至局部變量表中從1開始的連續(xù)位置上仙畦。

  • 操作數(shù)棧

??每個(gè)棧幀(見上圖)內(nèi)部都包含一個(gè)稱為操作數(shù)棧的后進(jìn)先出( Last-In-First-Out,LIFO)棧。棧幀中操作數(shù)棧的最大深度由編譯期決定音婶,并且通過方法的code屬性保存及提供給棧幀使用慨畸。

??棧幀在剛剛創(chuàng)建時(shí),操作數(shù)棧是空的衣式。Java虛擬機(jī)提供一些字節(jié)碼指令來從局部變量表或者對(duì)象實(shí)例的字段中復(fù)制常量或變量值到操作數(shù)棧中(如:iload寸士,aload,getfield等)碴卧,也提供了一些指令用于從操作數(shù)棧取走數(shù)據(jù)(將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表弱卡。如:istore,astore等)住册、操作數(shù)據(jù)(iadd婶博,isub等)以及把操作結(jié)果重新人棧。在調(diào)用方法時(shí)荧飞,操作數(shù)棧也用來準(zhǔn)備調(diào)用方法的參數(shù)以及接收方法返回結(jié)果凡人。

??例如,iadd字節(jié)碼指令的作用是將兩個(gè)int類型的數(shù)值相加,它要求在執(zhí)行之前操作數(shù)棧的棧頂已經(jīng)存在兩個(gè)由前面的其他指令所放人的int類型數(shù)值。在執(zhí)行iadd指令時(shí)垢箕,兩個(gè)int類型數(shù)值從操作棧中出棧划栓,相加求和兑巾,然后將求和結(jié)果重新入棧条获。在操作數(shù)棧中,一項(xiàng)運(yùn)算常由多個(gè)子運(yùn)算( subcomputation) 嵌套進(jìn)行蒋歌,一個(gè)子運(yùn)算過程的結(jié)果可以被其他外圍運(yùn)算所使用帅掘。

操作數(shù)棧與局部變量表之間傳遞參數(shù)示例:

static class VmStacks {

/******************方法下方字節(jié)碼是通過javap -v class文件獲得*********************/
        /**
        *  該方法主要演示jvm對(duì)方法調(diào)用過程
        */
        public int directInvoke() {
            return addI(1, 2);
        }

   public int directInvoke();
      descriptor: ()I
      flags: ACC_PUBLIC
      Code:
        stack=3, locals=1, args_size=1    // 最大棧深度為3,局部變量表個(gè)數(shù)為1
           0: aload_0    // 將this從局部變量表壓入操作數(shù)棧堂油,因?yàn)樵摲椒閷?shí)例方法修档,故局部變量表slot為0的就是實(shí)例本身(this)
           1: iconst_1  // 將常量值1壓入操作數(shù)棧
           2: iconst_2  // 將常量值2壓入操作數(shù)棧
           3: invokevirtual #2      // Method addI:(II)I  調(diào)用addI(int x, int y)方法
           6: ireturn   返回int類型的值
        LineNumberTable:
          line 86: 0
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0       7     0  this   Lmayfly/core/util/CodeByteTest$VmStacks;

1. 因?yàn)樵摲椒閷?duì)象實(shí)例方法,故方法調(diào)用的第一步是將當(dāng)前實(shí)例的自身引用this壓人操作數(shù)棧中
(如果是類方法府框,即static方法則沒有該步驟)吱窝。傳遞給方法的int類型參數(shù)值1和2隨后人棧。

2. 當(dāng)調(diào)用addI方法(即invokevirtual #2 指令)時(shí),Java虛擬機(jī)會(huì)創(chuàng)建一個(gè)新的棧幀院峡,
傳遞給addI方法的參數(shù)值會(huì)成為新棧幀中對(duì)應(yīng)局部變量的初始值兴使。即由directInvoke
方法推人操作數(shù)棧的this和兩個(gè)傳遞給addI方法的參數(shù)1與2,會(huì)作為addI方法棧幀的
第0照激、1发魄、2個(gè)局部變量。

3. 當(dāng)addI方法執(zhí)行結(jié)束俩垃、方法返回時(shí)励幼,int類型的返回值被壓入方法調(diào)用者的棧幀的操作數(shù)棧,
即directInvoke方法的操作數(shù)棧中口柳。而這個(gè)返回值又會(huì)立即返回給directInvoke的調(diào)用者苹粟。
directInvoke方法的返回過程由directInvoke方法中的ireturn指令實(shí)現(xiàn)。由addI方法所返回的
int類型值會(huì)壓入當(dāng)前操作數(shù)棧的棧頂跃闹,而ireturn指令則會(huì)把當(dāng)前操作數(shù)棧的棧頂值(此處就是addI的返回值)
壓人directInvoke方法的調(diào)用者的操作數(shù)棧六水。然后跳轉(zhuǎn)至調(diào)用directInvoke的那個(gè)方法的下一條指令繼續(xù)執(zhí)行,
并將調(diào)用者的棧幀重新設(shè)為當(dāng)前棧幀辣卒。Java虛擬機(jī)對(duì)不同數(shù)據(jù)類型(包括聲明為void,即沒有返回值的方法)
的返回值提供了不同的方法返回指令掷贾,各種不同返回值類型的方法都使用這一組返回指令來返回。
/**********************************************************************************/

        public int addI(int x, int y) {
            int z = y++;  // 純粹為了演示  i++與++i之間字節(jié)碼的區(qū)別荣茫,無其他意義
            return ++x + y;
        }


public int addI(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_2
         1: iinc          2, 1
         4: istore_3
         5: iinc          1, 1
         8: iload_1
         9: iload_2
        10: iadd
        11: ireturn
      LineNumberTable:
        line 71: 0
        line 72: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lmayfly/core/util/CodeByteTest$VmStacks;
            0      12     1     x   I
            0      12     2     y   I
            5       7     3     z   I

1. 方法調(diào)用者(如上個(gè)方法directInvoke)將操作棧上的變量出棧并傳遞給該方法棧幀的局部變量表中的的
0想帅,1,2位置的slot上啡莉。

2. 在0~4索引(指令操作碼在數(shù)組中的下標(biāo)港准,該數(shù)組以字節(jié)形式來存儲(chǔ)當(dāng)前方法的java虛擬機(jī)代碼,
也可以認(rèn)為是相對(duì)于方法起始處的字節(jié)偏移量)上的三條字節(jié)碼表示的是int z = y++ 該行代碼; 
 iload_ 2 從局部變量表的第2號(hào)抽屜里取出一個(gè)數(shù)咧欣,壓入棧頂浅缸,下一步直接在抽屜(局部變量表中的slot)
里實(shí)現(xiàn)+1的操作,而這個(gè)操作對(duì)棧頂元素的值沒有影響魄咕。所以istore_ 3只是把棧頂元素賦值給z衩椒,即z == y而不是y+1后的值;

3. 索引5~8表示++x, iinc 1, 1先在第1號(hào)抽屜里執(zhí)行+1操作哮兰,然后通過iload_ 1 把第1號(hào)抽屜里的數(shù)壓入棧頂毛萌,
所以操作數(shù)棧中存入的是+1之后的值。

4. 接著將2號(hào)抽屜的數(shù)值(即y)壓入棧頂喝滞,隨后執(zhí)行iadd指令阁将,將棧頂?shù)膬蓚€(gè)元素彈出棧相加后,
并將相加后的結(jié)果重新壓入棧頂并return給調(diào)用者右遭。
/**********************************************************************************/
    }
  • 動(dòng)態(tài)鏈接

??每個(gè)棧幀內(nèi)部都包含一個(gè)指向當(dāng)前方法所在類型的運(yùn)行時(shí)常量池的引用做盅,以便對(duì)當(dāng)前方法的代碼實(shí)現(xiàn)動(dòng)態(tài)鏈接缤削。在class文件里面,一個(gè)方法若要調(diào)用其他方法吹榴,或者訪問成員變量僻他,則需要通過符號(hào)引用(symbolic reference) 來表示,動(dòng)態(tài)鏈接的作用就是將這些以符號(hào)引用所表示的方法轉(zhuǎn)換為對(duì)實(shí)際方法的直接引用腊尚。類加載的過程中將要解析尚未被解析的符號(hào)引用吨拗,并且將對(duì)變量的訪問轉(zhuǎn)化為變量在程度運(yùn)行時(shí),位于存儲(chǔ)結(jié)構(gòu)中的正確偏移量婿斥。由于對(duì)其他類中的方法和變量進(jìn)行了晚期綁定(latebinding),所以即便那些類發(fā)生變化劝篷,也不會(huì)影響調(diào)用它們的方法。

  • 方法返回地址

??方法執(zhí)行時(shí)有兩種退出情況:第一民宿,正常退出娇妓,即正常執(zhí)行到任何方法的返回字節(jié)碼指令花嘶,如RETURN顽照、IRETURN、ARETURN等;第二臼氨,異常退出志群。無論何種退出情況着绷,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀锌云,退出可能有三種方式:

  1. 返回值壓入上層調(diào)用棧幀荠医。
  2. 異常信息拋給能夠處理的棧幀。
  3. PC計(jì)數(shù)器指向方法調(diào)用后的下一條指令桑涎。

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

??Java虛擬機(jī)可以支持多條線程同時(shí)執(zhí)行彬向,每一條Java虛擬機(jī)線程都有自的pc ( program counter) 寄存器。在任意時(shí)刻攻冷,一條Java 虛擬機(jī)線程只會(huì)執(zhí)行一個(gè)方法的代碼娃胆,這個(gè)正在被線程執(zhí)行的方法稱為該線程的當(dāng)前方法。如果這個(gè)方法不是native的等曼,那pc寄存器就保存Java虛擬機(jī)正在執(zhí)行的字節(jié)碼指令的地址,如果該方法是native的里烦,那pc寄存器的值是undefined。pc寄存器的容量至少應(yīng)當(dāng)能保存一個(gè)returnAddress類型的數(shù)據(jù)或者一個(gè)與平臺(tái)相關(guān)的本地指針的值涉兽。

Heap(堆區(qū))

??Heap是OOM故障最主要的發(fā)源地招驴,它存儲(chǔ)著幾乎所有的實(shí)例對(duì)象篙程,堆由垃圾收集器自動(dòng)回收枷畏,堆區(qū)由各子線程共享使用。通常情況下虱饿,它占用的空間是所有內(nèi)存區(qū)域中最大的拥诡,但如果無節(jié)制地創(chuàng)建大量對(duì)象触趴,也容易消耗完所有的空間。堆的內(nèi)存空間既可以固定大小渴肉,也可以在運(yùn)行時(shí)動(dòng)態(tài)地調(diào)整冗懦,通過如下參數(shù)設(shè)定初始值和最大值,比如-Xms256M -Xmx1024M仇祭, 其中-X表示它是JVM運(yùn)行參數(shù)披蕉,ms是memory start的簡(jiǎn)稱,mx是memory max的簡(jiǎn)稱乌奇,分別代表最小堆容量和最大堆容量没讲。但是在通常情況下,服務(wù)器在運(yùn)行過程中礁苗,堆空間不斷地?cái)U(kuò)容與回縮爬凑,勢(shì)必形成不必要的系統(tǒng)壓力,所以在線上生產(chǎn)環(huán)境中试伙,JVM的Xms和Xmx設(shè)置成一樣大小嘁信,避免在GC后調(diào)整堆大小時(shí)帶來的額外壓力。

??Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分疏叨,其中程序計(jì)數(shù)器潘靖、虛擬機(jī)棧本地方法棧三個(gè)區(qū)域隨線程而生蚤蔓,隨線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作秘豹。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時(shí)就已知的(盡管在運(yùn)行期會(huì)由JIT編譯器進(jìn)行一些優(yōu)化,但在基于概念模型的討論中昌粤,大體上可以認(rèn)為是編譯期可知的)既绕,因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個(gè)區(qū)域內(nèi)不需要過多考慮回收的問題涮坐,因?yàn)榉椒ńY(jié)束或線程結(jié)束時(shí)凄贩,內(nèi)存自然就跟隨著回收了。因此堆是垃圾收集的最主要內(nèi)存區(qū)域袱讹。

??如最開始那個(gè)布局圖疲扎,為何要將堆空間分為新生代、老年代捷雕,以及新生代又為何要?jiǎng)澐譃橐粋€(gè)Eden區(qū)和兩個(gè)Survivor(幸存者)區(qū)椒丧,結(jié)合GC講解可更好地理解為何要醬紫劃分。

?? GC(Garbage Collection 垃圾收集)

判斷對(duì)象已死

??堆中幾乎存放著Java世界中所有的對(duì)象實(shí)例救巷,垃圾收集器在對(duì)堆進(jìn)行回收前壶熏,第一件事情就是要確定這些對(duì)象有哪些還“存活”著,哪些已經(jīng)“死去”(即不可能再被任何途徑使用的對(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ì)象就是不可能再被使用的帽哑。引用計(jì)數(shù)算法(Reference Counting)的實(shí)現(xiàn)簡(jiǎn)單谜酒,判定效率也很高,但是妻枕,Java語言中沒有選用引用計(jì)數(shù)算法來管理內(nèi)存僻族,其中最主要的原因是它很難解決對(duì)象之間的相互循環(huán)引用的問題。舉個(gè)簡(jiǎn)單的例代碼如下:

public class ReferenceCountingGC {

    public Object instance;

    public ReferenceCountingGC(String name){}
}

public static void testGC(){

    ReferenceCountingGC a = new ReferenceCountingGC("objA");
    ReferenceCountingGC b = new ReferenceCountingGC("objB");

    a.instance = b;
    b.instance = a;

    a = null;
    b = null;
}

??我們可以看到屡谐,最后這2個(gè)對(duì)象已經(jīng)不可能再被訪問了鹰贵,但由于他們相互引用著對(duì)方,導(dǎo)致它們的引用計(jì)數(shù)永遠(yuǎn)都不會(huì)為0康嘉,通過引用計(jì)數(shù)算法碉输,也就永遠(yuǎn)無法通知GC收集器回收它們。

  • 根搜索算法

??在主流的商用程序語言中(Java和C#亭珍,甚至包括前面提到的古老的Lisp)敷钾,都是使用根搜索算法(GC Roots Tracing)判定對(duì)象是否存活的。這個(gè)算法的基本思路就是通過一系列的名為“GC Roots”的對(duì)象作為起始點(diǎn)肄梨,從這些節(jié)點(diǎn)開始向下搜索阻荒,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GCRoots到這個(gè)對(duì)象不可達(dá))時(shí)众羡,則證明此對(duì)象是不可用的侨赡。如下圖所示,對(duì)象object 5粱侣、object 6羊壹、object 7雖然互相有關(guān)聯(lián),但是它們到GCRoots是不可達(dá)的齐婴,所以它們將會(huì)被判定為是可回收的對(duì)象油猫。

根搜索算法

??通過根搜索算法,成功解決了引用計(jì)數(shù)所無法解決的問題“循環(huán)依賴”柠偶,只要你無法與 GC Root 建立直接或間接的連接情妖,系統(tǒng)就會(huì)判定你為可回收對(duì)象。那這樣就引申出了另一個(gè)問題诱担,哪些屬于 GC Root毡证。在Java語言里,可作為GCRoots的對(duì)象包括下面幾種:

  1. 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象蔫仙。
  2. 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象料睛。
  3. 方法區(qū)中的常量引用的對(duì)象。
  4. 本地方法棧中JNI (即一般說的Native方法)的引用的對(duì)象。
垃圾收集算法
  • 標(biāo)記-清除算法

??最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep) 算法秦效,如它的名字一樣雏蛮,算法分為"標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象涎嚼,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象阱州,之所以說它是最基礎(chǔ)的收集算法,是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的法梯。它的主要缺點(diǎn)有兩個(gè):一個(gè)是效率問題苔货,標(biāo)記和清除過程的效率都不高,另外一個(gè)是空間問題立哑,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片夜惭,空間碎片太多可能會(huì)導(dǎo)致,當(dāng)程在以后的運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集 動(dòng)作铛绰。標(biāo)記-清除算法的執(zhí)行過程如下圖所示诈茧。

標(biāo)記-清除
  • 復(fù)制算法

??為了解決效率問題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了捂掰,它將可用內(nèi)存按容量劃分為大小相等的兩塊敢会,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了这嚣,就將還存活著的對(duì)象復(fù)制到另外一塊上面鸥昏,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收姐帚,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況吏垮,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可罐旗,實(shí)現(xiàn)簡(jiǎn)單膳汪,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半九秀,未免太高了一點(diǎn)旅敷。復(fù)制算法的執(zhí)行過程如下圖所示。

復(fù)制算法

??現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代颤霎,IBM的專門研究表明媳谁,新生代中的對(duì)象98%是朝生夕死的,所以并不需要按照1: 1的比例來劃分內(nèi)存空間友酱,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間晴音,每次使用Eden和其中的一塊Survivor。當(dāng)回收時(shí)缔杉,將Eden和Survivor中還存活著的對(duì)象一次性地拷貝到另外一塊Survivor空間上锤躁,最后清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1或详,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90% ( 80%+10%)系羞,只有10%的內(nèi)存是會(huì)被“浪費(fèi)”的郭计。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù)椒振,我們沒有辦法保證每次回收都只有不多于10%的對(duì)象存活昭伸,當(dāng)Survivor空間不夠用時(shí),就需要依賴其他內(nèi)存(這里指老年代)澎迎。

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

??復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作庐杨,效率將會(huì)變低。更關(guān)鍵的是夹供,如果不想浪費(fèi)50%的空間灵份,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況哮洽,所以在老年代一般不能直接選用這種算法填渠。根據(jù)老年代的特點(diǎn),有人提出了另外一種“標(biāo)記-整理”(Mark-Compact)算法鸟辅,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣氛什,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng)剔桨,然后直接清理掉端邊界以外的內(nèi)存屉更,“標(biāo)記-整理”算法的示意圖如下圖所示。

標(biāo)記-整理算法
  • 分代收集算法

??當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法洒缀,這種算法并沒有什么新的思想瑰谜,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般般是把Java堆分為新生代和老年代树绩,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ㄈ浴T谛律校看卫占瘯r(shí)都發(fā)現(xiàn)有大批對(duì)象死去饺饭,只有少量存活渤早,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集瘫俊。而老年代中因?yàn)閷?duì)象存活率高鹊杖、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記一清理”或者“標(biāo)記一整理” 算法來進(jìn)行回收扛芽。

堆內(nèi)存及GC總結(jié)

??堆分成兩大塊:新生代和老年代骂蓖。對(duì)象產(chǎn)生之初在新生代,步入暮年時(shí)進(jìn)入老年代川尖,但是老年代也接納在新生代無法容納的超大對(duì)象登下。新生代= 1個(gè)Eden區(qū)+ 2個(gè)Survivor區(qū)。絕大部分對(duì)象在Eden區(qū)生成,當(dāng)Eden區(qū)裝填滿的時(shí)候被芳,會(huì)觸發(fā)YoungGarbage Collection缰贝, 即YGC(也叫MinorGc)。垃圾回收的時(shí)候畔濒,在Eden區(qū)實(shí)現(xiàn)清除策略剩晴,沒有被引用的對(duì)象則直接回收。依然存活的對(duì)象會(huì)被移送到Survivor區(qū)篓冲,這個(gè)區(qū)真是名副其實(shí)的存在李破。Survivor 區(qū)分為S0和S1兩塊內(nèi)存空間宠哄,送到哪塊空間呢?每次YGC的時(shí)候壹将,它們將存活的對(duì)象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清除毛嫉,交換兩塊空間的使用狀態(tài)诽俯。如果YGC要移送的對(duì)象大于Survivor區(qū)容量的上限,則直接移交給老年代承粤。假如一些沒有進(jìn)取心的對(duì)象以為可以一直在新生代的Survivor區(qū)交換來交換去暴区,那就錯(cuò)了。每個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器辛臊,每次YGC都會(huì)加1仙粱。
-XX:MaxTenuringThreshold參數(shù)能配置計(jì)數(shù)器的值到達(dá)某個(gè)閾值的時(shí)候,對(duì)象從新生代晉升至老年代彻舰。如果該參數(shù)配置為1,那么從新生代的Eden區(qū)直接移至老年代伐割。默認(rèn)值是15,可以在Survivor區(qū)交換14次之后刃唤,晉升至老年代隔心。對(duì)象分配及晉升流程圖如下圖所示。

對(duì)象分配與簡(jiǎn)要GC流程圖

Metaspace (元空間)

??早在JDK8版本中尚胞,元空間的前身Perm區(qū)(永久代)已經(jīng)被淘汰硬霍。在JDK7及之前的版本中,只有Hotspot才有Perm區(qū)笼裳,譯為永久代唯卖,它在啟動(dòng)時(shí)固定大小,很難進(jìn)行調(diào)優(yōu)躬柬,并且FGC時(shí)會(huì)移動(dòng)類元信息拜轨。在某些場(chǎng)景下,如果動(dòng)態(tài)加載類過多楔脯,容易產(chǎn)生Perm區(qū)的0OM撩轰。比如某個(gè)實(shí)際Web工程中,因?yàn)楣δ茳c(diǎn)比較多,在運(yùn)行過程中堪嫂,要不斷動(dòng)態(tài)加載很多的類偎箫,經(jīng)常出現(xiàn)致命錯(cuò)誤:" java.lang.OutOfMemoryError: PermGenspace"為了解決該問題,需要設(shè)定運(yùn)行參數(shù)-XX:MaxPermSize= 1280m皆串,如果部署到新機(jī)器上淹办,往往會(huì)因?yàn)镴VM參數(shù)沒有修改導(dǎo)致故障再現(xiàn)。不熟悉此應(yīng)用的人排查問題時(shí)往往苦不堪言恶复,除此之外怜森,永久代在垃圾回收過程中還存在諸多問題。所以谤牡,JDK8使用元空間替換永久代副硅。在JDK8及以上版本中,設(shè)定MaxPermSize參數(shù)翅萤,JVM在啟動(dòng)時(shí)并不會(huì)報(bào)錯(cuò)恐疲,但是會(huì)提示: Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize- =2560m; support was removed in 8.0。區(qū)別于永久代套么,元空間在本地內(nèi)存中分配培己。在JDK8里,Perm 區(qū)中的所有內(nèi)容中字符串常量移至堆內(nèi)存胚泌,其他內(nèi)容包括類元信息省咨、字段、靜態(tài)屬性玷室、方法零蓉、常量等都移動(dòng)至元空間內(nèi),比如下圖中的Object類元信息阵苇、靜態(tài)屬性System.out壁公、整型常量1000000等。圖中顯示在常量池中的String绅项,其實(shí)際對(duì)象是被保存在堆內(nèi)存中的紊册。

常量池

:以上大部分內(nèi)容(除代碼示例)摘抄整理自《深入理解java虛擬機(jī)》、《Java虛擬機(jī)規(guī)范 JavaSE 8版本》快耿、《碼出高效:Java開發(fā)手冊(cè)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末囊陡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子掀亥,更是在濱河造成了極大的恐慌撞反,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搪花,死亡現(xiàn)場(chǎng)離奇詭異遏片,居然都是意外死亡嘹害,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門吮便,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笔呀,“玉大人,你說我怎么就攤上這事髓需⌒硎Γ” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵僚匆,是天一觀的道長(zhǎng)微渠。 經(jīng)常有香客問我,道長(zhǎng)咧擂,這世上最難降的妖魔是什么逞盆? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮屋确,結(jié)果婚禮上纳击,老公的妹妹穿的比我還像新娘续扔。我一直安慰自己攻臀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布纱昧。 她就那樣靜靜地躺著刨啸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪识脆。 梳的紋絲不亂的頭發(fā)上设联,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音灼捂,去河邊找鬼离例。 笑死,一個(gè)胖子當(dāng)著我的面吹牛悉稠,可吹牛的內(nèi)容都是我干的宫蛆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼的猛,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼耀盗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起卦尊,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤叛拷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后岂却,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忿薇,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡裙椭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了署浩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骇陈。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瑰抵,靈堂內(nèi)的尸體忽然破棺而出你雌,到底是詐尸還是另有隱情,我是刑警寧澤二汛,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布婿崭,位于F島的核電站,受9級(jí)特大地震影響肴颊,放射性物質(zhì)發(fā)生泄漏氓栈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一婿着、第九天 我趴在偏房一處隱蔽的房頂上張望授瘦。 院中可真熱鬧,春花似錦竟宋、人聲如沸提完。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽徒欣。三九已至,卻和暖如春蜗字,著一層夾襖步出監(jiān)牢的瞬間打肝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工挪捕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粗梭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓级零,卻偏偏與公主長(zhǎng)得像断医,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子妄讯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345