1. 程式人生 > >Java虛擬機器——Java記憶體區域

Java虛擬機器——Java記憶體區域

1、執行時區域

  Java虛擬機器在執行Java程式的時候會把它管理的內厝劃分為若干個不同功能的資料區域,如圖所示

  • 首先是程式計數器,程式計數器可以理解為當前程式執行的位元組碼的行號指示器,計數器中的資料即是下一條將要執行的位元組碼指令的行號。因為Java虛擬機器的多執行緒是通過輪流切換並分配處理器執行時間的方式來實現的,在任意一個時刻,一個處理器(單核)或是一個核(多核)都只會執行一個執行緒中的指令,所以,每個執行緒都擁有自己的程式計數器,還有就是如果執行的是一個Java方法,那麼計數器中記錄的就是位元組碼的地址,如果執行的是Native方法,那麼計數器的值則是空(UnderFined)
  • 虛擬機器棧,虛擬機器棧描述的是Java方法執行的記憶體模型,每次方法在執行的時候都會建立一個棧幀,棧幀中儲存了局部變量表、運算元棧、動態連結、方法出口等等資訊。每一個方法的從呼叫到執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧出棧的過程。如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常,如果棧可以動態擴充套件,且擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。區域性變量表存放了編譯器可知的各種基本資料型別(boolean、byte、char、short、float、long、double)、物件引用(reference型別,它可能是一個指向物件起始地址的指標,也可能是一個代表物件的控制代碼或是其他於此物件相關的位置)和returnAddressl型別(指向了一條位元組碼指令的地址)
  • 本地方法棧和虛擬機器棧的作用是類似的,區別就是本地方法棧是為Native方法服務的,同樣也會丟擲StackOverflowError異常和OutOfMemoryError異常。
  • Java堆,Java堆首先是所有的執行緒共享的一塊記憶體區域,這個區域的唯一目的就是用於存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。如果堆中沒有足夠的記憶體完成例項分配,並且對也無法再擴充套件,將會丟擲OutOfMemoryError異常。
  • 方法區,這也是所有的執行緒共享的一塊記憶體區域,它用於儲存已被虛擬機器載入的類的資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,當方法區無法滿足記憶體分配需求時,將會丟擲OutOfMemoryError異常。
  • 執行時常量池,這是方法區的一部分,這部分專門用於存放編譯期產生的各種字面量和符號引用,這部分將在類被載入後進入方法區的常量池中存放,和方法區一樣無法滿足記憶體分配需求時,將會丟擲OutOfMemoryError異常。
  • 直接記憶體,這部分並不是虛擬機器執行時資料區的一部分,但也被頻繁使用。這部分出現的是因為Native函式自己建立的,就比如NIO類讀取檔案的時候,會直接分配堆外記憶體,這一部分記憶體就是直接記憶體,直接記憶體區域動態擴充套件時出現記憶體無法滿足時會丟擲OutOfMemoryError異常。

 

2、物件的建立

A a = new A();

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

  類載入檢查通過後,虛擬機器將這個新生物件在堆中分配記憶體,所需要的記憶體大小將在類載入完成後確定,假設Java堆中的記憶體是規整的,也就是說空閒的在一邊,非空閒的在一邊,當分配記憶體的時候只需要指向中間邊界的指標向空閒那邊移動,這種方式稱作指標碰撞,如果是非規整的,空閒的和非空閒的互相交錯著,那麼將有一個列表記錄著那部分是空閒的,這樣分配的時候就需要從這個列表中找到合適的空閒區域,並更新表中的記錄,這種方式叫作空閒列表。

  記憶體分配好之後,虛擬機器將對分配的記憶體空間都初始化為零值,初始化完成後,虛擬機器將對物件進行必要的設定,例如物件是屬於那個類的例項、怎麼才能找到類的元資料資訊,物件的雜湊碼、物件的GC分代年齡。等設定結束後,虛擬機器就已經建立好了一個新的物件,接下來在執行初始化方法init方法,這樣一個完整的物件就算完全生成了。

 

3、物件的記憶體佈局

  物件在記憶體中儲存的佈局分為3塊區域:物件頭、例項資料和對齊補充

  • 物件 頭包括兩部分,第一部分用於儲存物件自身執行時的資料,例如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,第二部分是型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。另外如果物件是一個Java陣列,那麼在物件頭陣列中還必須有存放記錄陣列長度的資料。
  • 例項資料部分是物件真正儲存的有效資訊,就是程式中定義的各種型別的欄位內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來
  • 對齊補充這一部分 不一定存在也沒有什麼特殊的含義,僅僅起到佔位符的作用,因為有的虛擬機器的自動記憶體管理系統要求物件的起始地址必須是8位元組整數倍,因此需要物件的大小也必須是8位元組的整數倍。

 

4、物件的訪問定位

  使用物件需要找到物件在堆中的地址,通常定位一個物件的地址主流的方式有使用控制代碼和直接指標兩種

  • 使用控制代碼,Java堆中會專門劃分出一塊記憶體來作為控制代碼池,棧上的reference資料中儲存的就是控制代碼的地址,而控制代碼中包含了物件例項資料與型別資料各自的地址資訊。
  • 直接指標將直接指向Java堆中的Java例項資料的地址,Java堆中的例項資料也將有一個指向物件型別資料的指標。

  這兩種方法各有各的好處,控制代碼方式中reference中儲存的控制代碼地址是一直不變的,當物件被移動的時候(在垃圾處理的時候物件被移動是很普遍的行為)時只會改變控制代碼中的例項資料指標。

  而直接指標的好處就是速度更快,當你要操作一個物件的時候,控制代碼方式需要先定位到控制代碼在定位到物件例項,直接指節省了一次指標定位的開銷。