1. 程式人生 > >深入理解java虛擬機器系列(一):java記憶體區域與記憶體溢位異常

深入理解java虛擬機器系列(一):java記憶體區域與記憶體溢位異常

文章主要是閱讀《深入理解java虛擬機器:JVM高階特性與最佳實踐》第二章:Java記憶體區域與記憶體溢位異常

的一些筆記以及概括。

好了開始。如果有什麼錯誤或者遺漏,歡迎指出。

一、概述

先上一張圖


這張圖主要列出了Java虛擬機器管理的記憶體的幾個區域。

常有人把Java記憶體區分為堆記憶體(Heap)和棧記憶體(Stack),這種分法比較粗糙,Java記憶體區域的劃分實際上遠比這複雜,從上圖就可以看出了。堆疊分法中所指的“棧”實際上只是虛擬機器棧,或者說是虛擬機器棧中的區域性變量表部分。接下來主要講解區域的各個部分.

二、執行時資料區域

1.程式計數器

  • .程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型裡(僅是概念模型,各種虛擬機器可能會通過一些更高效的方式去實現),位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令
  • Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。
  •  執行緒正在執行的是一個Java方法,如果正在執行的是Natvie方法,這個計數器值則為空(Undefined)。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

2.Java虛擬機器棧

棧幀是方法執行期的基礎資料結構棧容量可由-Xss引數設定


  • 與程式計數器一樣,Java虛擬機器棧(Java Virtual Machine Stacks)也是執行緒私有的,它的生命週期與執行緒相同。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
  • 常有人把Java記憶體區分為堆記憶體(Heap)和棧記憶體(Stack),這種分法比較粗糙,Java記憶體區域的劃分實際上遠比這複雜。其中所指的“堆”在後面會專門講述,而所指的“棧”就是現在講的虛擬機器棧,或者說是虛擬機器棧中的區域性變量表部分。
  • 區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。主要存放了編譯期可知的各種基本資料型別、物件引用(reference型別)、returnAddress型別

3.本地方法棧

棧容量可由-Xss引數設定

虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native方法服務。有的虛擬機器(譬如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。

4.Java堆

可通過引數 -Xms 和-Xmx設定  。

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

5.方法區

引數-XX:MaxPermSize可設定 .

方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

6.執行時常量池

可以通過-XX:PermSize和-XX:MaxPermSize設定

  • 執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。
  • 執行時常量池相對於Class檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只能在編譯期產生,執行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。

7.直接記憶體

可通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java堆的最大值(-Xmx指定)一樣

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

三、HotSpot虛擬機器物件探祕

主要探討HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的全過程。

1.物件的建立

虛擬機器遇到new指令時,

  • 首先去檢查這個指令的引數能否在常量池中定位到一個類的符號引用,並且檢查引用代表的類是否已被載入、解析和初始化過。如果沒有,則執行類載入過程(第7章)。
  • 載入檢查通過後,分配記憶體(記憶體在類載入完成後便可完全確定)。
  • 記憶體分配完成後,虛擬機器對物件進行必要的設定,如物件是哪個類的例項、如何找到類的元資料資訊等(都放在物件的物件頭中)。
  • 從虛擬機器角度看,一個新的物件產生了,但從java程式視角看,物件建立才剛剛開始,因為<init>方法還沒有執行,,所有欄位為零。執行new指令之後會接著執行<init>方法(構造方法),進行初始化,這樣一個真正可用的物件才算完成產生。

2.物件的記憶體佈局

分為物件頭、例項資料、對齊填充三部分。

1)物件頭

包含兩部分

  • 儲存物件自身的執行時資料,如雜湊碼、GC分代年齡等。長度在32位和64位的虛擬機器中,分別為32bit、 64bit,官方稱它為“Mark Word”
  • 型別指標,物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

注:如果物件是一個java陣列,物件頭中還必須有一塊記錄資料長度的資料。

2)例項資料

物件真正儲存的有用資訊,也是程式中定義的各種型別的欄位內容。

3)對齊填充

由於HotSpot虛擬機器要求物件的起始地址必須是8位元組的整數倍,通俗的說,就是物件大小必須是8位元組的整數倍。物件頭正好是8位元組的倍數。當例項資料部分沒有對齊時,需要通過對齊填充來補全。

3.物件的訪問定位

物件訪問在Java語言中無處不在,是最普通的程式行為,但即使是最簡單的訪問,也會卻涉及Java棧、Java堆、方法區這三個最重要記憶體區域之間的關聯關係,如下面的這句程式碼:
   Object obj = new Object();
 假設這句程式碼出現在方法體中,那“Object obj”這部分的語義將會反映到Java棧的本地變量表中,作為一個reference型別資料出現。

而“new Object()”這部分的語義將會反映到Java堆中,形成一塊儲存了Object型別所有例項資料值(Instance Data,物件中各個例項欄位的資料)的結構化記憶體
由於reference型別在Java虛擬機器規範裡面只規定了一個指向物件的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java堆中的物件的具體位置,因此不同虛擬機器實現的物件訪問方式會有所不同,主流的訪問方式有兩種:使用控制代碼和直接指標。

四、總結

最後上一張本章結構圖: