深入理解JVM01 - 自動(dòng)內(nèi)存管理機(jī)制

"??????This tutorial is experimental and unsupported."

內(nèi)存模型

下圖是描述JVM內(nèi)存模型的圖:

15475447681280.png

JVM包含兩個(gè)子系統(tǒng)和兩個(gè)組件病线,兩個(gè)子系統(tǒng)為Class loader(類(lèi)裝載)蒂窒、Execution engine(執(zhí)行引擎)惫东;兩個(gè)組件為Runtime data area(運(yùn)行時(shí)數(shù)據(jù)區(qū))芹缔、Native Interface(本地接口)捶闸。

Class loader(類(lèi)裝載): 根據(jù)給定的全限定名類(lèi)名(如:java.lang.Object)來(lái)裝載class文件到Runtime data area中的method area。

Execution engine(執(zhí)行引擎): 執(zhí)行classes中的指令乾忱。

Native Interface(本地接口): 與native libraries交互彤守,是其它編程語(yǔ)言交互的接口。

Runtime data area(運(yùn)行時(shí)數(shù)據(jù)區(qū)): 這就是我們常說(shuō)的JVM的內(nèi)存洒缀。

  • 程序計(jì)數(shù)器(Program Counter Register): 一塊較小的內(nèi)存空間瑰谜,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
  • Java虛擬機(jī)棧(Java Virtual Machine Stacks): 是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame[1])用于存儲(chǔ)局部變量表树绩、操作數(shù)棧萨脑、動(dòng)態(tài)鏈接、方法出口等信息饺饭。
  • 本地方法棧(Native Method Stack): 與虛擬機(jī)棧所發(fā)揮的作用是非常相似的渤早,為虛擬機(jī)使用到的Native方法服務(wù)。
  • Java堆(Java Heap): 是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊瘫俊,唯一目的就是存放對(duì)象實(shí)例鹊杖,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存悴灵。
  • 方法區(qū)(Method Area): 用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量骂蓖、靜態(tài)變量积瞒、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
  • 運(yùn)行時(shí)常量池(Runtime Constant Pool): 是方法區(qū)的一部分涯竟,Class文件中除了有類(lèi)的版本赡鲜、字段空厌、方法庐船、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table)嘲更,用于存放編譯期生成的各種字面量和符號(hào)引用筐钟,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
  • 直接內(nèi)存(Direct Memory): 并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分赋朦,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域篓冲。JDK 1.4中新加入了NIO(New Input/Output)類(lèi),引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式宠哄,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存壹将,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。

Heap和Method Area是被所有線程的共享使用的毛嫉;而Java stack, Program counter 和Native method stack是以線程為粒度的诽俯,每個(gè)線程獨(dú)自擁有自己的部分。

對(duì)象探秘

創(chuàng)建

下面是對(duì)象創(chuàng)建的主要流程:

JVM對(duì)象創(chuàng)建.png

虛擬機(jī)遇到一條new指令時(shí)承粤,先檢查常量池是否已經(jīng)加載相應(yīng)的類(lèi)暴区,如果沒(méi)有,必須先執(zhí)行相應(yīng)的類(lèi)加載辛臊。類(lèi)加載通過(guò)后仙粱,接下來(lái)分配內(nèi)存。若Java堆中內(nèi)存是絕對(duì)規(guī)整的彻舰,使用“指針碰撞“方式分配內(nèi)存伐割;如果不是規(guī)整的,就從空閑列表中分配刃唤,叫做”空閑列表“方式隔心。劃分內(nèi)存時(shí)還需要考慮一個(gè)問(wèn)題-并發(fā),也有兩種方式: CAS同步處理透揣,或者本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)济炎。然后內(nèi)存空間初始化操作,接著是做一些必要的對(duì)象設(shè)置(元信息辐真、哈希嗎...)须尚,最后執(zhí)行<init>方法崖堤。

內(nèi)存布局

一個(gè)Java對(duì)象在內(nèi)存中包括對(duì)象頭、實(shí)例數(shù)據(jù)和補(bǔ)齊填充3個(gè)部分:

對(duì)象的內(nèi)存布局.png

對(duì)象頭:

  • Mark Word:包含一系列的標(biāo)記位耐床,比如輕量級(jí)鎖的標(biāo)記位密幔,偏向鎖標(biāo)記位等等。在32位系統(tǒng)占4字節(jié)撩轰,在64位系統(tǒng)中占8字節(jié)胯甩;
  • Class Pointer:用來(lái)指向?qū)ο髮?duì)應(yīng)的Class對(duì)象(其對(duì)應(yīng)的元數(shù)據(jù)對(duì)象)的內(nèi)存地址。在32位系統(tǒng)占4字節(jié)堪嫂,在64位系統(tǒng)中占8字節(jié)偎箫;
  • Length:如果是數(shù)組對(duì)象,還有一個(gè)保存數(shù)組長(zhǎng)度的空間皆串,占4個(gè)字節(jié)淹办;

對(duì)象實(shí)際數(shù)據(jù):

對(duì)象實(shí)際數(shù)據(jù)包括了對(duì)象的所有成員變量,其大小由各個(gè)成員變量的大小決定恶复,比如:byte和boolean是1個(gè)字節(jié)怜森,short和char是2個(gè)字節(jié),int和float是4個(gè)字節(jié)谤牡,long和double是8個(gè)字節(jié)副硅,reference是4個(gè)字節(jié)(64位系統(tǒng)中是8個(gè)字節(jié))。

對(duì)齊填充:

Java對(duì)象占用空間是8字節(jié)對(duì)齊的翅萤,即所有Java對(duì)象占用bytes數(shù)必須是8的倍數(shù)恐疲。例如,一個(gè)包含兩個(gè)屬性的對(duì)象:int和byte断序,這個(gè)對(duì)象需要占用8+4+1=13個(gè)字節(jié)流纹,這時(shí)就需要加上大小為3字節(jié)的padding進(jìn)行8字節(jié)對(duì)齊,最終占用大小為16個(gè)字節(jié)违诗。

訪問(wèn)定位

目前主流的訪問(wèn)方式有通過(guò)句柄和直接指針兩種方式漱凝。

訪問(wèn)定位.png

這兩種訪問(wèn)方式各有利弊,使用句柄訪最大的好處是reference中存儲(chǔ)著穩(wěn)定的句柄地址诸迟,當(dāng)對(duì)象移動(dòng)之后(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)茸炒,只需要改變句柄中的對(duì)象實(shí)例地址即可,reference不用修改阵苇。

使用指針訪問(wèn)的好處是訪問(wèn)速度快壁公,它減少了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于java是面向?qū)ο蟮恼Z(yǔ)言绅项,在開(kāi)發(fā)中java對(duì)象的訪問(wèn)非常的頻繁紊册,因此這類(lèi)開(kāi)銷(xiāo)積少成多也是非常可觀的快耿,反之則提升訪問(wèn)速度囊陡。對(duì)于HotSpot虛擬機(jī)來(lái)說(shuō)芳绩,使用的就是直接指針訪問(wèn)的方式。

實(shí)戰(zhàn): OutOfMemoryError異常

Java 堆溢出

Java堆用來(lái)存儲(chǔ)對(duì)象撞反,因此只要不斷創(chuàng)建對(duì)象妥色,并保證 GC Roots 到對(duì)象之間有可達(dá)路徑來(lái)避免垃圾回收機(jī)制清楚這些對(duì)象,那么當(dāng)對(duì)象數(shù)量達(dá)到最大堆容量時(shí)就會(huì)產(chǎn)生 OOM遏片。

/**
 * java堆內(nèi)存溢出測(cè)試
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {

    static class OOMObject{}

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

運(yùn)行結(jié)果:

java.lang.OutOfMemoryError: Java heap space 
Dumping heap to java_pid7164.hprof … 
Heap dump file created [27880921 bytes in 0.193 secs] 
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space 
at java.util.Arrays.copyOf(Arrays.java:2245) 
at java.util.Arrays.copyOf(Arrays.java:2219) 
at java.util.ArrayList.grow(ArrayList.java:242) 
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) 
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) 
at java.util.ArrayList.add(ArrayList.java:440) 
at com.jvm.oom.HeapOOM.main(HeapOOM.java:17)

堆內(nèi)存 OOM 是經(jīng)常會(huì)出現(xiàn)的問(wèn)題嘹害,異常信息會(huì)進(jìn)一步提示 Java heap space

虛擬機(jī)棧和本地方法棧溢出

在 HotSpot 虛擬機(jī)中不區(qū)分虛擬機(jī)棧和本地方法棧,棧容量只由 -Xss 參數(shù)設(shè)定吮便。關(guān)于虛擬機(jī)棧和本地方法棧笔呀,在 Java 虛擬機(jī)規(guī)范中描述了兩種異常:

  • 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出 StackOverflowError 異常线衫。
  • 如果虛擬機(jī)在擴(kuò)展棧時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間凿可,則拋出 OutOfMemoryError 異常。
/**
 * 虛擬機(jī)棧和本地方法棧內(nèi)存溢出測(cè)試授账,拋出stackoverflow exception
 * VM ARGS: -Xss128k 減少棧內(nèi)存容量
 */
public class JavaVMStackSOF {

    private int stackLength = 1;

    public void stackLeak () {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length = " + oom.stackLength);
            throw e;
        }

    }

}

運(yùn)行結(jié)果:

stack length = 11420 
Exception in thread “main” java.lang.StackOverflowError 
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) 
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13) 
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13) 

以上代碼在單線程環(huán)境下,無(wú)論是由于棧幀太大還是虛擬機(jī)棧容量太小惨驶,當(dāng)內(nèi)存無(wú)法分配時(shí)白热,拋出的都是 StackOverflowError 異常。

如果測(cè)試環(huán)境是多線程環(huán)境粗卜,通過(guò)不斷建立線程的方式可以產(chǎn)生內(nèi)存溢出異常屋确,代碼如下所示。但是這樣產(chǎn)生的 OOM 與椥樱空間是否足夠大不存在任何聯(lián)系攻臀,在這種情況下,為每個(gè)線程的棧分配的內(nèi)存足夠大纱昧,反而越容易產(chǎn)生OOM 異常刨啸。這點(diǎn)不難理解,每個(gè)線程分配到的棧容量越大识脆,可以建立的線程數(shù)就變少设联,建立多線程時(shí)就越容易把剩下的內(nèi)存耗盡。這點(diǎn)在開(kāi)發(fā)多線程的應(yīng)用時(shí)要特別注意灼捂。如果建立過(guò)多線程導(dǎo)致內(nèi)存溢出离例,在不能減少線程數(shù)或更換64位虛擬機(jī)的情況下,只能通過(guò)減少最大堆和減少棧容量來(lái)?yè)Q取更多的線程悉稠。

/**
 * JVM 虛擬機(jī)棧內(nèi)存溢出測(cè)試, 注意在windows平臺(tái)運(yùn)行時(shí)可能會(huì)導(dǎo)致操作系統(tǒng)假死
 * VM Args: -Xss2M -XX:+HeapDumpOnOutOfMemoryError
 */

public class JVMStackOOM {

    private void dontStop() {
        while (true) {}
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {

                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        JVMStackOOM oom = new JVMStackOOM();
        oom.stackLeakByThread();
    }
}

方法區(qū)和運(yùn)行時(shí)常量池溢出

方法區(qū)用于存放Class的相關(guān)信息宫蛆,對(duì)這個(gè)區(qū)域的測(cè)試,基本思路是運(yùn)行時(shí)產(chǎn)生大量的類(lèi)去填滿方法區(qū)的猛,直到溢出耀盗。使用CGLib實(shí)現(xiàn)辑甜。

方法區(qū)溢出也是一種常見(jiàn)的內(nèi)存溢出異常,在經(jīng)常生成大量Class的應(yīng)用中袍冷,需要特別注意類(lèi)的回收情況磷醋,這類(lèi)場(chǎng)景除了使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語(yǔ)言外,常見(jiàn)的還有JSP文件的應(yīng)用(JSP第一次運(yùn)行時(shí)要編譯為Java類(lèi))胡诗、基于OSGI的應(yīng)用等邓线。

/**
 * 測(cè)試JVM方法區(qū)內(nèi)存溢出
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class MethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args,
                        MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject{}
}

本機(jī)直接內(nèi)存溢出

DirectMemory 容量可通過(guò) -XX:MaxDirectMemorySize 指定,如不指定煌恢,則默認(rèn)與Java堆最大值一樣骇陈。測(cè)試代碼使用了 Unsafe 實(shí)例進(jìn)行內(nèi)存分配。

由 DirectMemory 導(dǎo)致的內(nèi)存溢出瑰抵,一個(gè)明顯的特征是在Heap Dump 文件中不會(huì)看見(jiàn)明顯的異常你雌,如果發(fā)現(xiàn) OOM 之后 Dump 文件很小,而程序直接或間接使用了NIO二汛,那就可以考慮檢查一下是不是這方面的原因婿崭。

/**
 * 測(cè)試本地直接內(nèi)存溢出
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author linli.cro
 */
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

課后作業(yè)

經(jīng)典面試問(wèn)題: JVM內(nèi)存區(qū)域劃分為哪些區(qū)域,以及哪些區(qū)域可能發(fā)生OutOfMemoryError肴颊?

參考

  • 《深入理解Java虛擬機(jī): JVM高級(jí)特性與最佳實(shí)踐》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末氓栈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子婿着,更是在濱河造成了極大的恐慌授瘦,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件竟宋,死亡現(xiàn)場(chǎng)離奇詭異提完,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)丘侠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)徒欣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人婉陷,你說(shuō)我怎么就攤上這事帚称。” “怎么了秽澳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵闯睹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我担神,道長(zhǎng)楼吃,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮孩锡,結(jié)果婚禮上酷宵,老公的妹妹穿的比我還像新娘。我一直安慰自己躬窜,他們只是感情好浇垦,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著荣挨,像睡著了一般男韧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上默垄,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天此虑,我揣著相機(jī)與錄音,去河邊找鬼口锭。 笑死朦前,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鹃操。 我是一名探鬼主播韭寸,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼组民!你這毒婦竟也來(lái)了棒仍?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤臭胜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后癞尚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體耸三,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年浇揩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了仪壮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡胳徽,死狀恐怖积锅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情养盗,我是刑警寧澤缚陷,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站往核,受9級(jí)特大地震影響箫爷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一虎锚、第九天 我趴在偏房一處隱蔽的房頂上張望硫痰。 院中可真熱鬧,春花似錦窜护、人聲如沸效斑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)缓屠。三九已至,卻和暖如春坐搔,著一層夾襖步出監(jiān)牢的瞬間藏研,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工概行, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蠢挡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓凳忙,卻偏偏與公主長(zhǎng)得像业踏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涧卵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容