1. 程式人生 > >java記憶體管理機制剖析(一)

java記憶體管理機制剖析(一)

最近利用工作之餘學習研究了一下java的記憶體管理機制,在這裡記錄總結一下。

1-1、java記憶體區域

當java程式執行時,java虛擬機器會將記憶體劃分為若干個不同的資料區域,這些記憶體區域建立和銷燬的時間各不相同,所承擔的功能也不相同,他們各司其職,各盡所責。這些區域的劃分如下圖 

執行時資料區主要有五個區,分別是 堆 ,方法區,虛擬機器棧,本地方法棧,程式計數器 ,下面我來一一詳細講解這五個資料區

java堆是java虛擬機器管理記憶體中最大的一塊,它是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立, 此記憶體的唯一目的就是存放物件例項,幾乎所有的物件例項以及陣列都在堆分配記憶體 。

java虛擬機器規定,java堆可以處於物理上不連續的記憶體空間中,只要邏輯上連續即可。在實現時,既可以實現固定大小的,也可以是擴充套件的,可以 通過配置-Xmx和-Xms來擴充套件大小 。如果堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError

方法區

方法區也是被所有執行緒共享的一塊記憶體區域,在Java虛擬機器規範中,方法區是堆的邏輯組成部分,但他又被與堆區分開來,別名稱為Non-Heap,它主要的儲存內容有下面幾點

* 型別的完整有效名
* 型別直接父類的完整有效名
* 型別的修飾符(public,abstract,final的某個子集)
* 型別的常量池
* 域(Field)資訊
* 方法(Method)資訊
* 除了常量外的所有靜態(static)變數

總結起來就是主要用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,編譯器編譯後的程式碼等資料

這裡我在介紹一下常量池,域資訊和方法資訊

* 常量池
常量池也稱為執行時常量池(Runtime Constant Pool),用於存放編譯期生成的各種字面量和符號引用,它是這個型別用到的常量的一個有序集合,包括 實際的常量(String, Integer, 和Floating point常量)和型別,域和方法的符號引用 。 
池中的資料項像陣列項一樣,是通過索引訪問的。 因為常量池儲存了一個類型別所使用到的所有型別,域和方法的符號引用,所以它在java程式的動態連結中起了核心的作用

* 域(Field)資訊
域的相關資訊包括: 域名; 域型別; 域修飾符(public, private, protected,static,final volatile,transient的某個子集)

* 方法(Method)資訊
方法的相關資訊包括: 方法名, 方法的返回型別(或 void), 方法引數的數量和型別(有序的),方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個子集) ,除了abstract和native方法外,其他方法還有儲存方法的 位元組碼(bytecodes)    運算元棧和方法棧幀的區域性變數區的大小 。

java虛擬機器規範對方法區的限制比較寬鬆,除了和java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充套件外,還可以選擇不是實現垃圾收集。垃圾收集行為在方法區也比較少出現,當方法區無法滿足記憶體分配時,會丟擲 OutOfMemoryError

虛擬機器棧

虛擬機器棧是執行緒私有的,它的生命週期與執行緒相同,當我們start一個執行緒時,jvm會為當前執行緒開闢一塊虛擬機器棧,噹噹前執行緒死亡時,執行緒的虛擬機器棧也會銷燬。

程式碼中每個方法在執行的同時,都會建立一個棧幀(Stack Frame)用於儲存區域性變量表,運算元棧,動態連結,方法出口等資訊。每一個方法呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

區域性變量表存放了編譯期可知的各種基本資料型別(boolean,byte,char,short,int,float,long,double),物件引用和returnAddreass型別

JVM對這個區域規定了兩種異常情況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲SstackOverFlowError異常;如果虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryErro異常

本地方法棧

本地方法棧和虛擬機器棧的作用是一樣的,只不過本地方法棧是虛擬機器執行java方法時開闢的棧,而本地方法棧是虛擬機器用到Native方法時,開闢的棧。

程式計數器

程式計數器是一塊較小的記憶體,它可以看作是當前執行緒的位元組碼的行號指示器。在虛擬機器的概念模型裡,位元組碼直譯器工作時就是通過改變這個計數器的值來選取嚇一跳需要執行的位元組碼指令,分支,迴圈,跳轉,異常處理,執行緒恢復等基礎功能。

由於java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各執行緒指尖計數器互不影響,獨立儲存。

1-2、物件建立

瞭解了記憶體的資料區域,我們可以進一步瞭解物件是如何建立的了。這裡先通過一張流程圖一窺java的物件建立過程 


可以看到,當虛擬機器遇到一條new指令時,首先去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經載入。如果沒有,則執行載入。

載入完成後,便會在堆中為物件分配記憶體,JVM有兩種分配方式 ①指標碰撞,②空閒列表 ,下面我詳細講講這兩種分配方式。

* 指標碰撞
當java堆中記憶體是整齊的,所有用過的記憶體都放一邊,空閒的記憶體放在另一邊,中間放著一個指標座位分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊摞動一段與物件大小相等的距離,這種分配就叫指標碰撞
* 空閒列表
當Java堆的記憶體並不是完整的,已分配的記憶體和空閒記憶體相互交錯,JVM通過維護一個列表,記錄可用的記憶體塊資訊,當分配操作發生時,從列表中找到一個足夠大的記憶體塊分配給物件例項,並更新列表上的記錄。這種分配方式稱為空閒列表

當JVM所採用的垃圾收集器帶有壓縮整理功能時,java堆是規整的,這個時候會採用指標碰撞分配記憶體,否則會採用空閒列表分配記憶體。物件建立是一個非常頻繁的行為,進行堆記憶體分配時還需要考慮多執行緒併發問題,可能出現正在給物件A分配記憶體,指標或記錄還未更新,物件B又同時分配到原來的記憶體,解決這個問題有兩種方案:

* 採用CAS保證資料更新操作的原子性;
* 把記憶體分配的行為按照執行緒進行劃分,在不同的空間中進行,每個執行緒在Java堆中預先分配一個記憶體塊,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer, TLAB);