1、Java內(nèi)存區(qū)域與內(nèi)存溢出異常(JVM筆記)

聲明:本文集中的文章都是看了《深入理解Java虛擬機(jī)》所做的筆記,很多內(nèi)容是從書中摘抄,特此聲明。這里主要學(xué)習(xí)一些基本的概念,真正掌握可能還需要多加實(shí)踐。

一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域

注:圖片來自http://www.lxweimin.com/p/6173a467165e

1

Java虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)把它管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,根據(jù)《Java虛擬機(jī)規(guī)范(Java SE 7版)》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包含如圖所示的幾個(gè)運(yùn)行時(shí)區(qū)域。

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

程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是惟一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

1.2 Java虛擬機(jī)棧

  • 與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
  • 局部變量表存放了編譯期可知的各種基本類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用reference類型,它不同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)
  • 其中64位長度的longdouble類型的數(shù)據(jù)會(huì)占用兩個(gè)局部變量空間(Slot,其余的類型只占一個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
  • Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowEorror異常;如果虛擬機(jī)可以動(dòng)態(tài)擴(kuò)展(虛擬機(jī)棧大小也可以是固定的),如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,將拋出OutOfMemoryError異常。

1.3 本地方法棧

本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。虛擬機(jī)規(guī)范中對(duì)本地方法中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(如Sun HotSpot)直接就把本地方法棧和虛擬機(jī)棧合二為一。

1.4 Java堆

  • 對(duì)于大多數(shù)應(yīng)用來說,Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域。在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。在Java規(guī)范中的描述是:所有對(duì)象實(shí)例以及數(shù)組都要在堆上分配,但是隨著技術(shù)的發(fā)展,也不是那么絕對(duì)了。

  • Java堆是垃圾收集器的主要區(qū)域,因此很多時(shí)候也被稱為“GC堆”。從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)有Eden空間、From Survivor空間、To Survivor空間等。

  • 從內(nèi)存分配角度看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間,只要邏輯上是連續(xù)的即可。可以通過-Xmx指定可用最小內(nèi)存,通過-Xms指定可用最大內(nèi)存。

1.5 方法區(qū)

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

  • Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松,除了和堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不是先垃圾收集。垃圾收集在這個(gè)區(qū)域比較少見,但是并非進(jìn)入此區(qū)域的數(shù)據(jù)就永久存在了,這個(gè)區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,但是回收效果不是太好。

1.6 運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant poo Table),用于存放編譯器期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。

1.7 直接內(nèi)存

  • 直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。

  • JDK1.4中新加入了NIO類,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景顯著提高性能,因?yàn)楸苊饬嗽?code>Java堆和Native堆中來回復(fù)制數(shù)據(jù)。

二、HotSpot虛擬機(jī)對(duì)象探秘

2.1 對(duì)象的創(chuàng)建

在語言層面上,創(chuàng)建對(duì)象通常僅僅是一個(gè)new關(guān)鍵字而已,現(xiàn)在看在虛擬機(jī)中是怎樣的過程。

  • 虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程,這在后面詳細(xì)討論。

  • 在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全去確定。為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來,一般有兩種分配方式:

    • 指針碰撞(Bump the Point:如果堆是絕對(duì)規(guī)整的,用過的內(nèi)存和沒用過的內(nèi)存分開放置,中間使用一個(gè)指針作為分界點(diǎn)的指示器,那分配的時(shí)候只需要將指針移動(dòng)所需大小的內(nèi)存即可。
    • 空閑列表(Free List:如果堆內(nèi)存不是規(guī)整的,則需要維護(hù)一個(gè)列表來記錄哪些內(nèi)存是可用的,哪些是不可用的,在分配的時(shí)候需要根據(jù)這個(gè)表來進(jìn)行分配。
      選擇哪種分配方式有Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,使用Serial、ParNew等帶Compact過程的收集器時(shí),采用前一種;而使用CMS這種基于Mark-Sweep算法的收集器時(shí),采用后一種。
  • 在分配內(nèi)存還需要考慮一個(gè)問題,就是創(chuàng)建對(duì)象在虛擬機(jī)中是很頻繁的,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也并不是線程安全的,可能同一塊內(nèi)存會(huì)同時(shí)分配給多個(gè)線程。解決這個(gè)問題有兩種方案:

    • 一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性。
    • 另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)。哪個(gè)線程要分配內(nèi)存就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí),才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UserTLAB參數(shù)來設(shè)定。
  • 內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零(不包括對(duì)象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時(shí)進(jìn)行,這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初值就可以直接使用。

  • 之后,虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息,這些信息都存放在對(duì)象頭(Object Header中。

  • 在上面工作完成后,從虛擬機(jī)角度看,一個(gè)新對(duì)象已經(jīng)完成,但從Java程序的角度看,對(duì)象創(chuàng)建才剛剛開始——<init>方法還沒執(zhí)行,所有字段都還為零。一般來說,執(zhí)行new指令后會(huì)接著執(zhí)行<init>方法,把對(duì)象按照程序員的意思進(jìn)行初始化,這樣一個(gè)對(duì)象才算真正產(chǎn)生出來。

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

HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
對(duì)象頭包括兩部分信息:

  • 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit64bit,官方稱為"Mark Word"。其實(shí)對(duì)象要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出32位、64Bitmap能記錄的限度,但是Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的裝填復(fù)用存儲(chǔ)空間。如32HotSpot虛擬機(jī)中,如果對(duì)象處于未被鎖定的狀態(tài)下,那么25bit存儲(chǔ)對(duì)象哈希碼,4bit存儲(chǔ)對(duì)象分代年齡,2bit存儲(chǔ)鎖標(biāo)志位,1bit固定為0,而在其他狀態(tài)如下對(duì)象的存儲(chǔ)內(nèi)容如下:
存儲(chǔ)內(nèi)容 標(biāo)志位 狀態(tài)
對(duì)象哈希碼、對(duì)象的分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級(jí)鎖定
指向重量級(jí)鎖的指針 10 膨脹(重量級(jí)鎖定)
空,不需要記錄信息 11 GC標(biāo)記
偏向線程ID、偏向時(shí)間戳、對(duì)象分代年齡 01 可偏向
  • 對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,也就是說查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過對(duì)象本身,這在后面討論。如果對(duì)象是一個(gè)Java數(shù)組,那么對(duì)象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對(duì)象的元數(shù)據(jù)確定Java對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中卻不能確定數(shù)組的大小。

  • 對(duì)象頭之后便是實(shí)例數(shù)據(jù)部分。即對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中定義的各種類型的字段內(nèi)容(父類繼承的、子類中定義的都要記錄)。存儲(chǔ)的策略受到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。默認(rèn)分配策略為long/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),相同寬度的字段總是被分配在一起。在滿足這個(gè)條件下,在父類中定義的變量會(huì)在子類之前。如果CompactFields參數(shù)值為true(默認(rèn)),那么子類中較窄的變量可能會(huì)插入到父類變量的空隙中。

  • 第三部分對(duì)齊填充不是必然存在的,也沒有特別含義,僅僅起著占位符的作用。由于虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是把八字節(jié)的整數(shù)倍,即對(duì)象的大小必須是八字節(jié)的整數(shù)倍,如果對(duì)象不夠八字節(jié)的整數(shù)倍則需要填充對(duì)齊。

2.3 對(duì)象的訪問定位

建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊茫]有定義這個(gè)引用應(yīng)該通過何種方式去定位、訪問堆中的對(duì)象的具體位置,所以對(duì)象訪問方式也取決于虛擬機(jī)實(shí)現(xiàn)而定的。目前主流的訪問方式有使用句柄和直接指針兩種。

  • 如果使用句柄訪問的話,那么Java堆中將會(huì)劃分出一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例類型與類型數(shù)據(jù)各自的具體地址信息。如圖所示。

    2

  • 如果使用直接指針訪問,那么Java堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而reference中存儲(chǔ)的直接就是對(duì)象地址。如圖所示。

    3

這兩種對(duì)象訪問方式各有優(yōu)勢,使用句柄來訪問最大的好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改。使用直接指針訪問方式最大的好處就是速度更快。對(duì)于Sun HotSpot而言,使用的是第二種方式進(jìn)行對(duì)象訪問的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,312評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,410評(píng)論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,778評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,955評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,521評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,266評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,468評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,696評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評(píng)論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,193評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,431評(píng)論 2 378

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