1. 程式人生 > >Java記憶體區域與虛擬機器類載入機制

Java記憶體區域與虛擬機器類載入機制

一、Java執行時資料區域 

640?wx_fmt=other&wxfrom=5&wx_lazy=1

1、程式計數器

  “執行緒私有”的記憶體,是一個較小的記憶體空間,它可以看做當前執行緒所執行的位元組碼的行號指示器。Java虛擬機器規範中唯一一個沒有OutOfMemoryError情況的區域。

  位元組碼直譯器工作時就說通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

2、Java虛擬機器棧

  Java 虛擬棧,執行緒私有的,它的生命週期與執行緒相同。每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。

  通過人們所說的“棧”就說虛擬機器棧,或說是虛擬機器棧中的區域性變量表部分。

  • 區域性變量表存放了編譯期可知的各種基本資料型別(bloolean,byte,char,short,int,float,long,double),物件引用(reference型別,它不等同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。區域性變量表所需的記憶體空間在編譯器間完成分配,當進入一個方法是,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。

  • 一個後進先出(Last-In-First-Out)的運算元棧,也可以稱之為表示式棧(Expression Stack)。運算元棧和區域性變量表在訪問方式上存在著較大差異,運算元棧並非採用訪問索引的方式來進行資料訪問的,而是通過標準的入棧和出棧操作來完成一次資料訪問。每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,一個32bit的數值可以用一個單位的棧深度來儲存,而2個單位的棧深度則可以儲存一個64bit的數值,當然運算元棧所需的容量大小在編譯期就可以被完全確定下來,並儲存在方法的Code屬性中。  

  這個區域有兩個異常:
     ① 如果執行緒請求的棧深度大於虛擬所允許的深度,將拋StackOverflowError異常;
     ② 虛擬機器棧可以動態擴充套件,但擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

3、本地方法棧

  本地方法棧作用與虛擬機器棧相似,區別在於虛擬機器棧為虛擬機器執行java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務,執行緒私有
  Native方法常用的兩種請求:
    ① 在方法中呼叫一些不是有java語言寫的程式碼;
    ② 在方法中用java語言直接操作計算機硬體;
  異常:StackOverflowError、OutOfMemoryError

4、Java堆(Java Heap)

  Java堆是Java虛擬機器所管理的記憶體中最大的一塊。在虛擬機器啟動時建立,此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。
  Java堆是垃圾收集器管理的主要區域,因此也稱為“GC堆”;
  如果在堆中沒有記憶體完成例項分配,並且堆也無法擴充套件時,將會丟擲OutOfMemoryError異常。

5、方法區

  方法區用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。  當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。

  • 執行時常量池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於儲存編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

  • 執行時常量池相對於Class檔案常量池的另外一個特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用的比較多的便是String類的intern()方法。

 二、物件的建立

  1、虛擬機器遇到一條new指令時,首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入,解析和初始化過,如果沒有則必須執行相應的類載入過程。

  2、在類載入檢查通過後,接下來虛擬機器將新生物件分配記憶體。物件所需記憶體的大小在類載入完成後便可完全確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃出來。

  3、記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭)。

  4、虛擬機器要對物件進行必要的設定,主要針對物件頭的設定。

    • 第一部分,用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。(Mark Word)  

    • 第二部分,類性指標,即物件指向他的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

    • 物件頭:

  5、從虛擬機器的角度來看,一個新的物件已經產生了,但從Java查詢的視角來看,物件建立才剛剛開始—— <init>方法還沒執行,所有的欄位都還為零。

     執行完new指令之後會接著執行<init>方法,把物件按照程式設計師的意願進行初始化。這樣一個真正的物件才算完全產生出來。

三、物件的記憶體佈局

  物件在記憶體中儲存的佈局可以分為3塊局域:物件頭(Header)、例項資料(Instance Data)、對齊填充(Padding)。

  例項資料:物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。

  對齊填充:並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。

四、物件的訪問定位

  建立物件是為了使用物件,我們的Java程式需要通過棧上的reference資料來操作堆上的具體物件。目前主流的訪問方式有使用控制代碼和直接指標兩種。

  1、如果使用控制代碼訪問的話,那麼Java堆中將會劃出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別各自的具體地址資訊。

640?wx_fmt=other

  2、如果通過直接指標訪問,那麼Java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件的地址。(Sun HotSpot的實現方式)

640?wx_fmt=other

五、虛擬機器類的載入機制

  (一)、類載入的時機

  1、類的生命週期:類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止。

640?wx_fmt=other

  圖中,載入、驗證、準備、初始化和解除安裝這五個階段的順序是確定的,類的載入過程必須按照這種順序按部就班的開始,而解析階段則不一定。

  2、需立即對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始)的有且只有的5種請求:

    ① 遇到new、gerstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。

    如:使用new關鍵字例項化物件時候,讀取或設定一個類的靜態欄位(被final修飾,已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。

    ②使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

    ③當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

    ④當虛擬機器啟動時,使用者需要指定一個要執行的主類(包括main()方法的那個類),虛擬機器會先初始化這個主流。

    ⑤當使用JDK1.7的動態語言支援時。

  (二)、類載入的過程

  類載入的全過程:載入、驗證、準備、解析和初始化這五個階段。  

  1、載入

  “載入”是“類載入”過程的一個階段。 

   ① 通過一個類的全限定名來獲取定義此類的二進位制位元組流(通過類載入實現);

     ② 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;

   ③ 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

   2、驗證

  連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

  大致完成4個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。

  3、準備

  正式為變數分配記憶體並設定類變數初始值得階段,這些變數所使用的記憶體都在方法區中進行分配。首先這個時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中,其次,這裡所說的“初始值”通常情況下是資料型別的零值。

  4、解析

  解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

    • 符號引用:符號應用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時無歧義地定位到目標即可,與虛擬機器實現的記憶體佈局無關,引用的目標並不一定以及載入到記憶體中。

    • 直接引用:直接引用可以直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局相關的。引用的目標必定已存在於記憶體中。

  在16個用於操作符號引用的位元組碼指令之前,先對它們所使用的符號引用進行解析。所有虛擬機器實現可以根據需要來判斷到底是在類被載入器載入時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用之前才去解析它。

  解析動作主要針對類或介面、欄位、類方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行解析。

  5、初始化  

  初始化階段是類載入過程的最後一步,前面的類載入過程中,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式程式碼(或者說是位元組碼)。

  在準備階段,變數已經賦過一次系統要求的初始值,而在初始階段,則根據查詢員通過查詢制定的主觀計劃去初始化變數和其他資源,換而言之,初始化階段是執行類類構造器<client>()方法的過程。

  在<client>()方法中,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在他之後的變數,在前面的靜態語句塊可以賦值,但不能訪問。

六、類載入器

  類載入器是類載入過程中載入階段中“通過一個類的全限定名來獲取描述此類的二載入位元組流”的載入動作。

  主要分為啟動類載入器(C++語言實現,是虛擬機器自身的一部分)、擴充套件類載入器、應用程式類載入器,後面兩類載入器由Java語言實現,獨立於虛擬機器外部,並全部繼承自抽象類java.lang.Loader。

  其載入順序的實現為雙親委託派模型,如下圖所示:

640?wx_fmt=other

 原文地址:www.cnblogs.com

關注公眾號Java高階架構,分享更多Java知識。

640?wx_fmt=png
640?wx_fmt=png

◀◀◀ 長按二維碼關注我們