国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常

這篇具有很好參考價(jià)值的文章主要介紹了深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

一、前言

????????對(duì)于Java程序員來說,在虛擬機(jī)自動(dòng)內(nèi)存管理機(jī)制的幫助下,不再需要為每一個(gè)new操作去寫配對(duì)的delete/free代碼,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題,看起來由虛擬機(jī)管理內(nèi)存一切都很美好。不過,也正是因?yàn)镴ava程序員把控制內(nèi)存的權(quán)力交給了Java虛擬機(jī),一旦出現(xiàn)內(nèi)存泄漏和溢出方面的問題,如果不了解虛擬機(jī)是怎樣使用內(nèi)存的,那排查錯(cuò)誤、修正問題將會(huì)成為一項(xiàng)異常艱難的工作。

????????將從概念上介紹Java虛擬機(jī)內(nèi)存的各個(gè)區(qū)域,講解這些區(qū)域的作用、服務(wù)對(duì)象以及其中可能產(chǎn)生的問題,這也是翻越虛擬機(jī)內(nèi)存管理這堵圍墻的第一步。

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

????????Java虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域有各自的用途,以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而一直存在,有些區(qū)域則是依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域,如圖所示。

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常,JVM,java,JVM,Java虛擬機(jī),內(nèi)存劃分

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

????????程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在Java虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。

????????由于Java虛擬機(jī)的多線程是通過線程輪流切換、分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤?strong>線程私有”的內(nèi)存。

????????如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是本地(Native)方法,這個(gè)計(jì)數(shù)器值則應(yīng)為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在《Java虛擬機(jī)規(guī)范》中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

2.2、Java虛擬機(jī)棧

????????與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stack)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的線程內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候,Java虛擬機(jī)都會(huì)同步創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完畢的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。

????????經(jīng)常有人把Java內(nèi)存區(qū)域籠統(tǒng)地劃分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種劃分方式直接繼承自傳統(tǒng)的C、C++程序的內(nèi)存布局結(jié)構(gòu),在Java語言里就顯得有些粗糙了,實(shí)際的內(nèi)存區(qū)域劃分要比這更復(fù)雜。不過這種劃分方式的流行也間接說明了程序員最關(guān)注的、與對(duì)象內(nèi)存分配關(guān)系最密切的區(qū)域是“堆”和“棧”兩塊。其中,“堆”在稍后會(huì)專門講述,而“?!蓖ǔ>褪侵高@里講的虛擬機(jī)棧,或者更多的情況下只是指虛擬機(jī)棧中局部變量表部分。

????????局部變量表存放了編譯期可知的各種Java虛擬機(jī)基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它并不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。

????????這些數(shù)據(jù)類型在局部變量表中的存儲(chǔ)空間以局部變量槽(Slot)來表示,其中64位長度的long和double類型的數(shù)據(jù)會(huì)占用兩個(gè)變量槽,其余的數(shù)據(jù)類型只占用一個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。請(qǐng)讀者注意,這里說的“大小”是指變量槽的數(shù)量,虛擬機(jī)真正使用多大的內(nèi)存空間(譬如按照1個(gè)變量槽占用32個(gè)比特、64個(gè)比特,或者更多)來實(shí)現(xiàn)一個(gè)變量槽,這是完全由具體的虛擬機(jī)實(shí)現(xiàn)自行決定的事情。

????????在《Java虛擬機(jī)規(guī)范》中,對(duì)這個(gè)內(nèi)存區(qū)域規(guī)定了兩類異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機(jī)棧容量可以動(dòng)態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存會(huì)拋出OutOfMemoryError異常。

注釋:HotSpot虛擬機(jī)的棧容量是不可以動(dòng)態(tài)擴(kuò)展的,以前的Classic虛擬機(jī)倒是可以。所以在HotSpot虛擬機(jī)上是不會(huì)由于虛擬機(jī)棧無法擴(kuò)展而導(dǎo)致OutOfMemoryError異?!灰€程申請(qǐng)??臻g成功了就不會(huì)有OOM,但是如果申請(qǐng)時(shí)就失敗,仍然是會(huì)出現(xiàn)OOM異常的。

2.3、本地方法棧

????????本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。

????????《Java虛擬機(jī)規(guī)范》對(duì)本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有任何強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以根據(jù)需要自由實(shí)現(xiàn)它,甚至有的Java虛擬機(jī)(譬如Hot-Spot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧也會(huì)在棧深度溢出或者棧擴(kuò)展失敗時(shí)分別拋出StackOverflowErrorOutOfMemoryError異常。

2.4、Java堆

????????對(duì)于Java應(yīng)用程序來說,Java堆(Java Heap)是虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,Java世界里“幾乎”所有的對(duì)象實(shí)例都在這里分配內(nèi)存。在《Java虛擬機(jī)規(guī)范》中對(duì)Java堆的描述是:“所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在堆上分配”,而這里筆者寫的“幾乎”是指從實(shí)現(xiàn)角度來看,隨著Java語言的發(fā)展,現(xiàn)在已經(jīng)能看到些許跡象表明日后可能出現(xiàn)值類型的支持,即使只考慮現(xiàn)在,由于即時(shí)編譯技術(shù)的進(jìn)步,尤其是逃逸分析技術(shù)的日漸強(qiáng)大,棧上分配、標(biāo)量替換優(yōu)化手段已經(jīng)導(dǎo)致一些微妙的變化悄然發(fā)生,所以說Java對(duì)象實(shí)例都分配在堆上也漸漸變得不是那么絕對(duì)了。

????????Java堆是垃圾收集器管理的內(nèi)存區(qū)域,因此一些資料中它也被稱作“GC堆”(Garbage Collected Heap,幸好國內(nèi)沒翻譯成“垃圾堆”)。從回收內(nèi)存的角度看,由于現(xiàn)代垃圾收集器大部分都是基于分代收集理論設(shè)計(jì)的,所以Java堆中經(jīng)常會(huì)出現(xiàn)“新生代”“老年代”“永久代”“Eden空間”“From Survivor空間”“To Survivor空間”等名詞,這些概念在后續(xù)還會(huì)反復(fù)登場亮相,在這里筆者想先說明的是這些區(qū)域劃分僅僅是一部分垃圾收集器的共同特性或者說設(shè)計(jì)風(fēng)格而已,而非某個(gè)Java虛擬機(jī)具體實(shí)現(xiàn)的固有內(nèi)存布局,更不是《Java虛擬機(jī)規(guī)范》里對(duì)Java堆的進(jìn)一步細(xì)致劃分。不少資料上經(jīng)常寫著類似于“Java虛擬機(jī)的堆內(nèi)存分為新生代、老年代、永久代、Eden、Survivor……”這樣的內(nèi)容。在十年之前(以G1收集器的出現(xiàn)為分界),作為業(yè)界絕對(duì)主流的HotSpot虛擬機(jī),它內(nèi)部的垃圾收集器全部都基于“經(jīng)典分代”來設(shè)計(jì),需要新生代、老年代收集器搭配才能工作,在這種背景下,上述說法還算是不會(huì)產(chǎn)生太大歧義。但是到了今天,垃圾收集器技術(shù)與十年前已不可同日而語,HotSpot里面也出現(xiàn)了不采用分代設(shè)計(jì)的新垃圾收集器,再按照上面的提法就有很多需要商榷的地方了。

????????如果從分配內(nèi)存的角度看,所有線程共享的Java堆中可以劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB),以提升對(duì)象分配時(shí)的效率。不過無論從什么角度,無論如何劃分,都不會(huì)改變Java堆中存儲(chǔ)內(nèi)容的共性,無論是哪個(gè)區(qū)域,存儲(chǔ)的都只能是對(duì)象的實(shí)例,將Java堆細(xì)分的目的只是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存。在此,我們僅僅針對(duì)內(nèi)存區(qū)域的作用進(jìn)行討論。

????????根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的,這點(diǎn)就像我們用磁盤空間去存儲(chǔ)文件一樣,并不要求每個(gè)文件都連續(xù)存放。但對(duì)于大對(duì)象(典型的如數(shù)組對(duì)象),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于實(shí)現(xiàn)簡單、存儲(chǔ)高效的考慮,很可能會(huì)要求連續(xù)的內(nèi)存空間。

????????Java堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過參數(shù)-Xmx和-Xms設(shè)定)。如果在Java堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),Java虛擬機(jī)將會(huì)拋出OutOfMemoryError異常。

2.5、方法區(qū)

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

????????說到方法區(qū),不得不提一下“永久代”這個(gè)概念,尤其是在JDK 8以前,許多Java程序員都習(xí)慣在HotSpot虛擬機(jī)上開發(fā)、部署程序,很多人都更愿意把方法區(qū)稱呼為“永久代”(Permanent
Generation),或?qū)烧呋鞛橐徽劇1举|(zhì)上這兩者并不是等價(jià)的,因?yàn)閮H僅是當(dāng)時(shí)的HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把收集器的分代設(shè)計(jì)擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣使得HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。但是對(duì)于其他虛擬機(jī)實(shí)現(xiàn),譬如BEA JRockit、IBM J9等來說,是不存在永久代的概念的。原則上如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受《Java虛擬機(jī)規(guī)范》管束,并不要求統(tǒng)一。但現(xiàn)在回頭來看,當(dāng)年使用永久代來實(shí)現(xiàn)方法區(qū)的決定并不是一個(gè)好主意,這種設(shè)計(jì)導(dǎo)致了Java應(yīng)用更容易遇到內(nèi)存溢出的問題(永久代有-XX:MaxPermSize的上限,即使不設(shè)置也有默認(rèn)大小,而J9和JRockit只要沒有觸碰到進(jìn)程可用內(nèi)存的上限,例如32位系統(tǒng)中的4GB限制,就不會(huì)出問題),而且有極少數(shù)方法(例如String::intern())會(huì)因永久代的原因而導(dǎo)致不同虛擬機(jī)下有不同的表現(xiàn)。當(dāng)Oracle收購BEA獲得了JRockit的所有權(quán)后,準(zhǔn)備把JRockit中的優(yōu)秀功能,譬如Java Mission Control管理工具,移植到HotSpot虛擬機(jī)時(shí),但因?yàn)閮烧邔?duì)方法區(qū)實(shí)現(xiàn)的差異而面臨諸多困難??紤]到HotSpot未來的發(fā)展,在JDK 6的時(shí)候HotSpot開發(fā)團(tuán)隊(duì)就有放棄永久代,逐步改為采用本地內(nèi)存(Native Memory)來實(shí)現(xiàn)方法區(qū)的計(jì)劃了,到了JDK 7的HotSpot,已經(jīng)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Meta-space)來代替,把JDK 7中永久代還剩余的內(nèi)容(主要是類型信息)全部移到元空間中。

????????《Java虛擬機(jī)規(guī)范》對(duì)方法區(qū)的約束是非常寬松的,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,甚至還可以選擇不實(shí)現(xiàn)垃圾收集。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域的確是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣“永久”存在了。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,一般來說這個(gè)區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收有時(shí)又確實(shí)是必要的。以前Sun公司的Bug列表中,曾出現(xiàn)過的若干個(gè)嚴(yán)重的Bug就是由于低版本的HotSpot虛擬機(jī)對(duì)此區(qū)域未完全回收而導(dǎo)致內(nèi)存泄漏。

????????根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。

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

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

????????Java虛擬機(jī)對(duì)于Class文件每一部分(自然也包括常量池)的格式都有嚴(yán)格規(guī)定,如每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范上的要求才會(huì)被虛擬機(jī)認(rèn)可、加載和執(zhí)行,但對(duì)于運(yùn)行時(shí)常量池,《Java虛擬機(jī)規(guī)范》并沒有做任何細(xì)節(jié)的要求,不同提供商實(shí)現(xiàn)的虛擬機(jī)可以按照自己的需要來實(shí)現(xiàn)這個(gè)內(nèi)存區(qū)域,不過一般來說,除了保存Class文件中描述的符號(hào)引用外,還會(huì)把由符號(hào)引用翻譯出來的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中。

????????運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是說,并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是String類的intern()方法。

????????既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。

2.7、直接內(nèi)存

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

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

????????顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到Java堆大小的限制,但是,既然是內(nèi)存,則肯定還是會(huì)受到本機(jī)總內(nèi)存(包括物理內(nèi)存、SWAP分區(qū)或者分頁文件)大小以及處理器尋址空間的限制,一般服務(wù)器管理員配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存去設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略掉直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。

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

????????介紹完Java虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū)域之后,我們大致明白了Java虛擬機(jī)內(nèi)存模型的概況,相信讀者了解過內(nèi)存中放了什么,也許就會(huì)更進(jìn)一步想了解這些虛擬機(jī)內(nèi)存中數(shù)據(jù)的其他細(xì)節(jié),譬如它們是如何創(chuàng)建、如何布局以及如何訪問的。對(duì)于這樣涉及細(xì)節(jié)的問題,必須把討論范圍限定在具體的虛擬機(jī)和集中在某一個(gè)內(nèi)存區(qū)域上才有意義?;趯?shí)用優(yōu)先的原則,筆者以最常用的虛擬機(jī)HotSpot和最常用的內(nèi)存區(qū)域Java堆為例,深入探討一下HotSpot虛擬機(jī)在Java堆中對(duì)象分配、布局和訪問的全過程。

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

????????Java是一門面向?qū)ο蟮木幊陶Z言,Java程序運(yùn)行過程中無時(shí)無刻都有對(duì)象被創(chuàng)建出來。在語言層面上,創(chuàng)建對(duì)象通常(例外:復(fù)制、反序列化)僅僅是一個(gè)new關(guān)鍵字而已,而在虛擬機(jī)中,對(duì)象(文中討論的對(duì)象限于普通Java對(duì)象,不包括數(shù)組和Class對(duì)象等)的創(chuàng)建又是怎樣一個(gè)過程呢?

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

????????在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)實(shí)際上便等同于把一塊確定大小的內(nèi)存塊從Java堆中劃分出來。假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有被使用過的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間方向挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump The Pointer)。但如果Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)在一起,那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表(Free List)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定。因此,當(dāng)使用Serial、ParNew等帶壓縮整理過程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,既簡單又高效;而當(dāng)使用CMS這種基于清除(Sweep)算法的收集器時(shí),理論上就只能采用較為復(fù)雜的空閑列表來分配內(nèi)存。

????????除如何劃分可用空間之外,還有另外一個(gè)需要考慮的問題:對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使僅僅修改一個(gè)指針?biāo)赶虻奈恢?,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒來得及修改,對(duì)象B又同時(shí)使用了原來的指針來分配內(nèi)存的情況。解決這個(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è)線程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時(shí)才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定。

????????內(nèi)存分配完成之后,虛擬機(jī)必須將分配到的內(nèi)存空間(但不包括對(duì)象頭)都初始化為零值,如果使用了TLAB的話,這一項(xiàng)工作也可以提前至TLAB分配時(shí)順便進(jìn)行。這步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。

????????接下來,Java虛擬機(jī)還要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼(實(shí)際上對(duì)象的哈希碼會(huì)延后到真正調(diào)用Object::hashCode()方法時(shí)才計(jì)算)、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。

????????在上面工作都完成之后,從虛擬機(jī)的視角來看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了。但是從Java程序的視角看來,對(duì)象創(chuàng)建才剛剛開始——構(gòu)造函數(shù),即Class文件中的<init>()方法還沒有執(zhí)行,所有的字段都為默認(rèn)的零值,對(duì)象需要的其他資源和狀態(tài)信息也還沒有按照預(yù)定的意圖構(gòu)造好。一般來說(由字節(jié)碼流中new指令后面是否跟隨invokespecial指令所決定,Java編譯器會(huì)在遇到new關(guān)鍵字的地方同時(shí)生成這兩條字節(jié)碼指令,但如果直接通過其他方式產(chǎn)生的則不一定如此),new指令之后會(huì)接著執(zhí)行<init>()方法,按照程序員的意愿對(duì)對(duì)象進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全被構(gòu)造出來。

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

????????在HotSpot虛擬機(jī)里,對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可以劃分為三個(gè)部分:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)對(duì)齊填充(Padding)。

????????HotSpot虛擬機(jī)對(duì)象的對(duì)象頭部分包括兩類信息。第一類是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32個(gè)比特和64個(gè)比特,官方稱它為“Mark Word”。對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了32、64位Bitmap結(jié)構(gòu)所能記錄的最大限度,但對(duì)象頭里的信息是與對(duì)象自身定義的數(shù)據(jù)無關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個(gè)有著動(dòng)態(tài)定義的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲(chǔ)盡量多的數(shù)據(jù),根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在32位的HotSpot虛擬機(jī)中,如對(duì)象未被同步鎖鎖定的狀態(tài)下,Mark Word的32個(gè)比特存儲(chǔ)空間中的25個(gè)比特用于存儲(chǔ)對(duì)象哈希碼,4個(gè)比特用于存儲(chǔ)對(duì)象分代年齡,2個(gè)比特用于存儲(chǔ)鎖標(biāo)志位,1個(gè)比特固定為0,在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容如表所示。

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常,JVM,java,JVM,Java虛擬機(jī),內(nèi)存劃分

????????對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類型元數(shù)據(jù)的指針,Java虛擬機(jī)通過這個(gè)指針來確定該對(duì)象是哪個(gè)類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,換句話說,查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過對(duì)象本身,這點(diǎn)我們會(huì)在后續(xù)具體討論。此外,如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是如果數(shù)組的長度是不確定的,將無法通過元數(shù)據(jù)中的信息推斷出數(shù)組的大小。

????????接下來實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,即我們?cè)诔绦虼a里面所定義的各種類型的字段內(nèi)容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(-XX:FieldsAllocationStyle參數(shù))和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)的分配順序?yàn)閘ongs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上默認(rèn)的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個(gè)前提條件的情況下,在父類中定義的變量會(huì)出現(xiàn)在子類之前。如果HotSpot虛擬機(jī)的+XX:CompactFields參數(shù)值為true(默認(rèn)就為true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節(jié)省出一點(diǎn)點(diǎn)空間。

????????對(duì)象的第三部分是對(duì)齊填充,這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍。對(duì)象頭部分已經(jīng)被精心設(shè)計(jì)成正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,如果對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊的話,就需要通過對(duì)齊填充來補(bǔ)全。

3.3、對(duì)象的訪問定位

????????創(chuàng)建對(duì)象自然是為了后續(xù)使用該對(duì)象,我們的Java程序會(huì)通過棧上的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ù)與類型數(shù)據(jù)各自具體的地址信息,其結(jié)構(gòu)如圖所示。

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常,JVM,java,JVM,Java虛擬機(jī),內(nèi)存劃分

????????如果使用直接指針訪問的話,Java堆中對(duì)象的內(nèi)存布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如果只是訪問對(duì)象本身的話,就不需要多一次間接訪問的開銷,如圖所示。

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常,JVM,java,JVM,Java虛擬機(jī),內(nèi)存劃分

這兩種對(duì)象訪問方式各有優(yōu)勢(shì):

????????使用句柄來訪問的最大好處就是reference中存儲(chǔ)的是穩(wěn)定句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改。

????????使用直接指針來訪問最大的好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象訪問在Java中非常頻繁,因此這類開銷積少成多也是一項(xiàng)極為可觀的執(zhí)行成本。

????????就討論的主要虛擬機(jī)HotSpot而言,它主要使用第二種方式進(jìn)行對(duì)象訪問(有例外情況,如果使用了Shenandoah收集器的話也會(huì)有一次額外的轉(zhuǎn)發(fā)),但從整個(gè)軟件開發(fā)的范圍來看,在各種語言、框架中使用句柄來訪問的情況也十分常見。

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

????????在《Java虛擬機(jī)規(guī)范》的規(guī)定里,除了程序計(jì)數(shù)器外,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都有發(fā)生OutOfMemoryError(下文稱OOM)異常的可能,本節(jié)將通過若干實(shí)例來驗(yàn)證異常實(shí)際發(fā)生的代碼場景,并且將初步介紹若干最基本的與自動(dòng)內(nèi)存管理子系統(tǒng)相關(guān)的HotSpot虛擬機(jī)參數(shù)。

IDEA中設(shè)置JVM運(yùn)行參數(shù):

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常,JVM,java,JVM,Java虛擬機(jī),內(nèi)存劃分

深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常,JVM,java,JVM,Java虛擬機(jī),內(nèi)存劃分

4.1、Java堆溢出

????????Java堆用于儲(chǔ)存對(duì)象實(shí)例,我們只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象,那么隨著對(duì)象數(shù)量的增加,總?cè)萘坑|及最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。

? ? ? ? 下面代碼清單限制Java堆的大小為20MB,不可擴(kuò)展(將堆的最小值-Xms參數(shù)與最大值-Xmx參數(shù)設(shè)置為一樣即可避免堆自動(dòng)擴(kuò)展),通過參數(shù)-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常的時(shí)候Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便進(jìn)行事后分析。

public class HeapOOM
{

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

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

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]

????????Java堆內(nèi)存的OutOfMemoryError異常是實(shí)際應(yīng)用中最常見的內(nèi)存溢出異常情況。出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆棧信息“java.lang.OutOfMemoryError”會(huì)跟隨進(jìn)一步提示“Java heap space”。

????????要解決這個(gè)內(nèi)存區(qū)域的異常,常規(guī)的處理方法是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對(duì)Dump出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析。第一步首先應(yīng)確認(rèn)內(nèi)存中導(dǎo)致OOM的對(duì)象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。

????????如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對(duì)象到GC Roots的引用鏈,找到泄漏對(duì)象是通過怎樣的引用路徑、與哪些GC Roots相關(guān)聯(lián),才導(dǎo)致垃圾收集器無法回收它們,根據(jù)泄漏對(duì)象的類型信息以及它到GC Roots引用鏈的信息,一般可以比較準(zhǔn)確地定位到這些對(duì)象創(chuàng)建的位置,進(jìn)而找出產(chǎn)生內(nèi)存泄漏的代碼的具體位置。

????????如果不是內(nèi)存泄漏,換句話說就是內(nèi)存中的對(duì)象確實(shí)都是必須存活的,那就應(yīng)當(dāng)檢查Java虛擬機(jī)的堆參數(shù)(-Xmx與-Xms)設(shè)置,與機(jī)器的內(nèi)存對(duì)比,看看是否還有向上調(diào)整的空間。再從代碼上檢查是否存在某些對(duì)象生命周期過長、持有狀態(tài)時(shí)間過長、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn)行期的內(nèi)存消耗。

????????以上是處理Java堆內(nèi)存問題的簡略思路,處理這些問題所需要的知識(shí)、工具與經(jīng)驗(yàn)是后面我們將會(huì)針對(duì)具體的虛擬機(jī)實(shí)現(xiàn)、具體的垃圾收集器和具體的案例來進(jìn)行分析,這里就先暫不展開。

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

????????由于HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧,因此對(duì)于HotSpot來說,-Xoss參數(shù)(設(shè)置本地方法棧大小)雖然存在,但實(shí)際上是沒有任何效果的,棧容量只能由-Xss參數(shù)來設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在《Java虛擬機(jī)規(guī)范》中描述了兩種異常:

  • 1)如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。
  • 2)如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展棧容量無法申請(qǐng)到足夠的內(nèi)存時(shí),將拋出OutOfMemoryError異常。

????????《Java虛擬機(jī)規(guī)范》明確允許Java虛擬機(jī)實(shí)現(xiàn)自行選擇是否支持棧的動(dòng)態(tài)擴(kuò)展,而HotSpot虛擬機(jī)的選擇是不支持?jǐn)U展,所以除非在創(chuàng)建線程申請(qǐng)內(nèi)存時(shí)就因無法獲得足夠內(nèi)存而出現(xiàn)OutOfMemoryError異常,否則在線程運(yùn)行時(shí)是不會(huì)因?yàn)閿U(kuò)展而導(dǎo)致內(nèi)存溢出的,只會(huì)因?yàn)闂H萘繜o法容納新的棧幀而導(dǎo)致StackOverflowError異常。

????????為了驗(yàn)證這點(diǎn),我們可以做兩個(gè)實(shí)驗(yàn),先將實(shí)驗(yàn)范圍限制在單線程中操作,嘗試下面兩種行為是否能讓HotSpot虛擬機(jī)產(chǎn)生OutOfMemoryError異常:

1、使用-Xss參數(shù)減少棧內(nèi)存容量。

結(jié)果:拋出StackOverflowError異常,異常出現(xiàn)時(shí)輸出的堆棧深度相應(yīng)縮小。

2、定義了大量的本地變量,增大此方法幀中本地變量表的長度。

結(jié)果:拋出StackOverflowError異常,異常出現(xiàn)時(shí)輸出的堆棧深度相應(yīng)縮小。

?首先,對(duì)第一種情況進(jìn)行測(cè)試,具體如代碼清單所示。

    /**
     * VM Args:-Xss128k
     */
    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:2402
Exception in thread "main" java.lang.StackOverflowError
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:20)
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)
at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)
……后續(xù)異常堆棧信息省略

????????對(duì)于不同版本的Java虛擬機(jī)和不同的操作系統(tǒng),棧容量最小值可能會(huì)有所限制,這主要取決于操作系統(tǒng)內(nèi)存分頁大小。譬如上述方法中的參數(shù)-Xss128k可以正常用于32位Windows系統(tǒng)下的JDK 6,但是如果用于64位Windows系統(tǒng)下的JDK 11,則會(huì)提示棧容量最小不能低于180K,而在Linux下這個(gè)值則可能是228K,如果低于這個(gè)最小限制,HotSpot虛擬器啟動(dòng)時(shí)會(huì)給出如下提示:

The Java thread stack size specified is too small. Specify at least 228k

第二種情況此處不做驗(yàn)證,感興趣可以自己寫一個(gè)自定義長串的變量進(jìn)行測(cè)試。

????????出現(xiàn)StackOverflowError異常時(shí),會(huì)有明確錯(cuò)誤堆??晒┓治觯鄬?duì)而言比較容易定位到問題所在。如果使用HotSpot虛擬機(jī)默認(rèn)參數(shù),棧深度在大多數(shù)情況下(因?yàn)槊總€(gè)方法壓入棧的幀大小并不是一樣的,所以只能說大多數(shù)情況下)到達(dá)1000~2000是完全沒有問題,對(duì)于正常的方法調(diào)用(包括不能做尾遞歸優(yōu)化的遞歸調(diào)用),這個(gè)深度應(yīng)該完全夠用了。但是,如果是建立過多線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)量或者更換64位虛擬機(jī)的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。這種通過“減少內(nèi)存”的手段來解決內(nèi)存溢出的方式,如果沒有這方面處理經(jīng)驗(yàn),一般比較難以想到,這一點(diǎn)讀者需要在開發(fā)32位系統(tǒng)的多線程應(yīng)用時(shí)注意。也是由于這種問題較為隱蔽,從JDK 7起,以上提示信息中“unable to create native thread”后面,虛擬機(jī)會(huì)特別注明原因可能是“possibly?out of memory or process/resource limits reached”。

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

????????由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起進(jìn)行。前面曾經(jīng)提到HotSpot從JDK 7開始逐步“去永久代”的計(jì)劃,并在JDK 8中完全使用元空間來代替永久代的背景故事,在此我們就以測(cè)試代碼來觀察一下,使用“永久代”還是“元空間”來實(shí)現(xiàn)方法區(qū),對(duì)程序有什么實(shí)際的影響。

????????String::intern()是一個(gè)本地方法,它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象的引用;否則,會(huì)將此String對(duì)象包含的字符串添加到常量池中,并且返回此String對(duì)象的引用。在JDK 6或更早之前的HotSpot虛擬機(jī)中,常量池都是分配在永久代中,我們可以通過-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可間接限制其中常量池的容量,具體實(shí)現(xiàn)如代碼清單所示,請(qǐng)讀者測(cè)試時(shí)首先以JDK 6來運(yùn)行代碼。

/**
 * VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用Set保持著常量池引用,避免Full GC回收常量池行為
        Set<String> set = new HashSet<String>();
        // 在short范圍內(nèi)足以讓6MB的PermSize產(chǎn)生OOM了
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}

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

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)

????????從運(yùn)行結(jié)果中可以看到,運(yùn)行時(shí)常量池溢出時(shí),在OutOfMemoryError異常后面跟隨的提示信息是“PermGen space”,說明運(yùn)行時(shí)常量池的確是屬于方法區(qū)(即JDK 6的HotSpot虛擬機(jī)中的永久代)的一部分。

????????而使用JDK 7或更高版本的JDK來運(yùn)行這段程序并不會(huì)得到相同的結(jié)果,無論是在JDK 7中繼續(xù)使用-XX:MaxPermSize參數(shù)或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數(shù)把方法區(qū)容量同樣限制在6MB,也都不會(huì)重現(xiàn)JDK 6中的溢出異常,循環(huán)將一直進(jìn)行下去,永不停歇。出現(xiàn)這種變化,是因?yàn)樽訨DK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版本,限制方法區(qū)的容量對(duì)該測(cè)試用例來說是毫無意義的。這時(shí)候使用-Xmx參數(shù)限制最大堆到6MB就能夠看到以下兩種運(yùn)行結(jié)果之一,具體取決于哪里的對(duì)象分配時(shí)產(chǎn)生了溢出:

// OOM異常一:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.lang.Integer.toString(Integer.java:440)
at java.base/java.lang.String.valueOf(String.java:3058)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)

// OOM異常二:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.HashMap.resize(HashMap.java:699)
at java.base/java.util.HashMap.putVal(HashMap.java:658)
at java.base/java.util.HashMap.put(HashMap.java:607)
at java.base/java.util.HashSet.add(HashSet.java:220)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)

????????方法區(qū)的主要職責(zé)是用于存放類型的相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。對(duì)于這部分區(qū)域的測(cè)試,基本的思路是運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),直到溢出為止。

????????在JDK 8以后,永久代便完全退出了歷史舞臺(tái),元空間作為其替代者登場。在默認(rèn)設(shè)置下,那些正常的動(dòng)態(tài)創(chuàng)建新類型的測(cè)試用例已經(jīng)很難再迫使虛擬機(jī)產(chǎn)生方法區(qū)的溢出異常了。不過,HotSpot還是提供了一些參數(shù)作為元空間的防御措施,主要包括:

  • -XX:MaxMetaspaceSize:設(shè)置元空間最大值,默認(rèn)是-1,即不限制,或者說只受限于本地內(nèi)存大小。
  • -XX:MetaspaceSize:指定元空間的初始空間大小,以字節(jié)為單位,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載,同時(shí)收集器會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過-XX:MaxMetaspaceSize(如果設(shè)置了的話)的情況下,適當(dāng)提高該值。
  • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空間剩余容量的百分比,可減少因?yàn)樵臻g不足導(dǎo)致的垃圾收集的頻率。類似的還有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空間剩余容量的百分比。

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

????????直接內(nèi)存(Direct Memory)的容量大小可通過參數(shù)-XX:MaxDirectMemorySize來指定,如果不去指定,則默認(rèn)與Java堆最大值(由-Xmx指定)一致,下列代碼清單越過了DirectByteBuffer類直接通過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配(Unsafe類的getUnsafe()方法指定只有引導(dǎo)類加載器才會(huì)返回實(shí)例,體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類庫里面的類才能使用Unsafe的功能,在JDK 10時(shí)才將Unsafe的部分功能通過VarHandle開放給外部使用),因?yàn)殡m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常,但它拋出異常時(shí)并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過計(jì)算得知內(nèi)存無法分配就會(huì)在代碼里手動(dòng)拋出溢出異常,真正申請(qǐng)分配內(nèi)存的方法是Unsafe::allocateMemory()。

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 */
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ùn)行結(jié)果:

Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

????????由直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見有什么明顯的異常情況,如果讀者發(fā)現(xiàn)內(nèi)存溢出之后產(chǎn)生的Dump文件很小,而程序中又直接或間接使用了DirectMemory(典型的間接使用就是NIO),那就可以考慮重點(diǎn)檢查一下直接內(nèi)存方面的原因了。

文章總結(jié):

????????到此為止,我們明白了虛擬機(jī)里面的內(nèi)存是如何劃分的,哪部分區(qū)域、什么樣的代碼和操作可能導(dǎo)致內(nèi)存溢出異常。雖然Java有垃圾收集機(jī)制,但內(nèi)存溢出異常離我們并不遙遠(yuǎn),本章只是講解了各個(gè)區(qū)域出現(xiàn)內(nèi)存溢出異常的原因,下篇文章將詳細(xì)講解Java垃圾收集機(jī)制為了避免出現(xiàn)內(nèi)存溢出異常都做了哪些努力。文章來源地址http://www.zghlxwxcb.cn/news/detail-596294.html

到了這里,關(guān)于深入理解Java虛擬機(jī)(二)Java內(nèi)存區(qū)域與內(nèi)存溢出異常的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 深入理解Java虛擬機(jī)jvm-對(duì)象的內(nèi)存布局

    深入理解Java虛擬機(jī)jvm-對(duì)象的內(nèi)存布局

    在HotSpot虛擬機(jī)里,對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可以劃分為三個(gè)部分:對(duì)象頭(Header)、實(shí)例 數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。 HotSpot虛擬機(jī)對(duì)象的對(duì)象頭部分包括兩類信息。第一類是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈 希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、

    2024年02月09日
    瀏覽(17)
  • Step2:Java內(nèi)存區(qū)域與內(nèi)存溢出異常

    Step2:Java內(nèi)存區(qū)域與內(nèi)存溢出異常

    對(duì)于Java程序員來說,再虛擬機(jī)自動(dòng)內(nèi)存管理機(jī)制的幫助下,不再需要為每一個(gè)new操作去寫配對(duì)的delete/free代碼,不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題,看起來由虛擬機(jī)管理內(nèi)存一切都很美好。不過正是因?yàn)镴ava程序員把控制內(nèi)存的權(quán)利交給來Java虛擬機(jī),一旦出現(xiàn)內(nèi)存泄露方

    2024年02月07日
    瀏覽(16)
  • JVM哪些區(qū)域會(huì)出現(xiàn)內(nèi)存溢出

    JVM(Java Virtual Machine)是指Java虛擬機(jī),是一種可以在不同平臺(tái)上運(yùn)行Java字節(jié)碼的虛擬計(jì)算機(jī)。JVM是Java語言的核心,通過將Java代碼編譯成字節(jié)碼并在JVM上運(yùn)行,實(shí)現(xiàn)了跨平臺(tái)的特性。 1.方法區(qū)(Metaspace) 方法區(qū)用于存儲(chǔ)類的信息、靜態(tài)變量和常量等數(shù)據(jù)。在JDK8及以上版本中

    2024年02月08日
    瀏覽(27)
  • java面經(jīng)03-虛擬機(jī)篇-jvm內(nèi)存結(jié)構(gòu)&垃圾回收、內(nèi)存溢出&類加載、引用&悲觀鎖&HashTable、引用&finalize

    java面經(jīng)03-虛擬機(jī)篇-jvm內(nèi)存結(jié)構(gòu)&垃圾回收、內(nèi)存溢出&類加載、引用&悲觀鎖&HashTable、引用&finalize

    要求 掌握 JVM 內(nèi)存結(jié)構(gòu)劃分 尤其要知道方法區(qū)、永久代、元空間的關(guān)系 結(jié)合一段 java 代碼的執(zhí)行理解內(nèi)存劃分 執(zhí)行 javac 命令編譯源代碼為字節(jié)碼 執(zhí)行 java 命令 創(chuàng)建 JVM,調(diào)用類加載子系統(tǒng)加載 class,將類的信息存入 方法區(qū) 創(chuàng)建 main 線程,使用的內(nèi)存區(qū)域是 JVM 虛擬機(jī)棧 ,

    2024年02月09日
    瀏覽(22)
  • 深入理解JVM虛擬機(jī)第十五篇:虛擬機(jī)棧常見異常以及如何設(shè)置虛擬機(jī)棧的大小

    深入理解JVM虛擬機(jī)第十五篇:虛擬機(jī)棧常見異常以及如何設(shè)置虛擬機(jī)棧的大小

    ???? 學(xué)習(xí)交流群: ??1:這是 孫哥suns 給大家的福利! ??2:我們免費(fèi)分享Netty、Dubbo、k8s、Mybatis、Spring...應(yīng)用和源碼級(jí)別的視頻資料 ????3:QQ群: 583783824 ? ???? ?工作微信: BigTreeJava 拉你進(jìn)微信群,免費(fèi)領(lǐng)??! ????4:本文章內(nèi)容出自上述:Spring應(yīng)用課程!????

    2024年02月06日
    瀏覽(27)
  • 【JVM】Java堆 :深入理解內(nèi)存中的對(duì)象世界

    【JVM】Java堆 :深入理解內(nèi)存中的對(duì)象世界

    人不走空 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ? ? 目錄 ? ????????個(gè)人主頁:人不走空?????? ??系列專欄:算法專題 ?詩詞歌賦:斯是陋室,惟吾德馨 ?編輯 什么是Java堆? 作用和特點(diǎn) 1. 存儲(chǔ)對(duì)象實(shí)例 2. 垃圾收集 3. 對(duì)象

    2024年01月19日
    瀏覽(30)
  • [AIGC] 利用 chatgpt 深入理解 Java 虛擬機(jī)(JVM)

    [AIGC] 利用 chatgpt 深入理解 Java 虛擬機(jī)(JVM)

    Java 虛擬機(jī)(JVM)是 Java 編程語言的核心運(yùn)行環(huán)境,它負(fù)責(zé)解釋和執(zhí)行 Java 字節(jié)碼。它是 Java 程序能夠跨平臺(tái)運(yùn)行的關(guān)鍵,因?yàn)椴煌牟僮飨到y(tǒng)和硬件平臺(tái)都有自己的指令集和體系結(jié)構(gòu),而 JVM 則提供了一個(gè)統(tǒng)一的運(yùn)行環(huán)境,使得 Java 程序可以在不同的平臺(tái)上無需修改就能運(yùn)行

    2024年02月22日
    瀏覽(22)
  • 深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐

    Java虛擬機(jī) Java虛擬機(jī)(Java Virtual Machine,JVM)是Java語言的核心,是執(zhí)行Java二進(jìn)制代碼的虛擬計(jì)算機(jī)。 JVM本身是一個(gè)進(jìn)程,負(fù)責(zé)解析Java程序并將其轉(zhuǎn)換為特定平臺(tái)可以執(zhí)行的指令集。 通過JVM,Java程序可以實(shí)現(xiàn)“一次編寫,到處運(yùn)行”的特性,使Java具有很強(qiáng)的平臺(tái)無關(guān)特性。

    2024年02月07日
    瀏覽(30)
  • “深入理解JVM:Java虛擬機(jī)的工作原理揭秘“

    標(biāo)題:深入理解JVM:Java虛擬機(jī)的工作原理揭秘 摘要:本文將深入解析Java虛擬機(jī)(JVM)的工作原理,包括JVM的組成部分、類加載過程、運(yùn)行時(shí)數(shù)據(jù)區(qū)域、垃圾回收機(jī)制等。通過詳細(xì)的代碼示例,幫助讀者更好地理解JVM的內(nèi)部機(jī)制。 正文: 一、JVM的組成部分 Java虛擬機(jī)是Java語

    2024年02月13日
    瀏覽(24)
  • “深入理解Java虛擬機(jī)(JVM):背后的工作原理解析“

    標(biāo)題:深入理解Java虛擬機(jī)(JVM):背后的工作原理解析 摘要:本文將深入探討Java虛擬機(jī)(JVM)的工作原理,包括內(nèi)存管理、垃圾回收、即時(shí)編譯器等關(guān)鍵概念,以及如何優(yōu)化代碼以提高性能。通過示例代碼和詳細(xì)解釋,讀者將對(duì)JVM的底層原理有更深入的理解。 正文: 一、

    2024年02月12日
    瀏覽(27)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包