Jvm運行時數據區


一:運行時數據區

  Java虛擬機在執行Java程序的過程中會把它管理的內存分為若干個不同的數據區域。這些區域有着各自的用途,一級創建和銷毀的時間,有的區域隨着虛擬機進程的啟動而存在,有些區域則依賴用戶線程的啟動和結束而建立和銷毀。根據《Java虛擬機規范》中規定,jvm所管理的內存大致包括以下幾個運行時數據區域,如圖所示:

 

 

 

圖解:

其中置灰部分是跟隨虛擬機啟動而存在的,線程共享

白色區域則是跟隨線程啟動而存在,線程私有

 

 

 

 

 

下面進行單獨講解這幾塊區域:

1.程序計數器

  占據一塊較小的內存空間,可以看做當前線程所執行的字節碼的行號指示器。在虛擬機概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能都需要依賴這個計數器來完成。

  由於jvm的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此未來線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們成這類內存區域為“線程私有”的內存。

  如果線程正在執行的是一個Java方法,這個計數器記錄的則是正在執行的虛擬機字節碼指令的地址;

  如果正在執行的是Native方法,這個計數器則為空(undefined)。

此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。

 2.Java虛擬機棧

  線程私有,生命周期和線程相同,虛擬機棧描述的是Java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀  用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法從調用直至完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

  局部變量表存放了編譯期可知的各種基本類型數據(boolean、byte、char、short、int、float、long、double)、對象引用、returnAddress類型(指向了一條字節碼指令的地址)。

  其中64位長度的long和double類型的數據會占用2個局部變量表空間(slot),其余的數據類型只占用1個。局部變量表所需的內存空間在編譯期完成分配,當進入一個方法時,這個方法所需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

  在Java虛擬機規范中,對此區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將會拋出Stack OverflowError異常;如果虛擬機棧可以動態擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

3.本地方法棧

  本地方法棧與虛擬機棧所發揮的作用非常相似,他們之間的區別不過是虛擬機棧為虛擬機執行Java方法(字節碼)服務,而本地方法棧則為虛擬機中使用到的native方法服務。在虛擬機規范中對本地方法棧中方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機直接把本地方法棧和虛擬機棧合二為一,與虛擬機棧一樣也會拋出Stack OverflowError異常和OutOfMemoryError異常。

4.Java堆

   對於大多數應用來說,堆空間是jvm內存中最大的一塊。Java堆是被所有線程共享,虛擬機啟動時創建,此內存區域唯一的目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。這一點在Java虛擬機規范中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配,標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也就變得不那么絕對了。

  Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。從內存回收角度看,由於現在收集器基本都采用分代收集算法,所以Java堆還可以細分為:新生代和老年代;再細致一點的有Eden空間,From Survivor空間,To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能划分出多個線程私有的分配緩沖區。不過無論如何划分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例,進一步划分的目的是為了更好的回收內存,或者更快的分配內存。(如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。)

 

5.方法區(也有人叫永久代)

   和堆一樣所有線程共享,主要用於存儲已被jvm加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。

  (在JDK1.7發布的HotSpot中,已經把字符串常量池移除方法區了。)

6.常量池:

  運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。

  Java虛擬機對class文件每一部分的格式都有嚴格規定,每一個字節用於存儲哪種數據都必須符合規范才會被jvm認可。但對於運行時常量池,Java虛擬機規范沒做任何細節要求。

  運行時常量池有個重要特性是動態性,Java語言不要求常量一定只在編譯期才能產生,也就是並非預置入class文件中常量池的內容才能進入方法區的運行時常量池,運行期間也有可能將新的常量放入池中,這種特性使用最多的是String類的intern()方法。

  既然運行時常量池是方法區的一部分,自然受到方法區內存的限制。當常量池無法再申請到內存時會拋出outOfMemeryError異常。

 

 

2018.10.20 修改:

以上信息主要講述的就是JVM運行時數據區的內存划分情況,但是你有沒有想過他們是如何創建的?如何布局的?如何訪問的?現在我們就來帶着這些問題往下繼續深入。

 

二: 對象的創建

2.1 當虛擬機遇到一條New指令時:會進行如下步驟

  1. 檢查指令的參數(即工作中我們New的對象),能否在常量池中找到它的符號引用。
  2. 如果存在,檢查符號引用代表的類是否被加載、解析、初始化過。(如果沒有則執行類的加載-----相關加載過程參考《Jvm類的加載機制》)。
  3. 加載通過后,虛擬機將為新生對象分配內存。(所需內存大小在類加載完成后便可確定)

2.2 兩種內存分配的方式:

  指針碰撞:假設Java堆中的內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊。中間放着一個指針作為分界點的指示器,分配內存就僅僅是把指針往空閑空間那邊挪動一段與對象大小相等的距離。這種方式則屬於指針碰撞

  空閑列表:如果堆中的內存並不是規整的,已使用的內存和空閑內存相互交錯,顯然無法使用指針碰撞。虛擬機就必須維護一個列表,記錄哪些內存是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新記錄表上的數據。這種方式屬於空閑列表

具體選擇哪種分配方式由Java堆決定,而Java堆是否規整,則有GC收集器決定。因此使用Serial、ParNew等帶Compact過程的收集器時,系統采用的分配算法是指針碰撞。而使用CMS這種基於Mark-Sweep算法的收集器時,通常采用的空閑列表。

2.3如何保證分配內存時線程的安全性?

  1. 對分配內存的動作進行同步處理(實際上虛擬機采用CAS配上失敗重試的機制保證了更新操作的原子性)
  2. 把分配內存的動作按照線程划分在不同的空間之中進行(即每個線程在Java堆中預先分配一小塊內存(稱為本地線程分配緩沖))。

三:對象的內存布局 

 在HotSpot虛擬機中對象的內存布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)

對象頭包括兩部分信息:

  1. 存儲對象自身的運行時數據(如:哈希碼、GC分代年齡、鎖 等)
  2. 類型指針(即對象指向他的類元數據的指針,虛擬機根據此指針來確認對象屬於哪個類的實例)

實例數據:

  實例數據才是對象真正存貯的有效信息(即程序中所定義的各種類型的字段內容)。

對齊填充:

  不是必然存在的,僅僅起到占位符的作用。

四:對象的訪問定位

  創建對象就是為了在程序中使用,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。

對象的訪問方式:

句柄訪問:Java堆中划分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,句柄中包含了對象實例數據與類型數據各自的具體地址信息。

優點:reference中存儲句柄地址是穩定的。在對象被移動時只會改變句柄中的實例數據指針,而reference本身不需要修改。

句柄訪問圖示:

指針訪問:reference中存儲的直接就是對象地址。

優點:速度快,節省了指針定位的時間成本。

指針訪問圖示:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM