Java記憶體管理原理及記憶體區域詳解

codeceo發表於2015-07-29

一、概述

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干不同的資料區域,這些區域都有各自的用途以及建立和銷燬的時間。Java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域,如下圖所示:

Java記憶體區域詳解

下面就每一個區域進行闡述。

二、執行時資料區域

程式計數器

程式計數器,可以看做是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型裡,位元組碼直譯器工作就是通過改變程式計數器的值來選擇下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都要依賴這個計數器來完成。

多執行緒中,為了讓執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間互不影響、獨立儲存,因此這塊記憶體是 執行緒私有 的。

當執行緒正在執行的是一個Java方法,這個計數器記錄的是在正在執行的虛擬機器位元組碼指令的地址;當執行的是Native方法,這個計數器值為空。

此記憶體區域是唯一一個沒有規定任何OutOfMemoryError情況的區域 。

Java虛擬機器棧

Java虛擬機器棧也是執行緒私有的 ,它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結串列、方法出口資訊等。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

區域性變數表中存放了編譯器可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用和returnAddress型別(指向了一條位元組碼指令的地址)。

如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

本地方法棧

本地方法棧與虛擬機器的作用相似,不同之處在於虛擬機器棧為虛擬機器執行的Java方法服務,而本地方法棧則為虛擬機器使用到的Native方法服務。有的虛擬機器直接把本地方法棧和虛擬機器棧合二為一。

會丟擲stackOverflowError和OutOfMemoryError異常。

Java堆

Java堆是所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立,此記憶體區域的唯一目的就是存放物件例項 。

Java堆是垃圾收集器管理的主要區域。由於現在收集器基本採用分代回收演算法,所以Java堆還可細分為:新生代和老年代。從記憶體分配的角度來看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區(TLAB)。

Java堆可以處於物理上不連續的記憶體空間,只要邏輯上連續的即可。在實現上,既可以實現固定大小的,也可以是擴充套件的。

如果堆中沒有記憶體完成例項分配,並且堆也無法完成擴充套件時,將會丟擲OutOfMemoryError異常。

方法區

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

相對而言,垃圾收集行為在這個區域比較少出現,但並非資料進了方法區就永久的存在了,這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,

當方法區無法滿足記憶體分配需要時,將丟擲OutOfMemoryError異常。

執行時常量池:

是方法區的一部分,它用於存放編譯期生成的各種字面量和符號引用。

直接記憶體

直接記憶體不是虛擬機器執行時資料區的一部分,在NIO類中引入一種基於通道與緩衝區的IO方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。

直接記憶體的分配不會受到Java堆大小的限制,但是會受到本機記憶體大小的限制,所有也可能會拋OutOfMemoryError異常。

三、物件的建立、佈局和訪問過程

物件的建立

建立一個物件通常是需要new關鍵字,當虛擬機器遇到一條new指令時,首先檢查這個指令的引數是否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果那麼執行相應的類載入過程。

類載入檢查通過後,虛擬機器將為新生物件分配記憶體。為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。分配的方式有兩種: 一種叫 指標碰撞 ,假設Java堆中記憶體是絕對規整的,用過的和空閒的記憶體各在一邊,中間放著一個指標作為分界點的指示器,分配記憶體就是把那個指標向空閒空間的那邊挪動一段與物件大小相等的距離。 另一種叫 空閒列表 :如果Java堆中的記憶體不是規整的,虛擬機器就需要維護一個列表,記錄哪個記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄。 採用哪種分配方式是由Java堆是否規整決定的,而Java堆是否規整是由所採用的垃圾收集器是否帶有壓縮整理功能決定的。 另 外一個需要考慮的問題就是物件建立時的執行緒安全問題,有兩種解決方案:一是對分配記憶體空間的動作進行同步處理;另一種是吧記憶體分配的動作按照執行緒劃分在不 同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體(TLAB),哪個執行緒要分配記憶體就在哪個執行緒的TLAB上分配,只有TLAB用完並分配 新的TLAB時才需要同步鎖定。

記憶體分配完成後,虛擬機器需要將分配到的記憶體空間初始化為零值。這一步操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就可以直接使用。

接下來虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的後設資料資訊等,這些資訊存放在物件的物件頭中。

上面的工作都完成以後,從虛擬機器的角度來看一個新的物件已經產生了。但是從Java程式的角度,還需要執行init方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。

物件的記憶體佈局

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可分為三個部分: 物件頭、例項資料和對齊填充。

物件頭包括兩個部分:第一部分用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、執行緒所持有的鎖等。官方稱之為“Mark Word”。第二個部分為是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

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

對齊填充並不是必然存在的,僅僅起著佔位符的作用。、Hotpot VM要求物件起始地址必須是8位元組的整數倍,物件頭部分正好是8位元組的倍數,所以當例項資料部分沒有對齊時,需要通過對齊填充來對齊。

物件的訪問定位

Java程式通過棧上的reference資料來操作堆上的具體物件。主要的訪問方式有使用控制程式碼和直接指標兩種:

控制程式碼:Java堆將會劃出一塊記憶體來作為控制程式碼池,引用中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊 。如圖所示:

Java記憶體區域詳解

直接指標:Java堆物件的佈局要考慮如何放置訪問型別資料的相關資訊,引用中儲存的就是物件地址 。如圖所示:

Java記憶體區域詳解

兩個方式各有優點,使用控制程式碼最大的好處是引用中儲存的是穩定的控制程式碼地址,物件被移動時只會改變控制程式碼中例項的地址,引用不需要修改、使用直接指標訪問的好處是速度更快,它節省了一次指標定位的時間開銷。

相關文章