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

1 程序計數(shù)器
定義
- 是一塊較小的內(nèi)存空間,它可以看作是
當(dāng)前線程所執(zhí)行的字節(jié)碼行號指示器。
為什么是線程隔離的數(shù)據(jù)區(qū)?
- 由于JVM的多線程是通過線程輪流i切換、分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器都只會執(zhí)行一條程序中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器。
- 如果程序正在執(zhí)行Java方法,計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令地址;如果正在執(zhí)行的是Native方法,計數(shù)器值為空(undefined)
2 Java虛擬機棧(VM Stack)
定義
- -描述的是Java方法執(zhí)行的
線程內(nèi)存模型:每個方法被執(zhí)行的時候,JVM都會同步創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法出口等信息。
為什么是線程隔離的數(shù)據(jù)區(qū)?
- 因為Java虛擬機棧描述的是Java方法執(zhí)行的線程內(nèi)存模型,每個線程執(zhí)行的時間和順序不一定相同,所以棧幀也一定相同。
異常
- 如果線程請求的棧深度大于虛擬機所允許的深度,拋出
StackOverflowError。 - 如果Java虛擬機棧容量可以動態(tài)擴展,當(dāng)棧擴展時無法申請到足夠的內(nèi)存拋
OutOfMemoryError。
3 本地方法棧(Native Method Stack)
定義
- 和Java虛擬機棧相似,描述的是
本地方法執(zhí)行的線程內(nèi)存模型。
虛擬機異同
- 虛擬機可以根據(jù)需要自由實現(xiàn)本地方法棧
- HotSpot虛擬機直接把虛擬機棧和本地方法棧合二為一
4 Java堆
定義
- 所有的對象實例以及數(shù)組都應(yīng)當(dāng)在堆上分配。
為什么是線程共享的?
- 因為是內(nèi)存中最大的一塊,并且是垃圾收集器管理的內(nèi)存區(qū)域。
異常
- 如果在Java堆沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,拋出
OutOfMemoryError。
5 方法區(qū)
定義
- 用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。
- 《Java虛擬機規(guī)范》中把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做非堆(Non-Heap)目的是與Java堆區(qū)分開來。
異常
- 如果方法無法滿足新的內(nèi)存分配需求時,拋出
OutOfMemoryError。
6 運行時常量
定義
- 方法區(qū)的一部分,用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。
什么是Class常量池?
- class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
運行期間能將新的常量放入池嗎?
- 并非預(yù)置入Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,例如String類的intern方法。
異常
- 當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出
OutOfMemoryError。
7 直接內(nèi)存
?直接內(nèi)存不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機規(guī)范》中定義的內(nèi)存區(qū)域,但是這部分也被頻繁使用。
定義
- NIO類引入一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在
Java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。
異常 - 各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制,從而導(dǎo)致動態(tài)擴展時出現(xiàn)
OutOfMemoryError。
8 對象
?以虛擬機HotSpot和常用的內(nèi)存區(qū)域Java堆為例,探討Java堆中對象分配、布局和訪問的全過程。
8.1 創(chuàng)建對象
?創(chuàng)建對象分為以下 四步:
①當(dāng)Java虛擬機遇到一條字節(jié)碼new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,則必須執(zhí)行相應(yīng)的類加載過程。
②在類加載檢查通過后,接下來虛擬機將為新生對象分配內(nèi)存。有以下兩種分配方法:
- 指針碰撞:在規(guī)整的內(nèi)存中,以指針作為使用過的內(nèi)存和空閑內(nèi)存的分界點指示器,把指針向空閑方向挪動一段與對象大小相等的距離。
- 空閑列表:虛擬機維護一個列表,記錄可用內(nèi)存,分配時從列表中找到一塊合適的空間劃分給對象實例,并更新列表上的記錄。
對象創(chuàng)建在虛擬機中時非常頻繁的行為,但是在并發(fā)情況下也不是線程安全的。解決這個問題有兩種可選方案:
一、虛擬機采用CAS配上失敗重試的方法保證更新操作的原子性;
二、在每個線程Java堆中預(yù)先分配一小塊內(nèi)存,成為本地線程分配緩沖TLAB,哪個線程需要分配內(nèi)存,就在哪個線程的TLAB上分配,只有TLAB用完需要分配新的TLAB才需要同步。
③內(nèi)存分配完成之后,虛擬機必須將分配到的內(nèi)存空間(不包括對象頭)都初始化為零值,如果使用了TLAB,則可提前至TLAB分配時順便進行。之后對對象進行必要的設(shè)置,這些信息將存放在對象的對象頭之中。
④從虛擬機的視角看,一個新的對象已經(jīng)產(chǎn)生。接著執(zhí)行<init>()方法,按照程序員的意愿對對象進行初始化,一個真正的對象才算被完全構(gòu)造出來。
8.2 對象的內(nèi)存布局
?在HotSpot虛擬機里,對象在堆內(nèi)存中國的存儲布局可以劃分為三個部分:對象頭、實例數(shù)據(jù)和對其填充。
對象頭
?HotSpot虛擬機對象的對象頭部分包括兩類信息。
- 第一類部分用于存儲對象自身運行時數(shù)據(jù)(HashCode、GC分代年齡等等)
- 第二類是指針類型,即對象指向它的類型元數(shù)據(jù)的指針,通過這個指針確認對象是哪個類的實例。
實例數(shù)據(jù)
- 對象真正存儲的有效信息,即我們在程序代碼里定義的各種類型的字段內(nèi)容。
對齊填充
- 占位符,目的是為了保證對象的大小是8字節(jié)的整數(shù)倍。
8.3 對象的訪問定位
?《Java虛擬機規(guī)范》規(guī)定reference只是一個對象的引用,沒有定義引用通過什么方式去定位和訪問堆中對象的位置。主流的訪問方式主要有使用句柄和直接指針兩種:
- Java堆中將可能劃分出一塊內(nèi)存來作為句柄池,reference中存儲句柄地址,句柄包含了對象的實例數(shù)據(jù)與類型數(shù)據(jù)各自的地址信息。
- reference中直接存儲對象的地址。
句柄訪問的優(yōu)缺點
- 優(yōu)點:句柄處于穩(wěn)定位置,內(nèi)存整理時reference不需要被改變(垃圾回收時移動對象是一種常見的現(xiàn)象)。
- 缺點:多了一次定位開銷
直接指針的優(yōu)缺點
-
與句柄相反,內(nèi)存整理時reference要改變,但是訪問對象時少了一次指針定位的開銷。
總結(jié)
- Java內(nèi)存結(jié)構(gòu)可以大致分為由所有線程共享的數(shù)據(jù)區(qū)和線程隔離的數(shù)據(jù)區(qū)。
- 方法區(qū)也屬于邏輯上的堆,在HotSpot中VM stack和Native Method Stack合并在一起,因此可以大致分為堆棧、程序計數(shù)器。
- 對象訪問定位的方法各有優(yōu)勢,不同虛擬機實現(xiàn)不同。
