天天看點

深入了解虛拟機之Java記憶體區域

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

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

深入了解虛拟機之Java記憶體區域

程式計數器是一塊較小的記憶體空間,可以看作是目前線程所執行的位元組碼的行号訓示器。位元組碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等功能都需要依賴這個計數器來完。

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

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

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

局部變量表主要存放了編譯器可知的各種資料類型、對象引用。

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

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

方法區與Java堆一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即使編譯器編譯後的代碼等資料。

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

運作時常量池是方法區的一部分。Class檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有常量池資訊(用于存放編譯期生成的各種字面量和符号引用)

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

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

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

通過上面的介紹我們大概知道了虛拟機的記憶體情況,下面我們來詳細的了解一下HotSpot虛拟機在Java堆中對象配置設定、布局和通路的全過程。

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

在類加載檢查通過後,接下來虛拟機将為新生對象配置設定記憶體。對象所需的記憶體大小在類加載完成後便可确定,為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。配置設定方式有 “指針碰撞” 和 “空閑清單” 兩種,選擇那種配置設定方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。

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

接下來,虛拟機要對對象進行必要的設定,例如這個對象是那個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希嗎、對象的GC分代年齡等資訊。這些資訊存放在對象頭中,根據虛拟機目前運作狀态的不同,如是否啟用偏向鎖等,對象頭會與不同的設定方式。 new指令執行完後,再按照程式員的意願執行init方法後一個真正可用的對象才誕生。

在Hotspot虛拟機中,對象在記憶體中的布局可以分為3快區域:對象頭、執行個體資料和對齊填充。

Hotspot虛拟機的對象頭包括兩部分資訊,第一部分用于存儲對象自身的自身運作時資料(哈希嗎、GC分代年齡、鎖狀态标志等等),另一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是那個類的執行個體。

執行個體資料部分是對象真正存儲的有效資訊,也是在程式中所定義的各種類型的字段内容。

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

建立對象就是為了使用對象,我們的Java程式通過棧上的reference資料來操作堆上的具體對象。對象的通路方式有虛拟機實作而定,目前主流的通路方式有①使用句柄和②直接指針兩種:

如果使用句柄的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊; 

深入了解虛拟機之Java記憶體區域

如果使用直接指針通路,那麼Java堆對像的布局中就必須考慮如何防止通路類型資料的相關資訊,reference中存儲的直接就是對象的位址。

深入了解虛拟機之Java記憶體區域

這兩種對象通路方式各有優勢。使用句柄來通路的最大好處是reference中存儲的是穩定的句柄位址,在對象被移動時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。使用直接指針通路方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

來源:https://mp.weixin.qq.com/s/IL_uLVS-G0M48YnTUP9N5Q

繼續閱讀