JVM之內存區域分配

   在上一篇文章中,我們講了Java代碼在JVM中的各種邏輯關係,那麼本文就具體的講述JVM各個組成部分。

JVM的內存區域劃分

Java程序運行時數據區

  Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域,這些區域都有各自的用途,以及創建和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,而有些區域則依賴用戶線程的啓動和結束而建立和銷燬。JVM所管理的內存會包含以下幾個內存區域:


程序計數器

    程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,比如分支、循環、跳轉、異常處理、線程恢復等基礎的功能都需要依賴這個計數器來完成。
    由於Java虛擬機的多線程是通過輪流切換處理器的執行時間的方式來實現的,在任何一個確定的時刻,一個處理器只能執行一個線程中的指令,因此,爲了使線程在切換後能夠恢復到正確的位置,每一個程序計數器必須是獨立的,也就是說程序計數器是每個線程所私有的,稱之爲此內存區域爲線程私有的內存

虛擬機棧(線程棧)

    與程序計時器一樣,虛擬機棧也是線程私有的,它的生命週期與線程相同,虛擬機中描述的是Java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(Stack Frame),用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直到執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧道出棧的過程。其中局部變量表中存放了編譯器可知的各種基本數據類型、對象引用類型(不是對象本身)。
    在Java虛擬機規範中,對這個虛擬機棧區域規定了兩種異常狀況:
  (1)如果線程請求的棧深度大於虛擬機所允許的深度將會拋出StackOverflowError錯誤。
  (2)如果虛擬機棧可以動態拓展,如果拓展時爲無法申請到足夠的內存就會拋出OutMemoryError錯誤。

本地方法棧

     本地方法棧與虛擬機棧所發揮的作用是相似的,他們之間的區別只不過是虛擬機棧爲虛擬機執行Java方法,而本地方法棧是執行虛擬機所使用到的Native方法服務,與虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和OutMemoryError錯誤。

Java堆

    對於大多數應用該程序來說,Java堆屎Java虛擬機所管理的內存中最大的一塊,Java堆屎被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目地就是存放對象實例,幾乎所有的對象實例都在這裏分配內存,JVM中規範說明了所有的對象實例以及數組都要在堆上分配。
    Java堆也是垃圾收集器所管理的主要區域,因此很多時候也被稱爲GC堆。Java堆可以處於物理上不連續的內存空間,只要邏輯上是連續的即可。

方法區

    方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯後的代碼等數據。

運行時常量池(Runtime Constant Pool)

    運行時常量池是方法區的一部分,Class文件中除了有淚的版本、字段、方法、藉口等描述信息外,還有一項信息就是常量池,用於存放編譯器生成的各種字面量和符號引用。Java虛擬機對Class文件的每一部分的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上都要求才會被虛擬機認可、加載和執行。
    運行時常量池對於Class文件常量池的另外一個特徵就是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入Class文件中的常量池的內容才能進入方法去運行時常量池,運行期間也可以將新的常量放入池中,這種特性體現最多的就是String的intern()方法。

對象的創建

    Java是一門面向對象的編程語言,在Java程序運行過程中無時無刻都有對象被創建出來。在語言層面上,創建對象(如克隆、反序列化)通常僅僅是new一個關鍵字而已,但是在虛擬機中,對象的創建確實很複雜的。
    當虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過,如果沒有,那必須先執行相應的類加載過程。
    在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。內存分配完成以後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象實例字段在Java代碼中可以不賦初始值就可以直接使用,程序能訪問到這些字段的數據類型所對應的零值。
    接下來虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據、對象的哈希碼、對象的GC分代年齡等信息,這些信息都是包含對象的對象頭重,根據虛擬機當前的運行狀態的不同,如是否啓用偏向鎖等。
    以上所有工作完成後一個新的對象就已經產生了,但是從Java程序的角度來看,對象的創建纔剛開始,因爲其init方法還未執行,所有的字段都還是零,所以,一般來說,執行new指令之後會接着執行init方法,把對象按照程序員的意願進行初始化,這樣一個對象纔算是完全創建成功。
   小結一下:當類加載通過後,會按照以下的步驟來創建一個新的對象:
  (1)對象的數據進行初始化爲零的操作(僅將對象的數據部分初始化,不包含對象頭)。
  (2)對象頭的設置。
  (3)對象數據的初始化,這個初始化是按照程序員給定的值進行初始化。

對象的內存佈局

    在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭、實例數據和對齊填充(這個只是啓動佔位符的作用)。

對象頭

    HotSpot虛擬機的對象頭包括兩部分信息:
   (1)第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機中分別爲32bit和64bit.
   (2)另一部分就是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針。如果對象是一個Java數組,那在對象頭重還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數據的元數據中卻無法確定數組的大小。

實例數據

    實例數據部分是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛擬機分配策略的參數和字段在Java源代碼定義的順序的影響。HotSpot虛擬機默認的分配策略是longs/doubles、ints、shorts/chars、bytes/booleans、oops,從分配策略可以看出,相同長度的字段總是被分配到一起,在滿足這個條件的前提下,在父類中定義的變量會出現在子類之前,如果CompactFoelds參數值爲true,那麼子類中較窄的變量也可能會被插入到父類變量的空隙之中。

對象的訪問定位

    建立對象是爲了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。由於reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中對象的具體位置,所以對象的訪問取決於不同虛擬機的實現。
  (1)使用句柄訪問
   如果使用句柄訪問的話,那麼Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

  (2)使用直接指針訪問
    如果使用直接指針的訪問,那麼Java堆對象的佈局中就必須考慮如何防止訪問類型數據的相關信息,而reference中存儲的直接的就是對象地址。

   這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移到(垃圾收集時對象經常會被移動)只會改變句柄的實例數據指針,而reference本身並不需要修改。使用直接指針訪問方式的最大好處就是速度快,它節省了一次指針定位的時間開銷。

    總結

      本文主要介紹了JVM的內存劃分情況以及各部分存儲什麼樣的數據,以及對象在堆內存的分佈情況、對象的構成,以及對象的訪問和定位。我們需要重點注意的是Java對象的創建和初始化過程以及其在內存的分佈情況,通過本文我們需要搞清楚一個問題:對象到底是什麼?是的,對象的頭部分主要是用於程序的各種邏輯實現,主要由JVM使用,我們平時使用到的對象就是指對象的實例數據部分,那麼對象的實例數據部分到底有哪些數據?主要的,對象包含了Class的成員屬性(全局變量)和類的行爲(Method)等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章