JVM基礎(chǔ)(一) - Java內(nèi)存區(qū)域與內(nèi)存溢出異常

本文主要內(nèi)容出自周志明老師《深入理解Java虛擬機》一書较锡,是筆者結(jié)合自己的理解月杉,提取重點,重新組織排版钩杰,再補充了一些內(nèi)容后纫塌,總結(jié)的讀書筆記。

JVM運行時數(shù)據(jù)區(qū)的劃分

線程共享的數(shù)據(jù)區(qū)特征
  • 虛擬機啟動時創(chuàng)建榜苫,生命周期與進程相同
  • 內(nèi)存分配和回收是動態(tài)的护戳,GC負(fù)責(zé)的區(qū)域
線程私有的數(shù)據(jù)區(qū)特征
  • 線程啟動時創(chuàng)建翎冲,生命周期與線程相同
  • 內(nèi)存的分配和回收都具備確定性垂睬,方法結(jié)束或線程結(jié)束就回收,不需過多考慮回收問題

程序計數(shù)器(Program Counter Register)

一塊較小的內(nèi)存空間抗悍,當(dāng)前線程所執(zhí)行字節(jié)碼的行號指示器驹饺。

  • 線程私有
  • JVM 5大數(shù)據(jù)區(qū)中唯一一個沒有規(guī)定OOM的區(qū)域
  • 執(zhí)行Java方法時,計數(shù)器記錄的是字節(jié)碼指令的地址缴渊;執(zhí)行Native方法時赏壹,計數(shù)器值為空(undefined)

為什么需要程序計數(shù)器呢?

JVM 的多線程是通過線程輪流切換并分配CPU時間的方式來實現(xiàn)的衔沼,在任何一個確定的時刻蝌借,一個處理器(對于多核處理器來說是一個內(nèi)核)都只會執(zhí)行一條線程中的指令。因此指蚁,為了線程切換后能恢復(fù)到正確的執(zhí)行位置菩佑,每條線程都需要有一個獨立的程序計數(shù)器。

Java虛擬機棧(Java Virtual Machine Stacks)

虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表凝化、操作數(shù)棧稍坯、動態(tài)鏈接、方法出口等信息搓劫。每一個方法從調(diào)用直至執(zhí)行完成的過程瞧哟,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程混巧。

  • 線程私有,生命周期與線程相同
  • StackOverflowError:棧深度大于虛擬機所允許的深度
  • OutOfMemorryError:如果虛擬機椙诳可以動態(tài)擴展(大部分虛擬機可動態(tài)擴展咧党,只不過Java虛擬機規(guī)范中也允許固定長度的虛擬機棧),擴展時無法申請足夠內(nèi)存

經(jīng)常有人把Java內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack)雄可,這種分法比較粗糙凿傅,其流行只能說明大多數(shù)程序員最關(guān)注的、與對象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域是這兩塊数苫。其中所指的『堆』就是后面即將提到的Java堆聪舒,而所指的『棧』就是這里的虛擬機棧虐急,或者說是虛擬機棧中局部變量表部分箱残。

局部變量表

局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte止吁、char被辑、short、int敬惦、float盼理、long、double)俄删、對象引用(reference)和returnAddress類型(指向了一條字節(jié)碼指令的地址)宏怔。

  • 局部變量表的容量以變量槽(Variable Slot)為最小單位
  • 64位長度的 long 和 double 類型的數(shù)據(jù)占用2個slot,其余數(shù)據(jù)類型只占用1個slot
  • 局部變量表所需內(nèi)存空間在編譯期已經(jīng)確定畴椰,在方法運行期間不會改變大小
局部變量表的影響

讓我們通過以下示例代碼直觀地感受一下局部變量表的影響臊诊。第一個recursion()沒有參數(shù)和局部變量,第二個包含3個參數(shù)和4個局部變量斜脂,因此后者占用更多內(nèi)存空間抓艳,在jvm參數(shù)-Xss 128K下分別執(zhí)行兩個方法:

private static int count=0;

public static void recursion(){
    System.out.println("count="+count);
    count++;
    recursion();
}

public static void recursion(int a,int b,int c){
    long l1=12;
    short sl=1;
    byte b1=1;
    String s="1";
    System.out.println("count="+count);
    count++;
    recursion(1,2,3);
}

執(zhí)行第一個無參的recursion()的輸出:

count=4495
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)

執(zhí)行第二個有參的recursion()的輸出:

count=3865
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:564)
at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:619)

可見,在同等的棧容量下帚戳,局部變量少的函數(shù)可以支持更深的調(diào)用層次玷或,換句話說,一個線程中可調(diào)用的方法數(shù)就越多片任。

本地方法棧(Native Method Stack)

本地方法棧與虛擬機棧的作用類似偏友,區(qū)別只是前者為執(zhí)行Native方法服務(wù),后者為執(zhí)行Java方法服務(wù)蚂踊。有的虛擬機(如Sun HotSpot虛擬機)直接把本地方法棧和虛擬機棧合二為一约谈。

  • 線程私有
  • 和Java虛擬機棧一樣,也會拋出StackOverflowError 和 OutOfMemorryError

Java堆(Java Heap)

所有的對象實例以及數(shù)組都要在堆上分配,但是隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟棱诱,棧上分配泼橘、標(biāo)量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化發(fā)生,所有的對象都分配在堆上也漸漸變得不是那么"絕對"了迈勋。

  • 線程共享
  • OutOfMemorryError:Java heap space
  • GC的主要區(qū)域炬灭,因此也被稱作"GC堆"
  • JVM所管理的內(nèi)存中最大的一塊
  • 虛擬機啟動時創(chuàng)建
虛擬機規(guī)范對該區(qū)的限制
  • 可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上連續(xù)即可
  • 即可以實現(xiàn)成固定大小的靡菇,也可以是可擴展的重归,當(dāng)前主流虛擬機都是按照可擴展來實現(xiàn)的

方法區(qū)(Method Area)

用于存儲已被虛擬機加載的類信息、常量厦凤、靜態(tài)變量鼻吮、即時編譯器編譯后的代碼等數(shù)據(jù)。

  • 線程共享
  • OutOfMemorryError:PermGen spage
  • GC比較少出現(xiàn)(虛擬機實現(xiàn)時也可以選擇不實現(xiàn)GC较鼓,但事實證明該區(qū)域的GC是必要的)
  • 有一個別名叫"Non-Heap"(非堆):虛擬機規(guī)范中把方法區(qū)描述為堆的一個邏輯部分椎木,為了與Java堆區(qū)分開來
淺談“永久代”(Permanent Generation)

在HotSpot虛擬機上,很多人都更愿意把方法區(qū)稱為“永久代”博烂,但本質(zhì)上兩者并不等價香椎,僅僅是因為HotSpot虛擬機的設(shè)計團隊使用永久代來實現(xiàn)方法區(qū)而已。而對于其他虛擬機(如BEA JRockit禽篱、IBM J9等)來說是不存在永久代的概念的畜伐。目前,在HotSpot虛擬機上也有放棄永久代并逐步改為采用Native Memory來實現(xiàn)方法區(qū)的規(guī)劃了躺率,在JDK1.7的HotSpot中玛界,已經(jīng)把原本放在永久代的字符串常量池移出。

虛擬機規(guī)范對該區(qū)的限制
  • 可以處于物理上不連續(xù)的內(nèi)存空間中肥照,只要邏輯上連續(xù)即可
  • 即可以實現(xiàn)成固定大小的脚仔,也可以是可擴展的勤众,當(dāng)前主流虛擬機都是按照可擴展來實現(xiàn)的
  • 可以選擇不實現(xiàn)垃圾收集

垃圾收集行為在方法區(qū)是比較少出現(xiàn)的舆绎,但并非數(shù)據(jù)進入了方法區(qū)就如永久代的名字一樣“永久”存在了。這里的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載们颜,一般來說吕朵,這里的回收“成績”難以令人滿意,尤其是類型的卸載窥突,條件相當(dāng)苛刻努溃,但是這部分區(qū)域的回收確實是必要的

運行時常量池(Runtime Constant Pool)

運行時常量池是方法區(qū)的一部分阻问。Class文件中除了有類的版本梧税、字段、方法、接口等描述信息外第队,還有一項信息是常量池(Constant Pool Table)哮塞,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放凳谦。

直接內(nèi)存

并非虛擬機運行時數(shù)據(jù)區(qū)的一部分忆畅,也不是Java虛擬機規(guī)范定義的內(nèi)存區(qū)域。但這部分也被頻繁使用尸执,而且也可能導(dǎo)致OOM家凯。

JDK 1.4中加入的NIO類,引入了一種基于通道與緩沖區(qū)的I/O方式如失,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存绊诲,然后通過一個存儲在Java堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作。

本機直接內(nèi)存的分配不會受到Java堆大小的限制褪贵,但還是會受到本機總內(nèi)存大小以及處理器尋址空間的限制驯镊。

5大數(shù)據(jù)區(qū)對比

JVM數(shù)據(jù)區(qū) 私有/共享 創(chuàng)建時機 生命周期 垃圾收集 內(nèi)存溢出
程序計數(shù)器 線程私有 線程啟動時 與線程相同
虛擬機棧 線程私有 線程啟動時 與線程相同 StackOverflowError OutOfMemoryError
本地方法棧 線程私有 線程啟動時 與線程相同 StackOverflowError OutOfMemoryError
Java堆 線程共享 JVM啟動時 與進程相同 主要區(qū)域 OutOfMemoryError: Java heap space
方法區(qū) 線程共享 JVM啟動時 與進程相同 較少出現(xiàn) OutOfMemoryError: PermGen space

對象初探秘

對象的創(chuàng)建

在Java中,從語言層面上來看竭鞍,創(chuàng)建對象通常只是一個 new 關(guān)鍵字而已板惑,而在虛擬機中,對象(這里討論的對象僅限于普通對象偎快,不包括數(shù)組和Class對象)的創(chuàng)建又是怎樣一個過程呢冯乘?

虛擬機遇到一條 new 指令時:

  1. 執(zhí)行類加載檢查
    • 檢查指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并檢查這個符號引用代表的類是否已被加載晒夹、解析和初始化過裆馒。
    • 若沒有,則執(zhí)行相應(yīng)的類加載過程丐怯。
  2. 為新生對象分配內(nèi)存
指針碰撞

假設(shè)Java堆中內(nèi)存是絕對規(guī)整的喷好,所有用過的內(nèi)存放在一邊,空閑的內(nèi)存放在另一邊读跷,中間放著一個指針作為分界點的指示器梗搅,那所分配內(nèi)存就僅僅是把指針向著空閑內(nèi)存那邊移動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”效览。

空閑列表

如果Java堆中的內(nèi)存并不是規(guī)整的无切,已使用內(nèi)存和空閑內(nèi)存相互交錯,那就沒辦法簡單地進行指針碰撞了丐枉,虛擬機就必須維護一個列表哆键,記錄哪些內(nèi)存塊是可用的臼膏,在分配的時候從列表中找出一塊足夠大的空間劃分給對象實例嘿期,并更新列表上的記錄蛾方,這種分配方式稱為“空閑列表”。

如何選擇分配方式

選擇哪種分配方式由Java堆是否規(guī)整決定起暮,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有 壓縮整理 功能決定枢析。因此秀睛,在使用Serial笼痹、ParNew等待Compact過程的收集器時,系統(tǒng)采用的分配算法是指針碰撞识补,而使用CMS這種基于Mark-Sweep算法的收集器時族淮,通常采用空閑列表。

對象的內(nèi)存布局

在HotSpot虛擬機中凭涂,對象在內(nèi)存中存儲的布局可以分為 3 塊區(qū)域:對象頭(Header)祝辣、實例數(shù)據(jù)(Instance Data)、和對齊填充(Padding)切油。

對象頭

對象頭包括兩部分信息蝙斜,第一部分用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)澎胡、GC分代年齡孕荠、鎖狀態(tài)標(biāo)志等,官方稱這些數(shù)據(jù)為 “Mark Word” 攻谁。

對象頭的另一部分是類型指針稚伍,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過該指針來確定這個對象是哪個類的實例戚宦。但并非所有的虛擬機實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針个曙,換句話說,查找對象的元數(shù)據(jù)信息并不一定要經(jīng)過對象本身受楼。另外垦搬,如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)艳汽,因為虛擬機可以通過普通對象的元數(shù)據(jù)確定該對象的大小猴贰,但是從數(shù)組的元數(shù)據(jù)中卻無法確定數(shù)組的大小。

實例數(shù)據(jù)

實例數(shù)據(jù)是對象真正存儲的有效信息河狐,也是在程序代碼中定義的各種類型的字段內(nèi)容米绕。無論是從父類繼承下來的,還是子類中定義的甚牲,都需要記錄义郑。

對齊填充

對齊填充并不是必然存在的蝶柿,也沒有特別的含義丈钙,它僅僅起著占位符的作用。由于HotSpot VM要求對象起始地址必須是8字節(jié)的整數(shù)倍交汤,話句話說雏赦,就是對象大小必須是8字節(jié)的整數(shù)倍劫笙。而對象頭部分正好是8字節(jié)的倍數(shù),因此星岗,當(dāng)對象的實例數(shù)據(jù)部分沒有對齊時填大,就需要通過對齊填充來補全。

對象的訪問定位

Java程序需要通過棧(具體是虛擬機棧中的局部變量表)上的reference數(shù)據(jù)來操作堆上的具體對象俏橘。而reference如何定位允华、訪問堆中對象的具體位置,則取決于不同的虛擬機實現(xiàn)寥掐。目前主流的訪問方式有使用 句柄直接指針 兩種靴寂。

問題:毫無疑問,局部變量中的reference存放在棧中召耘,那么成員變量中的reference又是存放在哪里百炬?

筆者也是看到這里時感到疑惑,上網(wǎng)查證了很多污它,但是說法不一剖踊,有的認(rèn)為在棧中(一概而論:對象在堆,引用在棧)衫贬,有的認(rèn)為在堆中(比如https://blog.csdn.net/qq_36596145/article/details/76300922)德澈,筆者認(rèn)為在方法區(qū)中(具體是方法區(qū)中的運行時常量池,因為class文件中有一個常量池固惯,用于存放編譯期生成的各種字面量和符號引用圃验,這部分信息在類加載后進入方法區(qū)的運行時常量池中存放)。

如果有讀者可以給出明確的結(jié)論缝呕,還請不吝賜教澳窑!

句柄式

在Java堆中劃分出一塊內(nèi)存用作句柄池,reference中存儲對象的句柄地址供常,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息摊聋。

使用句柄訪問方式的最大好處就是reference中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是很普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針栈暇,而reference本身不需要修改麻裁。

直接指針

reference中存儲的直接就是對象地址,此時對象的布局中就必須考慮如何放置對象類型數(shù)據(jù)的指針源祈。

  • HotSpot虛擬機采用的就是這種方式煎源。

使用直接指針訪問方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時間開銷香缺。由于對象訪問在Java中非常頻繁手销,因此這類開銷積少成多后也是一項非常可觀的執(zhí)行成本图张。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锋拖,一起剝皮案震驚了整個濱河市诈悍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽埃,老刑警劉巖侥钳,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柄错,居然都是意外死亡舷夺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門售貌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冕房,“玉大人,你說我怎么就攤上這事趁矾“也幔” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵毫捣,是天一觀的道長详拙。 經(jīng)常有香客問我,道長蔓同,這世上最難降的妖魔是什么饶辙? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮斑粱,結(jié)果婚禮上弃揽,老公的妹妹穿的比我還像新娘。我一直安慰自己则北,他們只是感情好矿微,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尚揣,像睡著了一般涌矢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上快骗,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天娜庇,我揣著相機與錄音,去河邊找鬼方篮。 笑死名秀,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的藕溅。 我是一名探鬼主播匕得,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蜈垮!你這毒婦竟也來了耗跛?” 一聲冷哼從身側(cè)響起裕照,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤攒发,失蹤者是張志新(化名)和其女友劉穎调塌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惠猿,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡羔砾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了偶妖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姜凄。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖趾访,靈堂內(nèi)的尸體忽然破棺而出态秧,到底是詐尸還是另有隱情,我是刑警寧澤扼鞋,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布申鱼,位于F島的核電站,受9級特大地震影響云头,放射性物質(zhì)發(fā)生泄漏捐友。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一溃槐、第九天 我趴在偏房一處隱蔽的房頂上張望匣砖。 院中可真熱鬧,春花似錦昏滴、人聲如沸猴鲫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽变隔。三九已至,卻和暖如春蟹倾,著一層夾襖步出監(jiān)牢的瞬間匣缘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工鲜棠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肌厨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓豁陆,卻偏偏與公主長得像柑爸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盒音,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355