Java&Android 基礎知識梳理(3) 記憶體區域

澤毛發表於2017-12-21

一、概述

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的區域,它們有的隨著虛擬機器程式的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立而銷燬。 下面,我們就分兩個部分討論:

  • 執行緒隔離的資料區
  • 所有執行緒共享的資料區

二、執行緒隔離的資料區

2.1 程式計數器

  • 概念 程式計數器是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時會通過改變這個計數器的指來取下一條需要執行的位元組碼指令。 如果執行緒正在執行的是Java方法,那麼計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,那這個計數器則為空。 此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OOM情況的區域。
  • 為什麼需要執行緒隔離 由於Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器。

2.2 Java虛擬機器棧

  • 概念 虛擬機器棧描述的是Java方法執行的記憶體模型,每個方法在執行的同時會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。 區域性變數表:存放了編譯期可知的各種基本資料型別(boolean/byte/..),物件引用(指向物件起始地址的引用指標,或者是指向一個代表物件的控制程式碼,或者是其它與此物件相關的位置)和returnAddress地址。 區域性變數表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧中分配多大區域性變數空間是完全確定的,在方法執行期間不會改變區域性變數表的大小。 每一個方法從呼叫到執行完成的過程,就對應一個棧幀在虛擬機器棧中出棧到入棧的過程。
  • 為什麼需要執行緒隔離 因為每個執行緒所執行的邏輯和時序不同,所以它們的虛擬機器棧自然也就不會一定相同,因此不能共用。
  • 異常 如果執行緒請求的棧深度大於虛擬機器所允許的深度,會丟擲StackOverflowError異常;如果虛擬機器棧可以動態擴充套件,當擴充套件時無法申請到足夠的記憶體,就會丟擲OOM

2.3 本地方法棧

  • 概念 和Java虛擬機器方法棧類似,不過本地方法棧為虛擬機器使用到的Native方法,有些虛擬機器(譬如HotSpot)直接將本地方法棧和虛擬棧合二為一。
  • 異常 和虛擬機器棧相同。

三、執行緒共享的資料區

3.1 Java

  • 概念 Java堆在虛擬機器啟動時建立,它的目的是存放物件例項,它也是垃圾收集器管理的主要區域。 Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上連續即可,在實現上,既可以實現成固定大小的,也可以是可擴充套件的。
  • 異常 如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,會丟擲OOM異常。

下面我們討論一下堆中的物件分配、佈局和訪問過程

3.1.1 物件的建立

物件的建立分為以下幾步:

  • 第一步:當虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入、解析和初始化過,如果沒有,那麼必須執行相應的類載入過程,
  • 第二步:接下來虛擬機器將為新生物件分配記憶體,物件所需記憶體的大小在類載入完成後便可確定,分配的方式有兩種:
  • 指標碰撞:用過和空閒的記憶體以指標作為分界點的指示器,分配記憶體就是把指標向空閒空間那邊挪動一個與物件大小相等距離,這種方式要求記憶體是規整的。
  • 空閒列表:維護一個列表,記錄哪些記憶體是可用的,在分配和回收時更新列表。

多執行緒的問題下的解決方案:

  • 對分配記憶體空間的動作進行同步,虛擬機器上採用CAS配上失敗重試的方式保證更新操作的原子性。

  • 每個執行緒在Java堆中預先分配一小塊記憶體,成為本地執行緒分配緩衝TLAB,哪個執行緒需要分配記憶體,就在哪個執行緒的TLAB上分配,只有TLAB用完需要分配新的TLAB才需要同步。

  • 第三步:在記憶體分配完成,把除了物件頭之外的分配到的記憶體空間都初始化為零值,接下來就是對物件進行必要的設定,這些資訊存放在物件頭中。

  • 第四步:當物件頭設定完畢之後,從虛擬機器的視角來看,新的物件就產生了,接著就執行<init>方法,把物件按照程式設計師的意願進行初始化。

3.1.2 物件的記憶體佈局

物件在記憶體中儲存的佈局可以分為三個區域:物件頭、例項資料、對其填充。

  • 物件頭

  • 儲存物件自身的執行時資料:HashCodeGc分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,所佔位數和虛擬機器位數相同。

  • 型別指標:物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項

  • 如果物件是一個Java陣列,那麼在物件頭中還必須有一塊記錄陣列長度的資料。

  • 例項資料 包括在父類和子類中所定義的各種型別的欄位內容,儲存順序收到虛擬機器分配策略引數的影響,相同寬度的欄位總是被分配到一起,在滿足這個前提條件下,父類中定義的變數會出現在子類之前。

  • 對齊填充 HotSpot要求物件的大小必須是8位元組的整數倍,而物件頭部分正好是8位元組的整數倍,當物件例項資料部分沒有對齊時,就需要通過對齊填充來不全。

3.1.3 物件的訪問

物件的訪問有兩種方式:

  • 使用控制程式碼,Java堆中劃分出一塊記憶體作為控制程式碼池,reference中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的地址資訊。 優點:reference中儲存的是穩定的控制程式碼地址,在物件被移動時,只會改變控制程式碼中的例項資料指標,而reference本身不需要修改。
  • 直接指標:在Java堆物件的佈局中放置訪問型別資料的相關資訊,reference中儲存的直接就是物件地址。 優點:速度快,HotSpot採用的就是這種方式。

3.2 方法區

  • 概念 方法區用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
  • 為什麼把方法區稱為“永久代” HotSpot選擇把GC分代收集器擴充套件至方法區,或者說用永久代來實現方法區,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分記憶體,能夠省去專門為這個方法區編寫記憶體管理的程式碼。 對這區域的記憶體回收主要是針對常量池的回收和對型別的解除安裝。
  • 異常 當方法區無法滿足記憶體分配需求時,將丟擲OOM
  • 執行時常量池 執行時常量池是方法區的一部分。 Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。 並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,例如String類的intern方法。 當常量池中無法再申請記憶體,就會丟擲OOM異常。

相關文章