JVM运行时结构、Java内存管理、JVM实例、HotSpot VM对象的创建、内存布局和访问定位
1.JVM運行時結(jié)構(gòu)
Java 運行時數(shù)據(jù)區(qū)域有程序計數(shù)器、Java虛擬機(jī)棧、本地方法棧、Java堆和方法區(qū)。其中前三個線程私有,隨線程生而生,線程滅而滅;后面兩個是線程間共享。
1.1 程序計數(shù)器
program counter register
較小
可看做是當(dāng)前程序所執(zhí)行的字節(jié)碼的行號指示器。
在虛擬機(jī)的概念模型中,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器完成。
JVM的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)。(在任何一個確定的時刻,一個處理器(多核處理器時是指一個內(nèi)核)都只會執(zhí)行一條線程中的指令)。為了讓線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器。各條線程之間的計數(shù)器互不影響,獨立存儲。
這類內(nèi)存是“線程私有”內(nèi)存。
- 若一個線程執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;
- 若執(zhí)行的是Native方法,這個計數(shù)器值則為空(Null)
此內(nèi)存區(qū)域是唯一一個在JVM規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
1.2 JVM棧
線程私有
生命周期與線程相同。
描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時會創(chuàng)建一個棧幀(Stack Frame),用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口信息。每一個從方法調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)中入棧到出棧的過程。
局部變量表:存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean/byte/char/short/int/float/long/double,其中l(wèi)ong和double占用兩個局部變量空間(slot),其余的占用一個)、對象引用(reference類型,它不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉种频囊弥羔?#xff0c;也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。局部變量表需要的內(nèi)存空間在編譯期間內(nèi)完成分配。當(dāng)進(jìn)入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
JVM規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:
- 如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;
- 如果虛擬機(jī)可以動態(tài)擴(kuò)展(當(dāng)前大部分的JVM都可動態(tài)擴(kuò)展,只不過JVM規(guī)范中允許固定長度的虛擬機(jī)棧),如果擴(kuò)展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。
1.3 本地方法棧
Native Method Stack
與虛擬機(jī)棧發(fā)揮的作用是相似的。
虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(字節(jié)碼)服務(wù)。
本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)。
本地方法棧也會拋出StackOverFlow和OutOfMemoryError異常。
1.4 Java堆
Java Heap
對大多數(shù)應(yīng)用來說,Java堆是JVM所管理的內(nèi)存中最大的一塊。它是被線程所共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建。
唯一目的:存放Java對象實例。JVM規(guī)范中描述到:所有(現(xiàn)在沒那么絕對了)的對象實例以及數(shù)組都要在堆上分配。
Java堆是垃圾收集器管理的主要區(qū)域,很多時候也叫做“GC堆”。
- 從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集,所以Java堆還可以細(xì)分為:新生代和老年代;
- 再細(xì)致一點新生代可以分為Eden空間、From Survivor空間、To Survivor空間等。
- 不過無論如何劃分,都與存放的內(nèi)容無關(guān),無論哪個區(qū)域,存放的都仍然是對象實例。
- 進(jìn)一步劃分的目的是為了更好地回收和分配內(nèi)存。
線程共享的Java堆可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。進(jìn)一步劃分的目的只是為了更好地回收內(nèi)存。
Java堆可以處在物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。既可實現(xiàn)成固定大小的,也可是可擴(kuò)展的。
如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴(kuò)展時,將會拋出OutOfMemoryError異常
1.5 方法區(qū)
Method Area,別名:Non-Heap(非堆)
是各個線程共享的內(nèi)存區(qū)域。
用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
JVM規(guī)范對方法區(qū)的限制非常寬松:不需要連續(xù)的內(nèi)存和可以選擇固定大小和可擴(kuò)展、還可以選擇不實現(xiàn)垃圾收集。
這區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載。
當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,會拋出OutOfMemoryError異常。
1.6 運行時常量池
Runtime Constant Pool,是方法區(qū)的一部分。
class文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池(Constant Pool Table),用于存放編譯器生成的各種字面量和符號引用。這部分內(nèi)容在類加載后進(jìn)入方法區(qū)的運行時常量池中存放。
JVM規(guī)范沒有對運行時常量池做任何細(xì)節(jié)要求。
運行時常量池相對于class文件常量池的另外一個重要特征是具備動態(tài)性。
Java語言并不要求常量一定只有編譯器才能生成,也就是并非預(yù)置入class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中。——這種特性被開發(fā)人員利用最多的就是String類的intern()方法。
當(dāng)常量池?zé)o法再申請到內(nèi)存時就會拋出OutOfMemoryError異常。
1.7 直接內(nèi)存
Direct Memory
并不是虛擬機(jī)運行時數(shù)據(jù)區(qū)的一部分,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域。但它被頻繁地使用,也可能導(dǎo)致OutOfMemoryError。
JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。
本機(jī)直接內(nèi)存的分配不會受到Java堆大小的限制,但還是會受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制。
1.8總結(jié)
| ? | 共享/私有 | 出現(xiàn)的異常 |
| 程序計數(shù)器 | 私 | 沒有規(guī)定任何OutOfMemory情況 |
| Java虛擬機(jī)棧 | 私 | OutOfMemoryError/StackOverflowError |
| 本地方法棧 | 私 | OutOfMemoryError/StackOverflowError |
| Java堆 | 共 | OutOfMemoryError |
| 方法區(qū) | 共 | OutOfMemoryError |
| 運行時常量池 | ? | OutOfMemoryError |
| 直接內(nèi)存 | ? | OutOfMemoryError |
1.9堆和棧的區(qū)別
| ? | 堆 | 棧 |
| 存放 | 所有的對象和數(shù)組實例 | 基本數(shù)據(jù)類型和引用變量,為執(zhí)行Java方法服務(wù) |
| 共享/私有 | 線程共享的 | 線程私有的,描述的是方法執(zhí)行的內(nèi)存模型 |
| 功能 | 主要用來存放對象的 | 主要是用來執(zhí)行程序的 |
| ? | 存取速度的緩慢。 可以在運行時動態(tài)地分配內(nèi)存, 生存期不需要提前告訴編譯器, | 存取速度更快 ;大小和生存期必須是確定的,因此缺乏一定的靈活性 |
1.10 StackOverflowStack和OutOfMemory(OOM)怎樣發(fā)生?
1.10.1OutOfMemory
1、java.lang.OutOfMemory:Java heap space
首先查看是否是堆溢出或內(nèi)存泄露。
要解決堆內(nèi)存異常的情況一般的手段是通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉(zhuǎn)儲快照進(jìn)行分析,重點是確認(rèn)內(nèi)存中的對象是否是有必要的,也就是要先分清楚是內(nèi)存泄露還是內(nèi)存溢出。
- ?如果是內(nèi)存泄露,可進(jìn)一步通過工具查看泄露對象到GC Roots的引用鏈。于是就能找到泄露的對象是怎么與GC Roots相連接導(dǎo)致進(jìn)行垃圾回收時沒能回收泄露對象所占的內(nèi)存,有了泄露對象的信息和GC Roots引用鏈的信息,就可以準(zhǔn)確地定位出泄露代碼的位置。
- 如果沒有發(fā)出內(nèi)存泄露,也是說,內(nèi)存中的對象確實還活著,那就應(yīng)該去檢查虛擬機(jī)的堆參數(shù)(-Xmx與-Xms),與物理機(jī)器對比看是否還可以進(jìn)一步擴(kuò)大,從代碼上檢查是否存在某些對象聲明周期過長、持有狀態(tài)時間過長等,嘗試減少程序運行內(nèi)存消耗。
2、java.lang.OutOfMemory:PermGen space
說明是運行時常量池出現(xiàn)問題。需要擴(kuò)大方法區(qū)來保證動態(tài)生成的class可以加載入內(nèi)存。
3、java.lang.OutOfMemory
直接內(nèi)存(直接內(nèi)存并不是虛擬機(jī)運行數(shù)據(jù)區(qū)的一部分)可以通過-XX:MaxDirectMemorySize指定 ,如果不指定,則默認(rèn)與Java堆最大值(-Xmx)一樣。
雖然使用DirectByteBuffer分配內(nèi)存也會拋出內(nèi)存溢出異常,但是它拋出異常時并沒有真正向操作系統(tǒng)申請分配內(nèi)存,而是通過計算得知內(nèi)存無法分配,于是手動拋出異常,真正申請分配內(nèi)存的方法是unsafe.allocateMemory().
由直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個明顯的特征是Heap Dump文件中不會看見明顯的異常,如果發(fā)現(xiàn)OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是出現(xiàn)了方法區(qū)溢出。
1.10.2 StackOverflowStack
拋出StackOverflowStack異常是線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常。
產(chǎn)生這種內(nèi)存溢出與棧空間是否足夠大并沒有任何關(guān)系,反而給每個線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。
出現(xiàn)StackOverflowError異常有時有錯誤堆棧可以閱讀,相對比較容易找到問題的所在。而且,如果使用虛擬機(jī)默認(rèn)參數(shù),在大多數(shù)情況下達(dá)到1000~2000是完全沒有問題的,對于正常的方法調(diào)用(包括遞歸),這個深度是完全沒有問題的。但是如果建立過多線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)或者更換64位虛擬機(jī)的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。
2.Java自動內(nèi)存管理
Java 的自動內(nèi)存管理就是給對象的分配內(nèi)存和回收分配給對象的內(nèi)存。
2.1對象的內(nèi)存分配
大方向,在堆上分配,對象主要分配在新生代的Eden區(qū)上,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配。少數(shù)情況下也可能會直接分配在老年代中,分配的規(guī)則不是百分百固定的,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集器組合,還有虛擬機(jī)與內(nèi)存相關(guān)的參數(shù)的設(shè)置。
2.1.1對象優(yōu)先在Eden區(qū)分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配。
當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時,虛擬機(jī)將發(fā)起一次Minor GC。
收集日志參數(shù):-XX:+PrintGCDetails,告訴虛擬機(jī)在發(fā)生垃圾收集行為時打印內(nèi)存回收日志,并且在進(jìn)程退出的時候輸出當(dāng)前的內(nèi)存各個區(qū)域分配情況。
參數(shù):-XX:SurvivorRatio=8,決定了新生代中Eden區(qū)域一個Survivor區(qū)的空間比例是8:1。
2.1.2大對象直接進(jìn)入老年代
大對象是指,需要大量連續(xù)內(nèi)存空間的Java對象。典型的就是,很長的字符串以及數(shù)組。
經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時,就提前出發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”它們。
參數(shù):-XX:PretenureSizeThreshold,令大于這個設(shè)置值的對象直接在老年代分配。
- 這個參數(shù)只對Serial和ParNew兩款收集器有效。
目的是避免在Eden區(qū)以及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代采用復(fù)制算法收集內(nèi)存)。
2.1.3 長期存活的對象將進(jìn)入老年代
內(nèi)存回收必須識別哪些對象應(yīng)放在新生代,哪些對象應(yīng)放在老年代中。
所以,虛擬機(jī)給每個對象定義了一個對象年齡(Age)計數(shù)器。
如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能夠被Survivor容納的話,將被移動到Survivor空間中,并且設(shè)置對象年齡為1.
對象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就會進(jìn)入老年代中。
對象進(jìn)入老年代的閾值,可通過參數(shù)-XX:MaxTenuringThreshold設(shè)置。
2.1.4動態(tài)判斷對象年齡
虛擬機(jī)并不是永遠(yuǎn)要求對象的年齡必須達(dá)到了MaxTenuringThreshold才能進(jìn)入老年代,如果在Survivor空間中相同年齡所有對象大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可直接進(jìn)入老年代。無需等到年齡閾值。
2.1.5空間擔(dān)保
在發(fā)生Minor GC之前,虛擬機(jī)會先檢查老年代最大的可用連續(xù)空間是否大于新生代所有對象的總空間。
- 如果這個條件成立,那么Minor GC可以確保是安全的。
- 如果不成立,虛擬機(jī)會查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。
- 若允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小。
- 若大于,將嘗試著進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險的。
- 若小于,或HandlePromotionFailure不允許冒險,那這時要改為進(jìn)行一次Full GC。
- 但大部分的HandlePromotionFailure開關(guān)打開,避免Full GC過于頻繁。
- 若允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小。
3.JVM實例
每當(dāng)一個Java程序運行時,都會有一個對應(yīng)的JVM實例,只有當(dāng)程序運行結(jié)束后,這個JVM才會退出。
JVM實例通過調(diào)用類的main()方法來啟動一個Java程序,而這個main()方法必須是公有的、靜態(tài)的且返回值為void的方法,該方法接受一個字符串?dāng)?shù)組的參數(shù),只有同時滿足這些條件才可以作為程序的入口方法。
4.HotSpot虛擬機(jī)中對象的創(chuàng)建
4.1語言層面上
創(chuàng)建對象(克隆、反序列化)通常僅僅是一個new關(guān)鍵字
4.2虛擬機(jī)中
當(dāng)虛擬機(jī)遇到一條new指令時,
4.3.1類加載檢查
首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
4.3.2為新生對象分配內(nèi)存
對象所需的內(nèi)存大小在類加載完成后便可完全確定。
分配方式:
指針碰撞(Bump the Pointer):假設(shè)Java堆是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的放在另一邊,中間放著一個指針作為分界點的指示器,那分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離。
空閑列表(Free List):若Java堆中的內(nèi)存不是規(guī)整的,那虛擬機(jī)就需維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,再分配的時候找到一塊足夠大的空間劃分給對象實例,并更新表上的記錄。
選擇哪種分配方式由Java堆是否規(guī)整決定。
Java堆是否規(guī)整由所采用的垃圾收集器是否帶有壓縮整理的功能決定。
- 在使用Serial、ParNew等帶compact過程的收集器時,系統(tǒng)采用的分配算法是指針碰撞;
- 在使用CMS這種基于Mark-Sweep算法的收集器時,通常采用空閑列表。
對象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,移動指針在并發(fā)情況下也不是線程安全的。
解決辦法:
- 對分配內(nèi)存空間的動作進(jìn)行同步處理——實際上虛擬機(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è)定。
4.3.3初始化內(nèi)存空間
內(nèi)存分配完之后,虛擬機(jī)需要將分配到內(nèi)存的空間都初始化為零值(不包括對象頭)。
如果使用TLAB這一步也可以在TLAB分配時進(jìn)行。
這一操作保證了對象的實例字段在Java代碼中可以不賦初值就直接使用。程序能訪問到這些字段的數(shù)據(jù)對應(yīng)的零值。
4.3.1虛擬機(jī)對對象進(jìn)行必要的設(shè)置。
這些信息存放在對象頭(Object Head)中。
以上完成之后,從虛擬機(jī)的角度看,一個新的對象已經(jīng)生成了。
4.4 Java程序看
上述完成后,對象的創(chuàng)建才剛剛開始,——<init>方法還沒有執(zhí)行,所有字段都還為零。
所以,一般來說(由字節(jié)碼是否跟隨invokespecial指令所決定),執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進(jìn)行初始化,這樣一個真正可用的對象才算安全產(chǎn)生。
5.HotSpot虛擬機(jī)中對象的內(nèi)存布局
對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance data)、對齊填充(Padding)。
5.1對象頭
包括兩部分信息:
5.1.1用于存儲對象自身的運行時數(shù)據(jù)(Mark Word)
- 哈希碼
- GC分代年齡
- 鎖狀態(tài)標(biāo)志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
對象頭信息時與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本。Mark Word會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,它非固定。
| 存儲內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
| 對象哈希碼、對象分代年齡 | 01 | 未鎖定 |
| 指向鎖記錄的指針 | 00 | 輕量級鎖定 |
| 指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
| 空,不需要記錄信息 | 11 | GC標(biāo)記 |
| 偏向線程ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
5.1.2類型指針
即對象指向它的類元數(shù)據(jù)的指針。
虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例。
并不是所有的虛擬機(jī)實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,即查找對象的元數(shù)據(jù)信息并不一定要通過對象本身。
若對象頭是一個Java數(shù)組,那么對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)。(因為虛擬機(jī)可以通過普通Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是從數(shù)組的元數(shù)據(jù)中卻無法確定數(shù)組大小)。
5.2實例數(shù)據(jù)
對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內(nèi)容。
這部分的存儲順序收到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。
HotSpot虛擬機(jī)默認(rèn)的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(ordinary object pointers)。
- 從分配策略來看,相同長度的字段總是被分配到一起。
- 在滿足這個前提下,在父類中定義的變量會出現(xiàn)在子類之前。
- 若CompactFields參數(shù)值為true(默認(rèn)),那么子類中較窄的變量也可能會插入到父類變量的空隙之中。
5.3對齊填充(非必需)
僅僅是占位符的作用。
由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍。
而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或2倍——32或64位,由虛擬機(jī)是32還是64確定),因此當(dāng)獨享實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補(bǔ)全。
6.對象的訪問定位
Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。
reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個指向?qū)ο蟮囊?#xff0c;并沒有定義這個引用應(yīng)該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問也是取決于VM實現(xiàn)而定的。
目前主流的訪問方式有:使用句柄和直接指針。
6.1使用句柄
Java堆中會劃分出一塊內(nèi)存來作為句柄池。
reference中存放的就是對象的句柄地址。而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
6.2直接指針
Java堆對象的布局必須考慮如何防止訪問類型數(shù)據(jù)的相關(guān)信息,而reference中存儲的直接就是對象地址。
6.3對比
| 句柄 | 直接指針 |
| reference中存的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針 | 速度更快,節(jié)省了一次指針定位的開銷 |
| (垃圾收集時移動對象的行為很普遍) | (對象訪問很頻繁,積少成多) |
?
總結(jié)
以上是生活随笔為你收集整理的JVM运行时结构、Java内存管理、JVM实例、HotSpot VM对象的创建、内存布局和访问定位的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 集合-2(Set(HashSet、Tre
- 下一篇: Java内存泄露和内存溢出、JVM命令行