《深入理解Java虛擬機》是我個人讀過的第一本關于JVM方面的書籍。十分有幸能夠讀到這本書,在此對作者表示深刻的敬意
不知道有沒有人和我一樣有類似的情況,就是一本書讀完,經過一段時間之后,林林總總最后留在腦子里的并不多,很多東西又還給了作者。有人可能會說,之所以會遺忘,是因為你根本沒有理解。我并不否認這點,說實話,很多書讀過一遍之后都會存在沒有讀懂的部分。但是隨著我們閱歷的豐富以及期間不斷的學習,過去不懂的部分終究會慢慢讀懂。鑒于這種情況,我覺得讀書筆記還是很有必要:讀書期間總結歸納,日后溫故知新
筆記僅僅是我個人對此書的一份總結,提綱挈領。如需了解細節,請完整閱讀原著,這本書我個人也強烈推薦。下面,開始《深入理解Java虛擬機》讀書筆記的第一篇--Java內存區域
運行時數據區域
JVM在運行Java程序時將內存區域劃分為不同的部分,這些區域有各自的用途以及各自的內存管理策略
在Java虛擬機的概念模型中,這些區域大致可以分為:程序計數器(Program Counter Register)、虛擬機棧(VM Stack)、本地方法棧(Native Method Stack)、堆(Heap)、方法區(Method Area)
其中前三者是線程隔離的內存區域,后兩者是線程共享的內存區域
1.程序計數器
程序計數器可以簡單理解為線程執行字節碼的行號指示器。由于在線程切換后需要恢復到程序的執行位置,因此每個線程都有各自的程序計數器
2.虛擬機棧
虛擬機棧描述的是Java方法執行的內存模型,方法在執行的時候會創建棧幀。棧幀用于存儲方法執行時所需的局部變量表、操作數棧、動態鏈接、方法出口等信息。方法的執行伴隨著棧幀在虛擬機棧中的入棧及出棧
局部變量表存儲了基本數據類型、對象引用、返回地址
Java虛擬機規范對此區域規定了兩種異常情況:當線程請求的棧深度大于虛擬機允許的最大深度將觸發StackOverflowError;如果虛擬機棧在動態擴展時無法申請到足夠的內存將觸發OutOfMemoryError
3.本地方法棧
本地方法棧與虛擬機棧作用類似,區別在于虛擬機棧用于Java方法執行,而本地方法棧用于本地方法執行
有些虛擬機甚至不區分本地方法棧和虛擬機棧,而是將它們合二為一
同樣本地方法棧也會觸發StackOverflowError和OutOfMemoryError
4.堆
堆用于存放對象實例,可以細分為新生代和老年代,進一步可以細分為Eden空間、From Survivor空間、To Survivor空間等。進一步細分區域的目的是根據對象的特征更好的分配以及回收內存空間
堆在物理上可以是不連續的空間,只要在邏輯上是連續的即可。如果當前堆內存已經用完并且無法動態擴展的時候會觸發OutOfMemoryError
5.方法區
方法區主要用于存放已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
運行時常量池是方法區的一部分,主要用于存放編譯期生成的字面量和符號引用,另外在運行時也可以將新的常量加入池中
這個區域的內存回收通常收益較低,主要是針對常量池的回收以及對類型的卸載(對類型卸載的要求十分嚴苛)
同樣,當方法區無法滿足內存分配需求時,將會觸發OutOfMemoryError
6.直接內存
直接內存并非Java虛擬機規范中定義的內存區域,但是在大量使用NIO的程序中有可能觸發OutOfMemoryError異常
NIO基于Channel和Buffer,可以直接使用本地方法分配堆外內存,然后通過存儲在堆中的DirectByteBuffer對象作為這塊堆外內存的引用進行操作。雖然堆外內存不會受到Java堆大小的限制,但是仍然會受制于物理內存以及操作系統的限制。當各內存區域總和大于物理內存或者達到操作系統限制,從而導致動態擴展時觸發OutOfMemoryError
對象探秘
不同的虛擬機,對于對象的創建、布局和訪問會有所差異,下面僅以HotSpot的堆內存為例進行介紹
1.對象的創建
#類檢查及類加載
虛擬機在遇到new指令的時候,首先會去常量池檢查類的符號引用,如果發現沒有對此類進行加載、解析、初始化,那么首先會對該類進行加載
#內存分配
之后虛擬機會為該對象分配內存。由于對象所需內存在類加載后就可以確定,因此分配內存實際上就是從未使用的堆內存中劃分出一塊確定大小的存儲空間。此過程有兩種方式:
1)對于規整的堆內存,直接將指針向空閑一側移動所需的大小,這種方式叫做“指針碰撞”
2)對于不規整對堆內存,虛擬機會維護一個空閑內存列表,當需要分配內存時,劃分出一塊足夠的空間并且更新空閑列表,這種方式叫做“空閑列表”
至于堆內存是否規整連續,取決于具體的垃圾收集器(主要取決于是否帶有compact功能)
由于對象的創建是一個十分頻繁的過程,在并發情況下會有并發安全的問題。解決的方式有兩種:
1)對內存分配進行同步處理,實際上虛擬機采用CAS的方式保證原子性
2)另一種方式是把內存分配的動作按照線程進行隔離,即每個線程會預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。只有在TLAB用完需要新分配的時候在采取同步處理
#初始化零值
內存分配完畢后,虛擬機會對該內存區域初始化零值,如果是TLAB方式,這一步可以提前到TLAB階段。這一步保證了對象實例屬性的初始化。這也就是為什么對象的實例屬性可以不賦初值就能夠直接訪問
#對象頭設置
接下來,虛擬機需要將對象進行一些設置,將類的元數據、哈希碼、GC分代信息等設置到對象頭中
#對象初始化
執行完上述步驟后,對象的實例屬性還都是零值,下面會執行<init>方法,按照程序的意圖對對象進行初始化(我的理解是執行構造方法)。之后在將對象的引用入棧。至此,對象的創建過程結束
2.對象的內存布局
在HotSpot虛擬機當中,對象的內存布局分為三個區域:對象頭、實例數據、對齊填充
#對象頭
對象頭主要存儲兩部分信息,一部分是對象的運行時數據(如哈希碼、GC分代信息、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等),另一部分是類型指針(即指向類元數據的指針,代表該對象是哪個類的實例。當然類的元數據也并不一定要存儲在對象頭,這個后續再討論)
#實例數據
實例數據部分用于存儲對象實例的數據,即實例屬性的內容,父類的屬性也會記錄下來。存儲順序會受到虛擬機的分配策略以及字段的定義順序的影響。默認的分配策略大致有幾點規則:
1)會將相同寬度的屬性分配在一起
2)滿足1的前提下,父類中的屬性出現在子類前
3)CompactFields參數為true的時候,會將子類中寬度較窄的屬性插入到父類屬性的空隙之中
#對齊填充
由于HotSpot虛擬機要求對象的其實地址必須是8字節的整數倍,因此對于對象大小不是8字節整數倍的對象,會被對齊填充
3.對象的訪問定位
對象的訪問解決的是如何通過棧中對象的引用訪問到堆中實際對象的問題,目前主流的方式有兩種:
1)句柄訪問,棧中對象的引用存儲的是對象的句柄地址(句柄在句柄池中維護),句柄存儲了堆中對象實例的地址以及方法區中對象的類型數據的地址。
這種方式的優點是引用中存儲的是穩定的句柄,當對象地址變化的時候(GC過程中可能會移動對象實例),只需要更新句柄,不需要更新引用;缺點也顯而易見,訪問對象時多一次指針操作
2)直接指針訪問,棧中對象的引用存儲的就是對象實例的地址,對象實例(對象頭)中又存儲了方法區中對象的類型數據的地址
這種方式的優點就是訪問迅速(比前者少一次指針操作),在HotSpot虛擬機中采用此種方式
思維導圖:
筆記1結束