Cerated by westfallon on 8/19
運行時數(shù)據(jù)區(qū)域
程序技術(shù)器
- 程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器
- 在虛擬機的概念模型里,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成
- 為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響,獨立儲存,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存
- 如果線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址
- 如果正在執(zhí)行的是
Native方法,這個計數(shù)器值則為空(Undefined) - 此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何
OutOfMemoryError情況的區(qū)域
Java虛擬機棧
- Java虛擬機棧(Java Virtual Machines Stacks)也是線程私有的,它的生命周期與線程相同
- Java虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于儲存局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程
- 經(jīng)常有人將Java內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),實際上遠比這復(fù)雜,只不過大多數(shù)程序員最關(guān)注這兩部分,其中的棧就是這里的虛擬機棧,或者說是虛擬機棧中的局部變量表部分
- 局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型、對象引用(
reference類型)和returnAddress類型(指向了一條字節(jié)碼指令的地址) - 局部變量表所需的內(nèi)存空間在編譯器完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小
- Java虛擬機規(guī)范中對這個區(qū)域規(guī)定了兩種異常狀況:
- 如果線程請求棧深度大于虛擬機所允許的深度,將拋出
StackOverflowError異常 - 如果虛擬機可以動態(tài)擴展,如果擴展時無法申請到足夠的內(nèi)存,就會拋出
OutOfMemoryError異常
- 如果線程請求棧深度大于虛擬機所允許的深度,將拋出
本地方法棧
- 本地方法棧(Native Method Stack)與虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法服務(wù),而本地方法棧則為虛擬機使用到的
Native方法服務(wù) - 與虛擬機棧一樣,本地方法棧也會拋出
StackOverflowError和OutOdMemoryError異常
Java堆
- 對大多數(shù)應(yīng)用來說,Java堆(Java Heap)是Java虛擬機所管理的內(nèi)存中最大的一塊
- Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建
- 此內(nèi)存的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存
- Java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為“GC堆”(Garbage Collected Heap)
- Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可
- 如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法擴展時,將會拋出
OutOfMemoryError異常
方法區(qū)
- 方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于儲存已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)
- 當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出
OutOfMemoryError異常
運行時常量池
- 運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分,存放編譯期生成的各種字面量和符號引用
- 運行時常量池相對于
Class文件常量池的另一個重要特征是具備動態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置于Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,這種特性被利用較多的是String類的intern()方法 - 當常量池無法再申請到內(nèi)存時會拋出
OutOfMemoryError異常
直接內(nèi)存
- 直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存被頻繁使用,也有可能導(dǎo)致
OutOfMemoryError異常 - NIO(New Input/Output)類引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以用
Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個儲存在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作,這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)
HotSpot虛擬機
對象的創(chuàng)建
- 首先將去檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程
- 接下來虛擬機將為新生對象分配內(nèi)存。對象所需內(nèi)存的大小在類加載完成后便可確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從堆中劃分出來
- 如果堆是絕對規(guī)整的,虛擬機使用一個指針劃分使用過的和沒使用過的內(nèi)存,這種方式稱為“指針碰撞”(Bump the Pointer)
- 如果堆的內(nèi)存不是規(guī)整的,虛擬機就必須維護一個列表,記錄那些內(nèi)存塊是可用的,分配時從列表中找到一塊足夠大的空間劃分給對象,并更新列表上的記錄,這種方式稱為“空閑列表”(Free List)
- 解決創(chuàng)建對象時并發(fā)情況下線程安全問題:
- 一種是對分配內(nèi)存空間的動作進行同步處理
- 另一種是把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行,即每個內(nèi)存在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內(nèi)存,就在哪個線程的TLAB上分配,只有TLAB用完并且分配新的TLAB時,才需要同步鎖定
- 內(nèi)存分配完成后,虛擬機需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行,這一步保證了對象的實例字段在Java代碼中可以不賦初始值直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值
- 接下來,虛擬機要對對象進行必要的設(shè)置。例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)中
- 以上工作完成后,從虛擬機的角度來看對象已經(jīng)創(chuàng)建完成,姐先來將執(zhí)行
init方法
對象的內(nèi)存布局
- 對象在內(nèi)存中儲存的布局可以分為三塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對象填充(Padding)
- 對象頭包括兩部分信息:
- 第一部分用于儲存對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等
- 另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。另外,如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)
- 實例數(shù)據(jù)部分是對象真正儲存的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。這部分儲存順序會受到虛擬機分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響
- 第三部分對其填充不是必須存在的,也沒有特別的含義,它僅僅起著占位符的作用,因為HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,即對象的大小必須是8字節(jié)的整數(shù)倍
對象的訪問定位
- Java程序需要通過棧上的
reference數(shù)據(jù)來操作堆上的具體對象 - 對象訪問方式是取決于虛擬機實現(xiàn)而定的,目前主流的訪問方式有使用句柄和直接指針兩種
- 如果使用句柄訪問的話,Java堆中將會劃分出一塊內(nèi)存來作為句柄池,
reference中儲存的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息 - 如果使用直接指針訪問,那么Java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而
reference對象中儲存的直接就是對象地址
- 如果使用句柄訪問的話,Java堆中將會劃分出一塊內(nèi)存來作為句柄池,
- 這兩種方式各有優(yōu)劣
- 使用句柄來訪問的最大好處就是
reference中儲存的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不會改變 - 使用直接指針訪問方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時間開銷
- 使用句柄來訪問的最大好處就是