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