深入理解虛擬機器之Java記憶體區域

SnailClimb發表於2018-04-27

《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第二版》讀書筆記與常見面試題總結

本節常見面試題:

介紹下Java記憶體區域(執行時資料區)。

物件的訪問定位的兩種方式。

1 概述

對於Java程式設計師來說,在虛擬機器自動記憶體管理機制下,不再需要像C/C++程式開發程式設計師這樣為內一個new 操作去寫對應的delete/free操作,不容易出現記憶體洩漏和記憶體溢位問題。正是因為Java程式設計師把記憶體控制權利交給Java虛擬機器,一旦出現記憶體洩漏和溢位方面的問題,如果不瞭解虛擬機器是怎樣使用記憶體的,那麼排查錯誤將會是一個非常艱鉅的任務。

2 執行時資料區域

Java虛擬機器在執行Java程式的過程中會把它管理的記憶體劃分成若干個不同的資料區域。

執行時資料區域

2.1 程式計數器

程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等功能都需要依賴這個計數器來完。

另外,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。

2.2 Java虛擬機器棧

與程式計數器一樣,Java虛擬機器棧也是執行緒私有的,它的生命週期和執行緒相同,描述的是Java方法執行的記憶體模型。

Java記憶體可以粗糙的區分為堆記憶體(Heap)和棧記憶體(Stack),其中棧就是現在說的虛擬機器棧,或者說是虛擬機器棧中區域性變數表部分。

區域性變數表主要存放了編譯器可知的各種資料型別、物件引用。

2.3本地方法棧

和虛擬機器棧所發揮的作用非常相似,區別是: 虛擬機器棧為虛擬機器執行Java方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。

2.4 堆

Java虛擬機器所管理的記憶體中最大的一塊,Java堆是所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項以及陣列都在這裡分配記憶體。 Java堆是垃圾收集器管理的主要區域,因此也被稱作GC堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集演算法,所以Java堆還可以細分為:新生代和老年代:在細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收記憶體,或者更快地分配記憶體。

2.5 方法區

方法區與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即使編譯器編譯後的程式碼等資料。

HotSpot虛擬機器中方法區也常被稱為 “永久代”,本質上兩者並不等價。僅僅是因為HotSpot虛擬機器設計團隊用永久代來實現方法區而已,這樣HotSpot虛擬機器的垃圾收集器就可以像管理Java堆一樣管理這部分記憶體了。但是這並不是一個好主意,因為這樣更容易遇到記憶體溢位問題。 相對而言,垃圾收集行為在這個區域是比較出現的,但並非資料進入方法區後就“永久存在”了。

2.6 執行時常量池

執行時常量池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有常量池資訊(用於存放編譯期生成的各種字面量和符號引用)

2.7直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。

JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基於通道(Channel)快取區(Buffer) 的I/O方式,它可以直接使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣就能在一些場景中顯著提高效能,因為避免了在Java堆和Native堆之間來回複製資料

本機直接記憶體的分配不會收到Java堆的限制,但是,既然是記憶體就會受到本機總記憶體大小以及處理器定址空間的限制。

3 HotSpot虛擬機器物件探祕

通過上面的介紹我們大概知道了虛擬機器的記憶體情況,下面我們來詳細的瞭解一下HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的全過程。

3.1 物件的建立

虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被載入過、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。

類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需的記憶體大小在類載入完成後便可確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。分配方式“指標碰撞”“空閒列表” 兩種,選擇那種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定

虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性。

接下來,虛擬機器要對物件進行必要的設定,例如這個物件是那個類的例項、如何才能找到類的後設資料資訊、物件的雜湊嗎、物件的GC分代年齡等資訊。這些資訊存放在物件頭中,根據虛擬機器當前執行狀態的不同,如是否啟用偏向鎖等,物件頭會與不同的設定方式。 new指令執行完後,再按照程式設計師的意願執行init方法後一個真正可用的物件才誕生。

3.2 物件的記憶體佈局

在Hotspot虛擬機器中,物件在記憶體中的佈局可以分為3快區域:物件頭例項資料對齊填充

Hotspot虛擬機器的物件頭包括兩部分資訊第一部分用於儲存物件自身的自身執行時資料(雜湊嗎、GC分代年齡、鎖狀態標誌等等),另一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是那個類的例項。

例項資料部分是物件真正儲存的有效資訊,也是在程式中所定義的各種型別的欄位內容。

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。 因為Hotspot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是物件的大小必須是8位元組的整數倍。而物件頭部分正好是8位元組的倍數(1倍或2倍),因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

3.3物件的訪問定位

建立物件就是為了使用物件,我們的Java程式通過棧上的reference資料來操作堆上的具體物件。物件的訪問方式有虛擬機器實現而定,目前主流的訪問方式有①使用控制程式碼②直接指標兩種:

  1. 如果使用控制程式碼的話,那麼Java堆中將會劃分出一塊記憶體來作為控制程式碼池,reference中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊;
    使用控制程式碼
  2. 如果使用直接指標訪問,那麼Java堆對像的佈局中就必須考慮如何防止訪問型別資料的相關資訊,reference中儲存的直接就是物件的地址。

使用直接指標

這兩種物件訪問方式各有優勢。使用控制程式碼來訪問的最大好處是reference中儲存的是穩定的控制程式碼地址,在物件被移動時只會改變控制程式碼中的例項資料指標,而reference本身不需要修改。使用直接指標訪問方式最大的好處就是速度快,它節省了一次指標定位的時間開銷。

歡迎關注我的微信公眾號:"Java面試通關手冊"(一個有溫度的微信公眾號,期待與你共同進步~~~堅持原創,分享美文,分享各種Java學習資源):

微信公眾號

相關文章