1. 程式人生 > >Java記憶體模型與物件揭祕

Java記憶體模型與物件揭祕

看了很多個關於Java記憶體模型的部落格,這篇部落格有著獨到的見解.

前言:最近看了《深入jvm》一書,感受頗深,但是不寫點什麼總感覺不是自己的,所以動手捋一捋。主要講的內容是java的記憶體區域,物件的建立,物件的記憶體佈局和物件的訪問方式。

一、java的記憶體區域劃分

這個問題幾乎是面試官必問的問題,很多人都會直接回答:“堆和棧”。其實這種劃分是很粗略的,要是遇到認真的面試官,你就尷尬了。說個題外話,不要把“棧”說成“堆疊”,雖然說很多書上它們是等同的,但是還是有部分面試官不買你的賬,博主就因此跌過坑。接下來看看java記憶體區域的細緻劃分吧,如下圖:

看了這個模型,發現我們使用堆和棧劃分記憶體區域還是很有道理的,java虛擬機器規範將方法區描述為堆的一個邏輯部分。這種模型和我們粗略的記憶體模型的對應關係為(等號左側是粗略的模型,右側是細緻的模型):

堆=堆+方法區

棧=虛擬機器棧+本地方法棧

所以說這種堆和棧的劃分事實上已經囊括了大部分的記憶體區域。

接下來我們來看看各個記憶體區域的區別:

  • 程式計數器:執行緒隔離,即每個執行緒都有自己的程式計數器,並且互不影響。分為兩種情況,當執行緒正在執行的是一個java方法,它的作用是作為位元組碼的行號指示器,指向下一條需要執行的指令。當執行緒正在執行的是一個Native方法,那麼它的值為空(Undefined)。java虛擬機器規範中唯一沒有定義OOM異常的記憶體區域。
  • java虛擬機器棧:執行緒隔離,生命週期與執行緒相同。它描述的是java方法執行的記憶體模型,每一個java方法執行的時候都會產生一個棧幀,用於儲存區域性變量表,運算元棧,動態連結,方法出口等資訊。當進入一個方法時,棧幀的大小是編譯器確定的,執行時不會改變其大小。當虛擬機器棧不可擴充套件的時候,可能丟擲StackOverflowError異常,反之,可能丟擲OOM異常。
  • 本地方法棧:與java虛擬機器棧功能一致,只不過本地方法棧是針對Native方法的。同樣在虛擬機器規範中定義了StackOverflowError和OOM兩種異常。
  • Java堆:這個應該是我們最熟悉的區域了,只要是用到new關鍵字建立的物件都會進入到這個區域,包括物件,陣列。堆還能進一步劃分,比如按照記憶體回收的角度來看,堆可以進一步劃分為新生代(Eden+Survivor)和老年代。按照記憶體分配的角度來看,堆可以進一步劃分為多個執行緒私有的分配快取區,即TLAB(Thread Local Allocation Buffer)。這種進一步的劃分是為了更高效地回收和分配記憶體。java虛擬機器規範中定義了OOM異常。
  • 方法區:這個區域很容易引發誤會,很多人會以為方法區會儲存方法中的區域性變數,然而並不是。這個區域用於儲存被載入的類的資訊,常量,靜態變數以及即時編譯器編譯後的程式碼等資料。虛擬機器規範中定義了OOM異常。還需要注意的一點是,HotSpot虛擬機器中的方法區被很多人稱之為“永生代”,這是因為HotSpot的開發團隊將分代收集演算法運用到了方法區,但是這並不是必須的。

附:再說說直接記憶體的概念,為什麼分開來說呢?這是因為直接記憶體並不是java執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。直接記憶體事實上是系統中沒有分配給當前程序的記憶體,因為系統分配給一個程序的空間往往是有限的,所以直接記憶體常被用來擴充套件可用的記憶體區域,也可以用來提升效能。原理是這樣的,使用Native函式庫直接分配堆外記憶體,通過一個儲存在java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作,從而避免了在java堆和Native堆之間複製資料的開銷。

二、物件的建立

大體上分為4個步驟:類載入、分配記憶體、記憶體區域的初始化、虛擬機器對物件進行必要的設定。

  • 類載入:虛擬機器遇到一條new指令的時候,首先檢查是否有必要進行類載入,即檢查指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用是否已經被載入、解析和初始化,若沒有,則進行類載入。
  • 分配記憶體:若是虛擬機器的垃圾回收器有壓縮整理記憶體的功能,即指標將記憶體區域分為兩個部分,一邊已分配,另一邊未分配,則採用指標碰撞(Bump the Pointer)進行記憶體分配。否則採用空閒列表(Free List)分配記憶體。
  • 記憶體空間初始化為零值:顧名思義,將物件分配到的記憶體空間進行初始化,但是不包括物件頭。
  • 對物件進行一些必要的設定:例如這個物件是哪個類的例項,如何找到類的元資料資訊,物件雜湊碼,GC分代年齡等資訊。這些資訊存放在物件頭(Object Header)中。

至此,虛擬機器認為這個物件已經建立完畢,但是在我們程式中,這個物件才剛剛可以使用。

三、物件的佈局

以HotSpot虛擬機器為例,物件在記憶體中的儲存佈局分為三個部分:物件頭(Object Header)、例項資料(Instance Data)、對齊填充(Padding)。

  • 物件頭:包括兩部分的資訊,第一部分用於儲存物件本身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。第二部分用於儲存物件的類元資料指標,虛擬機器可以通過這個指標找到物件的類元資料。(還有通過控制代碼尋找物件的並不需要這個指標,下面將會說到)
  • 例項資料:例項資料沒啥好說的,就是物件真正儲存的有效資訊,程式碼中定義的各型別欄位內容。
  • 對齊填充:它並不是必須的,僅僅起到了佔位符的作用。因為HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須要是8的整數倍,而物件頭剛好就是8的整數倍,因此我們只需要對例項資料進行部分填充就可以使得物件的起始地址是8的整數倍。這種考慮是為了設計簡單和定址效率。

四、物件的訪問方式

瞭解了物件的建立和物件在記憶體中的佈局之後,接下來就應該考慮如何去訪問它了。物件的訪問方式大體上分為兩種,一種是直接指標,一種是控制代碼池,取決於具體的虛擬機器實現。

  • 控制代碼池:這種方式java堆需要劃分一塊記憶體來作為控制代碼池,物件的引用reference指向的就是控制代碼地址,而控制代碼地址中存放的是物件的例項資料和型別資料的地址指標。這種方式也就是我們上面說到的,可以不通過物件來找到其類元資料,此時物件的物件頭可以不存型別資料的指標。模型圖如下:

  • 直接指標:這種方式就是我們常規的認識,因為我們經常是使用HotSpot虛擬機器的,而這個虛擬機器就是採用直接指標實現。這種方式物件的引用reference指向物件的存放空間地址。模型如下:

這種訪問方式有什麼區別呢?控制代碼訪問方式是穩定的控制代碼地址,在物件被移動的時候,只會改變控制代碼中例項資料的指標,而reference本身不需要修改,但是多了一次定址開銷。直接指標則是快,但是相應地在同場景下需要修改reference指標值。這些修改時虛擬機器自動完成的,對程式設計師透明,但是相應的開銷是無法避免的。

作者:吳coder 來源:CSDN 原文:https://blog.csdn.net/sinat_34596644/article/details/51761714?utm_source=copy