《java虛擬機(jī)建丧,說(shuō)點(diǎn)不一樣的》之 最全面的jvm運(yùn)行時(shí)數(shù)據(jù)區(qū)

記得有位大佬曾經(jīng)說(shuō)過(guò)這樣一句話:

如欲征服java,必須征服java虛擬機(jī)甚垦,如欲征服java虛擬機(jī)茶鹃,需先征服java虛擬機(jī)內(nèi)存模型涣雕。

java虛擬機(jī)內(nèi)存艰亮,是java虛擬機(jī)進(jìn)行對(duì)象內(nèi)存空間分配、垃圾回收的活動(dòng)室挣郭,只有先了解java虛擬機(jī)內(nèi)存才能在此基礎(chǔ)上進(jìn)一步了解對(duì)象內(nèi)存分配迄埃、垃圾回收等活動(dòng)。有別于真實(shí)物理機(jī)硬盤兑障、主存侄非、緩存、寄存器的存儲(chǔ)模型流译,java虛擬機(jī)內(nèi)存模型按照其存儲(chǔ)模塊負(fù)責(zé)的數(shù)據(jù)類型將其劃分為如下圖所示的模型:

java虛擬機(jī)內(nèi)存模型

堆是各個(gè)線程共享的內(nèi)存區(qū)域逞怨,是java對(duì)象內(nèi)存分配和垃圾回收的主戰(zhàn)場(chǎng),幾乎所有的對(duì)象都是在堆中創(chuàng)建的福澡。根據(jù)Java虛擬機(jī)規(guī)范(Java Virtual Machine Specification) 的規(guī)則叠赦,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可革砸。如果在堆中沒有內(nèi)存空間完成Java對(duì)象的內(nèi)存分配時(shí)除秀,將會(huì)拋出OutOfMemoryError(一下簡(jiǎn)稱OOM)。

關(guān)于堆的最常見虛擬機(jī)參數(shù):

  • -Xms :表示虛擬機(jī)堆的最小值算利,如 -Xms10M 表示堆的最小值為10MB
  • -Xmx :表示虛擬機(jī)堆的最大值册踩,如果 -Xmx100M 表示堆的最大值為100MB
/**
 * 設(shè)置虛擬機(jī)參數(shù)為:-Xms5M -Xmx5M
 */
public class HeapOOM {
    public static void main(String[] args) {
        ArrayList<Byte[]> bytes = new ArrayList<>();
        for (; ; ) {
            Byte[] _1M = new Byte[1024 * 1024];
            bytes.add(_1M);
        }

    }
}

執(zhí)行結(jié)果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at jvm.post1.HeapOOM.main(HeapOOM.java:15)

“Java heap space”類型的OOM表示堆中沒有可用的內(nèi)存空間,具體到本例子中就是在大小為5M的堆中沒有可用空間分配給大小為1M的數(shù)組對(duì)象效拭。再來(lái)看一個(gè)例子:

/**

 * 虛擬機(jī)參數(shù) -Xms5M -Xmx5M 
*/ 
public class HeapOOM1 {
    public static void main(String[] args) {
        ArrayList<Object> heapOOM1s = new ArrayList<>();
        for (; ; ) {
            heapOOM1s.add(new Object());
        }
    }
}

執(zhí)行結(jié)果:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at jvm.post1.HeapOOM1.main(HeapOOM1.java:14)

“GC overhead limit exceeded” 類型的OOM是在jdk6后引入的一種新的錯(cuò)誤類型暂吉。發(fā)生錯(cuò)誤的原因是虛擬機(jī)用了大量的時(shí)間進(jìn)行GC但是只釋放了較小的空間,這是虛擬機(jī)的一種保護(hù)機(jī)制缎患。具體到本例子中就是虛擬機(jī)在GC時(shí)沒有能回收內(nèi)存空間借笙,浪費(fèi)了時(shí)間卻沒有收獲,所以就拋出了這個(gè)錯(cuò)誤较锡∫导冢可以用 -XX:-UseGCOverheadLimit參數(shù)禁用這個(gè)檢查,但解決不了內(nèi)存問(wèn)題蚂蕴,只是把錯(cuò)誤的信息延后低散,替換成 java.lang.OutOfMemoryError: Java heap space錯(cuò)誤俯邓。

方法區(qū)

方法區(qū)和堆一樣,也是各個(gè)線程共享的內(nèi)存區(qū)域熔号,它用來(lái)存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息稽鞭、常量池、靜態(tài)變量等引镊。方法區(qū)是jdk5到j(luò)dk8變化較大的java虛擬機(jī)內(nèi)存區(qū)域朦蕴。在jdk5和jdk6時(shí),常量池是存在方法區(qū)的:

jdk5和jdk6

而從jdk7及其以后的版本弟头,常量池被放到了堆里面:

jdk7

常量池就是java語(yǔ)言系統(tǒng)級(jí)別的緩存吩抓,目的是讓程序在運(yùn)行過(guò)程中速度更快,更節(jié)省內(nèi)存空間赴恨,java的8種基本數(shù)據(jù)類型外加String類型疹娶,共9種類型都有對(duì)應(yīng)的常量池。這些類型的對(duì)象不可能全都放到常量池中存儲(chǔ)伦连,因此不同的類型有不同的存儲(chǔ)策略雨饺,具體到String類型的對(duì)象來(lái)說(shuō),有如下三條規(guī)則:

  • 用雙引號(hào)創(chuàng)建的對(duì)象放在常量池中惑淳,如 "Hello"额港,"Jvm"這種。
  • 用雙引號(hào)創(chuàng)建的對(duì)象相加產(chǎn)生的對(duì)象放在常量池歧焦,如 String s = "Hello" + "Jvm";移斩,這里的s對(duì)象就是放在常量池中的。
  • 調(diào)用String對(duì)象的intern方法會(huì)返回一個(gè)存放在常量池中的String對(duì)象,且兩個(gè)對(duì)象內(nèi)容相同倚舀。

再回到本篇的主題上叹哭,因?yàn)槌A砍匚恢玫淖兓诓煌膉dk版本下痕貌,下面代碼的執(zhí)行結(jié)果是不一樣的:

public class ConstantsPool {
    public static void main(String[] args) {

        String s = new String("Hello") + new String("Jvm"); //1
        String s1 = s.intern();  //2
        System.out.println(s == s1); //jdk5和jdk6中返回false风罩,jdk7及其以上版本返回true。
    }
}

在jdk7之前舵稠,程序在執(zhí)行//2處代碼之前常量池中沒有"HelloJvm"這個(gè)字符串常量超升,//2處代碼執(zhí)行時(shí),程序會(huì)在常量池中創(chuàng)建一個(gè)"HelloJvm"的字符串對(duì)象s1并返回哺徊,而常量池是在方法區(qū)的室琢。那一個(gè)在堆中的s對(duì)象和方法區(qū)中的s1對(duì)象比較地址是否相同,當(dāng)然會(huì)得到false落追。
在jdk7及其以后的版本盈滴,程序在執(zhí)行//2出代碼時(shí),發(fā)現(xiàn)常量池中同樣沒有"HelloJvm"這個(gè)對(duì)象,但因?yàn)槌A砍匾呀?jīng)遷移到堆中巢钓,常量池不需要存儲(chǔ)一個(gè)對(duì)象了病苗,程序只是簡(jiǎn)單的把s這個(gè)對(duì)象的引用在常量池中存儲(chǔ)了,此時(shí)s和s1指向的是同一個(gè)對(duì)象症汹,結(jié)果當(dāng)然是true硫朦。

上面簡(jiǎn)單介紹了jdk7中常量池的變化,而在jdk8中方法整個(gè)方法區(qū)被放到了物理機(jī)的本地內(nèi)存,同時(shí)也更名為元空間(MetaSpace):

jdk8

jdk8及其以后的版本背镇,元空間直接使用物理機(jī)的本地內(nèi)存咬展,在不加限制的情況下其最大值為本地內(nèi)存的最大可用值÷髡叮考慮到物理機(jī)上可能部署其它的應(yīng)用服務(wù)破婆,通常會(huì)給元空間加一個(gè)大小限制。

關(guān)于元空間最常見的虛擬機(jī)參數(shù)是:

  • -XX:MetaspaceSize : 表示虛擬機(jī)元空間發(fā)生MetadataGC時(shí)的初始閾值,如 -XX:MetaspaceSize=10M 表示元空間在第一次到大10M時(shí)济瓢,會(huì)發(fā)生一次MetadataGC荠割。
  • -XX:MaxMetaspaceSize : 表示虛擬機(jī)元空間的最大值為MaxMetaspaceSize妹卿,如 -XX:MaxMetaspaceSize=15M 表示元空間的最大值為15M旺矾,再大就會(huì)發(fā)生OOM異常。

關(guān)于元空間的的內(nèi)存溢出模擬夺克,我們需要借助CGLib來(lái)動(dòng)態(tài)的創(chuàng)建類箕宙,先引入如下maven依賴:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib-nodep</artifactId>
    <version>3.3.0</version>
</dependency>

具體代碼如下:

/**
 * 虛擬機(jī)參數(shù) -XX:MaxMetaspaceSize=10M 
 * @description 元空間內(nèi)存溢出
 */
public class MetaSpaceOOM {
    public static void main(String[] args) {
        BeanGenerator beanGenerator = new BeanGenerator();
        List<Class> classes = new ArrayList<>();
        for (int i=0; i<1000000000L;i++ ) {

            beanGenerator.addProperty("id"+i, Integer.class);
            Object aClass = beanGenerator.createClass();
            classes.add((Class) aClass);

        }
    }
}

執(zhí)行結(jié)果為:

Exception in thread "main" java.lang.IllegalStateException: Unable to load cache item
    at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
    at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:119)
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
    at net.sf.cglib.beans.BeanGenerator.createHelper(BeanGenerator.java:94)
    at net.sf.cglib.beans.BeanGenerator.createClass(BeanGenerator.java:85)
    at jvm.post1.MetaSpaceOOM.main(MetaSpaceOOM.java:19)
Caused by: java.lang.OutOfMemoryError: Metaspace
    at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:96)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:94)
    at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
    ... 6 more

可以看到,引起IllegalStateException異常的正是因?yàn)?Metaspace"類型的OOM錯(cuò)誤铺纽。具體原因?yàn)锽eanGenerator對(duì)象通過(guò)createClass方法不斷創(chuàng)建新的類柬帕,導(dǎo)致最大內(nèi)存為10MB的元空間沒辦法存儲(chǔ)類的信息而拋出異常。

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

虛擬機(jī)棧和本地方法棧狡门,都是線程私有的陷寝,主要用來(lái)存儲(chǔ)在線程運(yùn)行過(guò)程中的局部變量、操作數(shù)棧其馏、方法出入口等信息凤跑,這些信息是以棧幀的形式存儲(chǔ)的,虛擬機(jī)棧和本地方法棧的區(qū)別就是一個(gè)存儲(chǔ)java方法運(yùn)行時(shí)的棧幀數(shù)據(jù)一個(gè)存儲(chǔ)本地方法(native 關(guān)鍵字修飾的方法)運(yùn)行時(shí)的棧幀數(shù)據(jù)叛复。由于都是存儲(chǔ)棧幀數(shù)據(jù)仔引,兩種棧的區(qū)別不是很大,甚至在HotSpot虛擬機(jī)中褐奥,直接把這兩個(gè)合二為一咖耘,所以本小節(jié)把這兩種棧合起來(lái)說(shuō)。java程序在運(yùn)行時(shí)的棧數(shù)據(jù)結(jié)構(gòu)如下圖:

運(yùn)行時(shí)棧結(jié)構(gòu)

在介紹堆時(shí)撬码,我們?cè)f(shuō)過(guò)幾乎所有的對(duì)象都是在堆中創(chuàng)建的儿倒,這幾乎中的特例就來(lái)自于棧眶掌,對(duì)象是可以在棧上創(chuàng)建距糖,我們稱為棧上分配乐埠。


/**
 * 執(zhí)行棧上分配的虛擬機(jī)參數(shù)  -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -Xmx10M
 * 不執(zhí)行棧上分配的虛擬機(jī)參數(shù)  -XX:-DoEscapeAnalysis -XX:+EliminateAllocations -Xmx10M
 * 
 * 參數(shù)說(shuō)明:
 * DoEscapeAnalysis  : 逃逸分析随静,對(duì)于本例來(lái)說(shuō)逃逸分析可以判斷出//1處創(chuàng)建的對(duì)象是否會(huì)被本方法外的方法獲取到。
 * EliminateAllocations : 標(biāo)量替換慷吊,對(duì)于本例來(lái)說(shuō)袖裕,在逃逸分析的幫助下發(fā)現(xiàn)//1出的User對(duì)象不會(huì)逃逸出方法allo,那么消除User對(duì)象的堆內(nèi)存分配溉瓶,把它的字段改為一個(gè)個(gè)獨(dú)立的局部變量(本例中是int類型的標(biāo)量)存儲(chǔ)在線程的棧中急鳄。
 * 要模擬棧上分配,需要逃逸分析和標(biāo)量替換兩個(gè)功能都是開啟的堰酿。
 * @description 棧上分配
 */
public class StackAllocation {
    static class User{
        int i;
    }

    public static void allo() {
        User user = new User(); //1
        user.i = 4;
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000L; i++) {
            allo();
        }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
}

用不同的虛擬機(jī)參數(shù)執(zhí)行上面的代碼時(shí)疾宏,會(huì)發(fā)現(xiàn)同樣執(zhí)行1億次方法調(diào)用,棧上分配的執(zhí)行時(shí)間明顯比非棧上分配的執(zhí)行時(shí)間短触创。簡(jiǎn)單的解釋就是1億個(gè)的User對(duì)象不是被分配在堆上坎藐,這樣就避免了頻繁的GC,對(duì)性能自然有很大提升哼绑。

與棧相關(guān)的虛擬機(jī)參數(shù)主要有:

  • -Xss : 設(shè)置java線程棧的大小岩馍,如 -Xss100k 表示每個(gè)java線程棧的大小為100k。

線程棧是用來(lái)存方法的棧幀的抖韩。線程棧越大其能調(diào)用的方法深度越大蛀恩,運(yùn)行如下代碼可以印證此觀點(diǎn):


/**
 * 虛擬機(jī)參數(shù) -Xss1000K
 * @description 模擬棧內(nèi)存溢出
 */
public class StackOverFlowOOM {
    private static int num = 0;

    public static void loop(){
        num++;
        loop();
    }

    public static void main(String[] args) {
        try {
            loop();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(num);
        }
    }
}

當(dāng)Xss的值越大時(shí),程序中的num變量在棧溢出異常時(shí)的值越大茂浮。jdk8中如果不指定Xss參數(shù)的大小双谆,那么其默認(rèn)值為1MB,這也從內(nèi)存角度印證線程是一種昂貴的資源席揽,即使簡(jiǎn)單的創(chuàng)建一個(gè)線程而不分配給其處理任務(wù)顽馋,其也要占用一些內(nèi)存空間。

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

程序計(jì)數(shù)器是一塊較小的內(nèi)存空間幌羞,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器寸谜,因?yàn)椴僮飨到y(tǒng)會(huì)分配給各個(gè)線程一些時(shí)間片來(lái)運(yùn)行,當(dāng)時(shí)間片用完后新翎,就需要有程序計(jì)數(shù)器記錄線程執(zhí)行的位置程帕,用來(lái)在線程重新獲得時(shí)間片時(shí)能恢復(fù)到原來(lái)的執(zhí)行位置。從程序計(jì)數(shù)器的用途得知地啰,程序程序計(jì)數(shù)器也是線程私有的愁拭,而且也是唯一一個(gè)不會(huì)有OOM異常的虛擬機(jī)內(nèi)存區(qū)域。

篇尾小節(jié)

本篇主要簡(jiǎn)紹了java虛擬機(jī)在運(yùn)行時(shí)的各個(gè)內(nèi)存區(qū)域亏吝,簡(jiǎn)單介紹了它們的作用和內(nèi)存溢出的方式岭埠。

有任何不懂或者質(zhì)疑的地方,都?xì)g迎大家積極留言討論,留言必回惜论,一起學(xué)習(xí)進(jìn)步许赃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馆类,隨后出現(xiàn)的幾起案子混聊,更是在濱河造成了極大的恐慌,老刑警劉巖乾巧,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件句喜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡沟于,警方通過(guò)查閱死者的電腦和手機(jī)咳胃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)旷太,“玉大人展懈,你說(shuō)我怎么就攤上這事」╄担” “怎么了存崖?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嗜傅。 經(jīng)常有香客問(wèn)我金句,道長(zhǎng)檩赢,這世上最難降的妖魔是什么吕嘀? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮贞瞒,結(jié)果婚禮上偶房,老公的妹妹穿的比我還像新娘。我一直安慰自己军浆,他們只是感情好棕洋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乒融,像睡著了一般掰盘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赞季,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天愧捕,我揣著相機(jī)與錄音,去河邊找鬼申钩。 笑死次绘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播邮偎,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼管跺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了禾进?” 一聲冷哼從身側(cè)響起豁跑,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泻云,沒想到半個(gè)月后贩绕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壶愤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年淑倾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片征椒。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡娇哆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出勃救,到底是詐尸還是另有隱情碍讨,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布蒙秒,位于F島的核電站勃黍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏晕讲。R本人自食惡果不足惜覆获,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瓢省。 院中可真熱鬧弄息,春花似錦、人聲如沸勤婚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)馒胆。三九已至缨称,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間祝迂,已是汗流浹背睦尽。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留液兽,地道東北人骂删。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓掌动,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親宁玫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粗恢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

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