Java虛擬機(jī):內(nèi)存區(qū)域

版權(quán)聲明:本文為斑馬君學(xué)習(xí)總結(jié)文章,轉(zhuǎn)載請注明出處!

一、jdk、jre、jvm之間的關(guān)系

從廣義上講,運(yùn)行于java虛擬機(jī)上的語音及其相關(guān)的程序都屬于java技術(shù)體系中的一員。Sun官方所定義的java技術(shù)體系包括以下幾個組成部分:

  • 1 java程序設(shè)計語言
  • 2 各種硬件平臺上的java虛擬機(jī)
  • 3 Class文件格式
  • 4 Java API 類庫
  • 5 第三方Java類庫

    把Java程序設(shè)計語言、java虛擬、javaAPI類庫這三部分統(tǒng)稱為JDK。把JavaAPI類庫中的JavaSE API子集和Java虛擬機(jī)這兩部分統(tǒng)稱為JRE.JRE是java程序運(yùn)行的標(biāo)準(zhǔn)環(huán)境。Jvm: Java Virtual Machine java虛擬機(jī)。三者之間關(guān)系圖。
二、Java虛擬機(jī)內(nèi)存管理

虛擬機(jī)在執(zhí)行java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動而存在,有些區(qū)域則依賴用戶線程的啟動和結(jié)束而建立和銷毀。

2.1.線程共享區(qū):
方法區(qū):存儲運(yùn)行時常量池,已被虛擬機(jī)加載的類信息,常量,靜態(tài)變量,即時編譯器編譯后的代碼等數(shù)據(jù)。
Java堆:存儲對象實(shí)例。
2.2線程獨(dú)占區(qū)
本地方法棧:為jvm所調(diào)用到的Native腳本地方法服務(wù)。
虛擬機(jī)棧:存放方法運(yùn)行時所需的數(shù)據(jù),成為棧幀。
程序計數(shù)器:記錄當(dāng)前線程所執(zhí)行的字節(jié)碼的行號。

三、程序計數(shù)器

程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。程序計數(shù)器處于線程獨(dú)占區(qū)。

如果線程執(zhí)行的是Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。如果正在執(zhí)行的是native方法,這個計數(shù)器的值為undefined。此區(qū)域是唯一一個在java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

四、虛擬機(jī)棧

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

棧幀:每個方法執(zhí)行,都會創(chuàng)建一個棧幀,伴隨著方法從創(chuàng)建到執(zhí)行完成。用于存儲局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口等。

局部變量表:存放編譯期可知的各種基本數(shù)據(jù)類型,引用類型,returnAddress類型。
局部變量表在內(nèi)存空間的編譯期完成分配,當(dāng)進(jìn)入一個方法時,這個方法需要在幀分配多少內(nèi)存是固定的,在方法運(yùn)行期間是不會改變局部變量表的大小。

如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范中也允許固定長度的虛擬機(jī)棧),如果擴(kuò)展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。

五、本地方法棧

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

六、Java堆

存放對象事例。垃圾收集器管理的主要區(qū)域,新生代,老年代。Eden空間。通過-Xmx -Xms參數(shù)來控制堆內(nèi)存的大小。如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時,將會拋出OutOfMemoryError異常。

七、方法區(qū)

存儲虛擬機(jī)加載的類信息,常量,靜態(tài)變量,即時編譯器編譯后的代碼等數(shù)據(jù)。方法區(qū)并不等價于永久代。這區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和堆類型的卸載。

八、運(yùn)行時常量池

方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外。還有一項信息是常量池。用于存放編譯期生成的各種字面量和符號引用,者部分內(nèi)存將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放。比如String str =”abc”。

九、直接內(nèi)存:

并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分。也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。新加入了NIO類,引入了一個基于通道和緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。

對象的創(chuàng)建

對象的創(chuàng)建主要分四部分:1.給對象分配內(nèi)存 2.線程安全性問題 3.初始化對象 4.執(zhí)行構(gòu)造方法

對象分配內(nèi)存:虛擬機(jī)將為新生對象分配內(nèi)存。對象所需內(nèi)存的大小在類加載完成后便可完全確定(如何確定將在2.3.2節(jié)中介紹),為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。
指針碰撞
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
空閑列表記憶內(nèi)存
選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
線程安全問題:對象創(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 Allocation Buffer,TLAB)。哪個線程要分配內(nèi)存,就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定。

初始化對象:內(nèi)存分配完成后,虛擬機(jī)需要將分配到內(nèi)存空間都初始化為零值。

執(zhí)行構(gòu)造方法:虛擬機(jī)要對對象進(jìn)行必要的設(shè)置,例如這個對象是哪個類的實(shí)例,如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。

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

在HotSpot虛擬機(jī)中,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。
1.Header(對象頭)包含兩部分信息,第一部分用于存儲對象自身的運(yùn)行時數(shù)據(jù)(Mark Word) 哈希碼 GC分代年齡 鎖狀態(tài)標(biāo)志 線程持有的鎖,偏向線程ID 偏向時間戳。根據(jù)鎖的狀態(tài)不同,把這個對象頭區(qū)域記錄不同的信息,
2.類型指針:即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實(shí)例。
3.InstanceData:是對象真正存儲的有效數(shù)據(jù)。也是在程序代碼中所定義的各種類型的字段內(nèi)容。這部分的存儲順序會受到虛擬機(jī)分配策略參數(shù)和字段在java源碼中定義順序的影響。
4.Padding:對齊填充并不是必然存在的,它僅僅起著占位符的作用。

根據(jù)鎖的狀態(tài)存儲不同的對象頭信息

對象的訪問和定位

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

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

使用句柄池
2.直接指針:如果使用直接指針訪問,那么java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息。
使用直接對象
使用句柄來訪問的最大好處就是reference中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改。使用直接指針訪問的最大好處就是速度更快,它節(jié)省了一次指針定位的時間開銷。其中類的信息放在方法區(qū),調(diào)用方法要知道類的信息。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容