Java內(nèi)存區(qū)域與內(nèi)存溢出異常
1. 虛擬機(jī)自動管理機(jī)制
-
Java
- 虛擬機(jī)自動管理機(jī)制,新建對象的維護(hù)回收由虛擬機(jī)自動完成
- 不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題
- 一旦出現(xiàn)內(nèi)存泄漏和溢出,不了解虛擬機(jī)內(nèi)存使用,很難排查修正錯誤
-
C/C++
- 人為管理內(nèi)存,需要手動創(chuàng)建和維護(hù)對象
2. 運(yùn)行時數(shù)據(jù)區(qū)域
定義:Java虛擬機(jī)在執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域
-
特點(diǎn):
- 這些區(qū)域有各自的用途, 以及創(chuàng)建和銷毀的時間
- 有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動而一直存在
- 有些區(qū)域則是依賴用戶線程的啟動和結(jié)束而建立和銷毀
-
組成:
JVM組成.png-
程序計數(shù)器
- 本質(zhì)
- 一塊較小的內(nèi)存空間
- 原理
- 如果執(zhí)行一個Java方法,計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址
- 如果執(zhí)行的是本地方法,計數(shù)器值則為空(undefined)
- 作用
- 當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器,解釋器通過改變計數(shù)器的值來選取下一條字節(jié)碼指令
- 程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等都依賴計數(shù)器來完成
- 特點(diǎn)
- 線程私有的內(nèi)存,多線程情況下,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都有一個獨(dú)立的程序計數(shù)器,互不影響,獨(dú)立存儲
- 異常
- 此內(nèi)存區(qū)域是唯一一個在《Java虛擬機(jī)規(guī)范》中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域
- 本質(zhì)
-
Java虛擬機(jī)棧
-
本質(zhì)
- 線程私有的內(nèi)存空間
- 生命周期與線程同步
-
原理/作用
- 虛擬機(jī)棧描述的是Java方法執(zhí)行的線程內(nèi)存模型
- 每個方法被執(zhí)行時,Java虛擬機(jī)都會同步創(chuàng)建一棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法出后等信息
- 每一個方法被調(diào)用直至執(zhí)行完畢的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程
-
局部變量表
- 存儲內(nèi)容
- 存放編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型,boolean、byte、char、short、int、float、long、double
- 對象引用,reference類型,可能是指向?qū)ο笃鹗嫉刂返囊弥羔樆蛑赶虼韺ο蟮木浔蛘咂渌诖藢ο笙嚓P(guān)的位置
- returnAddress類型,指向了一條字節(jié)碼指令的地址
- 存儲單位
- 這些數(shù)據(jù)在局部變量表的存儲空間以局部變量槽來表示
- 64位長度的long和double類型占用兩個變量槽,其他數(shù)據(jù)只占用一個
- 變量槽分配
- 局部變量表所需的內(nèi)存空間在編譯期間完成分配
- 當(dāng)進(jìn)入一個方法時, 這個方法需要在棧幀中分配多大的局部變量空間(變量槽的數(shù)量)是完全確定的,在運(yùn)行期間不會改變
- 虛擬機(jī)真正使用多大的內(nèi)存空間來實(shí)現(xiàn)一個變量槽, 這是完全由具體的虛擬機(jī)實(shí)現(xiàn)自行決定的
- 存儲內(nèi)容
-
異常
- 《Java虛擬機(jī)規(guī)范》對此內(nèi)存區(qū)域規(guī)定了兩類異常狀況
- 如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常
- 當(dāng)Java虛擬機(jī)棧容量可以動態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時無法申請到足夠的內(nèi)存會拋出OutOfMemoryError異常;而HotSpot虛擬機(jī)的棧容量是不可以動態(tài)擴(kuò)展,所以當(dāng)線程申請棧空間失敗時,就會出現(xiàn)StackOverflowError異常
-
-
本地方法棧
- 本質(zhì)
- 線程私有的內(nèi)存空間
- 原理/作用
- 與虛擬機(jī)棧相似
- 本地方法棧和Java虛擬機(jī)棧區(qū)別
- Java虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)
- 本地方法棧為虛擬機(jī)使用到的本地方法服務(wù)
- 特點(diǎn)
- 《Java虛擬機(jī)規(guī)范》對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有任何強(qiáng)制規(guī)定
- 具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,甚至有的Java虛擬機(jī)(譬如HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一
- 異常
- 棧深度溢出,拋出StackOverflowError異常
- 棧擴(kuò)展失敗,拋出OutOfMemoryError異常
- 本質(zhì)
-
Java堆
- 本質(zhì)
- 所有線程共享的一塊內(nèi)存區(qū)域
- Java堆是垃圾收集器管理的內(nèi)存區(qū)域
- 在虛擬機(jī)啟動時創(chuàng)建
- 原理/作用
- 唯一目的是存放對象實(shí)例
- 內(nèi)存分配
- Java堆中可以劃分出多個線程私有的分配緩沖區(qū),以提升對象分配時的效率
- 內(nèi)存回收
- 采用分代收集理論設(shè)計的垃圾收集器,將Java堆劃分為新生代、老年代、永久代等
- 不采用分代設(shè)計的垃圾收集器
細(xì)分Java堆的目的只是為了更好的回收內(nèi)存,或者更快的分配內(nèi)存
- 特點(diǎn)
- 虛擬機(jī)所管理內(nèi)存中最大的一塊
- 物理上不連續(xù),邏輯上連續(xù)的內(nèi)存空間
- Java堆可實(shí)現(xiàn)為固定大小的,也可是可擴(kuò)展的(通過參數(shù)-Xmx和-Xms設(shè)定);當(dāng)前主流Java虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)
- 異常
- 如果Java堆中沒有內(nèi)存來完成實(shí)力分配,且無法再擴(kuò)展時,會拋出OutOfMemoryError異常
- 本質(zhì)
-
方法區(qū)
- 本質(zhì)
- 線程共享內(nèi)存區(qū)域
- 作用
- 存儲已被虛擬機(jī)加載的每個類的信息(包括類的名稱、方法信息、字段信息)、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)
- 內(nèi)存回收
- 該區(qū)域內(nèi)存回收主要是針對常量池的回收和對類型的卸載
- 回收效果不令人滿意,但是必要
- 特點(diǎn)
- 《Java虛擬機(jī)規(guī)范》把方法區(qū)描述為Java堆的一個邏輯部分,物理上別名為“非堆”,與堆區(qū)分開
- 不需要連續(xù)的內(nèi)存
- 可以選擇固定大小或者可擴(kuò)展
- 可選擇不實(shí)現(xiàn)垃圾收集
- 異常
- 如果方法區(qū)無法滿足新的內(nèi)存分配需求,將拋出OOM異常
- 發(fā)展
- 在JDK 6之前,HotSpot虛擬機(jī)使用永久代(Permanent
Generation)實(shí)現(xiàn)方法區(qū) - 在JDK 6時,HotSpot虛擬機(jī)開發(fā)團(tuán)隊就有放棄永久代,逐步用本地內(nèi)存(Native Memory)來實(shí)現(xiàn)方法區(qū)的計劃
- 到JDK 7,HotSpot虛擬機(jī)把原本放在永久代的字符串常量池、靜態(tài)變量等移到堆中
- 到JDK 8,HotSpot虛擬機(jī)完全廢棄永久代概念,改用在本地內(nèi)存實(shí)現(xiàn)的元空間(Metaspace)來代替,把JDK 7中永久代還剩余的內(nèi)容(主要是類的信息)全部移到元空間中
- 在JDK 6之前,HotSpot虛擬機(jī)使用永久代(Permanent
- 本質(zhì)
-
運(yùn)行時常量池
- 本質(zhì)
- 方法區(qū)的一部分
- 作用
- 存放常量池表,常量池表用于存放編譯期生成的各種字面量與符號引用
- 在類加載后這類信息被存放到方法區(qū)的運(yùn)行時常量池中
- 特點(diǎn)
- 《Java虛擬機(jī)規(guī)范》對運(yùn)行時常量池沒有做任何細(xì)節(jié)要求,可按照自己的需求來實(shí)現(xiàn)
- 一般除了保存Class文件中描述的符號引用外,還會把由符號引用翻譯出來的直接引用保存在此區(qū)域
- 具備動態(tài)性,并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常量池, 運(yùn)行期間也可以將新的常量放入池中
- 異常
- 當(dāng)常量池?zé)o法再申請到方法區(qū)內(nèi)存時會拋出OutOfMemoryError異常
- 本質(zhì)
-
直接內(nèi)存
- 本質(zhì)
- 本機(jī)內(nèi)存
- 并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分
- 也不是《Java虛擬機(jī)規(guī)范》定義的內(nèi)存區(qū)域
- 原理/作用
- NIO類的I/O方式,可以使用Native函數(shù)庫直接分配堆外內(nèi)存
- 再通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作
- 異常
- 本機(jī)直接內(nèi)存受到本機(jī)總內(nèi)存(包括物理內(nèi)存、SWAP分區(qū)或者分頁文件)大小以及處理器尋址空間的限制,當(dāng)虛擬機(jī)各內(nèi)存區(qū)域總和大于物理內(nèi)存限制,會導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn)OOM異常
- 本質(zhì)
-
3. HotSpot虛擬機(jī)對象探秘
-
對象的創(chuàng)建
-
類加載檢查
- 當(dāng)Java虛擬機(jī)遇到new指令,首先檢查該指令參數(shù)是否能在常量池中定位到一個類的符號引用
- 并檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化
-
分配內(nèi)存
為對象分配空間任務(wù)實(shí)際上等同于把一塊確定大小的內(nèi)存塊從堆里劃分出來
對象所需內(nèi)存的大小在類加載完成后便已確定
-
分配方法
- 指針碰撞,Java堆中內(nèi)存絕對規(guī)整,所有被使用過的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離
- 空閑列表,Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯在一起,虛擬機(jī)就必須維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實(shí)例,并更新列表上的記錄
選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact) 的能力決定;Serial、ParNew等收集器帶有空間壓縮整理,而CMS則沒有。
-
分配線程安全
對象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使僅僅修改一個指針?biāo)赶虻奈恢?,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對象A分配內(nèi)存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存的情況
- 一種是對分配內(nèi)存空間的動作進(jìn)行同步處理——實(shí)際上虛擬機(jī)是采用CAS配上失敗重試的方式保證更新操作的原子性
- 一種是把內(nèi)存分配的動作按照線程劃分在不同的空間之中進(jìn)行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local AllocationBuffer,TLAB),哪個線程要分配內(nèi)存,就在哪個線程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時才需要同步鎖定
虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定
-
內(nèi)存空間(不包括對象頭)初始化為零值
- 使用了TLAB的話,這一項工作也可以提前至TLAB分配時順便進(jìn)行
- 保證了對象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值
-
對象信息設(shè)置
- 對象設(shè)置,例如這個對象是哪個類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼(實(shí)際上對象的哈希碼會延后到真正調(diào)用Object::hashCode()方法時才 計算)、對象的GC分代年齡等信息
- 這些信息存放在對象的對象頭(Object Header)之中,而且根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對象頭會有不同的設(shè)置方式
-
對象初始化
- 以上流程只是完成了構(gòu)造函數(shù),所有的字段都為默認(rèn)的零值,對象需要的其他資源和狀態(tài)信息也還沒有按照預(yù)定的意圖構(gòu)造好
- 一般來說,new指令之后會接著執(zhí)行init()方法,按照程序員的意愿對對象進(jìn)行初始化,這樣一個真正可用的對象才算完全被構(gòu)造出來
-
-
對象的內(nèi)存布局
在HotSpot虛擬機(jī)里,對象在堆內(nèi)存中的存儲布局可以劃分為三個部分
-
對象頭(Header)
- 第一類是用于存儲對象自身的運(yùn)行時數(shù)據(jù)
- 包括哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等
- 這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32個比特和64個比特,官方稱它為“Mark Word”
- 對象需要存儲的運(yùn)行時數(shù)據(jù)很多,已經(jīng)超出32/64位Bitmap結(jié)構(gòu)所能記錄的最大限度
- 這些信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本
- 考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計成一個有著動態(tài)定義的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲盡量多的數(shù)據(jù),根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間
- 另外一部分是類型指針,對象指向它的類型元數(shù)據(jù)的指針
- Java虛擬機(jī)通過這個指針來確定該對象是哪個類的實(shí)例
- 不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針
- 如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)
- 第一類是用于存儲對象自身的運(yùn)行時數(shù)據(jù)
-
實(shí)例數(shù)據(jù)(Instance Data)
- 對象真正存儲的有效信息,即程序代碼里面所定義的各種類型的字段內(nèi)容
- 這部分的存儲順序會受到虛擬機(jī)分配策略參數(shù)(-XX:FieldsAllocationStyle參數(shù))和字段在Java源碼中定義順序的影響
- HotSpot虛擬機(jī)默認(rèn)的分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)
- 從以上默認(rèn)的分配策略中可以看到,相同寬度的字段總是被分配到一起存放
- 在滿足這個前提條件的情況下,在父類中定義的變量會出現(xiàn)在子類之前
- 如果HotSpot虛擬機(jī)的+XX:CompactFields參數(shù)值為true(默認(rèn)為true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節(jié)省空間
-
對齊填充(Padding)
- 不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用
- 由于HotSpot虛擬機(jī)的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,所以任何對象的大小都必須是8字節(jié)的整數(shù)倍
- 對象頭部分已經(jīng)被精心設(shè)計成正好是8字節(jié)的倍數(shù),如果對象實(shí)例數(shù)據(jù)部分沒有對齊的話,就需要通過對齊填充來補(bǔ)全
-
-
對象的訪問定位
創(chuàng)建對象自然是為了后續(xù)使用該對象,Java程序會通過棧上的reference數(shù)據(jù)來操作堆上的具體對象
-
對象訪問方式
《Java虛擬機(jī)規(guī)范》 里面只規(guī)定了reference是一個指向?qū)ο蟮囊?,并沒有定義這個引用應(yīng)該通過什么方式去定位、訪問到堆中對象的具體位置,所以對象訪問方式也是由虛擬機(jī)實(shí)現(xiàn)而定的
-
句柄。Java堆中將可能會劃分出一塊內(nèi)存來作為句柄池,棧上reference中存儲的就是對象的句柄地址,而句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息
句柄訪問模型.png -
直接指針。reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問的開銷
直接指針訪問模型.png -
句柄和直接指針的區(qū)別
- 使用句柄來訪問的最大好處就是reference中存儲的是穩(wěn)定句柄地址,在對象被移動時只會改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改
- 使用直接指針來訪問最大的好處就是速度更快,它節(jié)省了一次指針定位的時間開銷,HotSpot虛擬機(jī)主要使用直接指針的方式進(jìn)行對象訪問
-


