更多文章 通路我的部落格:http://www.caoyong.xin:8080/blogger
Java虛拟機記憶體區域詳解
半年前買了一本深入了解Java虛拟機,買了就放在那裡去了,期間拿出來想研究一下,還沒有看一會,哇 !腦袋疼
。也就又放回原處,這段時間事情不多,自己也靜下心來,看看這本被譽為佳作的書。
目錄結構
1:Java虛拟機介紹
2:記憶體區域介紹
2.1:程式計數器
2.2:Java虛拟機棧
2.3:Java堆
2.4:方法區
2.5:本地方法棧
3:對象的建立(轉載)
1:Java虛拟機介紹
學習Java的人,都聽說過Java虛拟機,也叫JVM,估計也就停留在這裡的,(我也差不多,剛開始)。Java語言的誕生在1995年(我出生)Java釋出了第一個版本Java1.0,這個時候Java喊出了一句口号
Write Once,Run AnyWhere 一次編寫,随處運作 而那個時候的Java虛拟機是 Sun Classic VM
。到2000年的時候Java1.3,就把HotSpot作為了一直沿用至今的Java虛拟機。是以我們現在用大部分Java虛拟機都是HotSpot
2:記憶體區域介紹
Java虛拟機在執行Java程式的時候會把記憶體分為幾個資料區域,看下面的圖,介紹了Java虛拟機的運作時資料區的劃分
下面我們就來一一介紹這些資料區域
2.1:程式計數器
程式計數器是一塊較小的記憶體區域,在java的位元組碼解析器當中,需要辨識目前的位元組碼解析到了哪個地方,同時需要來控制程式的流程,如果在程式當中沒有一個東西來記錄目前程式執行到哪個,同時下一步應該執行哪一步操作例如:分支、循環、跳轉、異常處理等操作都不是按照原本程式書寫的順序來執行的,是以為了能夠引導程式的運作,就需要引進一個用來引導位元組碼解析順序的東西,就叫做程式計數器。
Java虛拟機的多線程是通過線程輪流切換并配置設定處理器執行時間的方式來實作的。如果一個線程運作一半,就被挂起,等待另一個線程執行完畢後在接着執行。為了線程切換後可以正确的恢複到原來執行的位置,是以每個線程都應該有一個獨立的程式計數器,也就是說程式計數器這一塊記憶體區域是私有的。也就叫"線程私有"。
還有一點,如果線程正在執行的是一個java方法,那麼計數器記錄的是正在執行的虛拟機位元組碼指令位址。如果執行的native方法,計數器當中的内容應當是空。
還有此記憶體區域在java的虛拟機規範當中是唯一一個沒有規定OutOfMemoryError(記憶體溢出錯誤)的區域。
2.2:Java虛拟機棧
也叫Java棧,他也是線程私有的,Java虛拟機棧描述的是Java方法執行的記憶體模型。每個方法在執行的同時都會在Java虛拟機棧中建立一個棧幀,用于存儲局部變量表,操作數棧,動态連結,方法出口等資訊。每一個方發從調入到執行完畢,也就對應這棧幀進棧到出棧的過程。
局部變量表存放着編譯期可知的各種基本資料類型和對象的引用,我們通常說的Java棧存放對象引用,Java堆存放對象執行個體,現在應該明白了具體存放哪裡了。
Java虛拟機棧有兩種異常狀況
第一種 線程請求的棧深度大于最大可用深度,則抛出stackOverflowError;
第二種棧是可動态擴充的,但沒有記憶體空間支援擴充,則抛出OutofMemoryError。
2.3:Java堆
Java堆是Java虛拟機所管理的最大的一塊記憶體區域,Java堆是線程共享的一塊區域,當然Java堆也是垃圾收集器管理的主要區域,可以分為新生代和老年代(tenured)。新生代用于存放剛建立的對象以及年輕的對象,如果對象一直沒有被回收,生存得足夠長,老年對象就會被移入老年代。新生代又可進一步細分為eden、survivorSpace0(s0,from
space)、survivorSpace1(s1,to
space)。剛建立的對象都放入eden,s0和s1都至少經過一次GC并幸存。如果幸存對象經過一定時間仍存在,則進入老年代(tenured)。
2.4:方法區
方法區也是各個線程共享的記憶體區域,它 用于存儲已被虛拟機加載的類資訊,常量,靜态變量,及時編譯器編譯後的代碼資料。
方法區中有三個池(jdk1.6之前),
- 常量池(Constant Pool):常量池資料編譯期被确定,是Class檔案中的一部分。存儲了類、方法、接口等中的常量,當然也包括字元串常量。
- 字元串池/字元串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中産生的字元串類型資料。
- 運作時常量池(Runtime Constant Pool):方法區的一部分,所有線程共享。虛拟機加載Class後把常量池中的資料放入到運作時常量池。
- 常量池:可以了解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他項目資源關聯最多的資料類型。
- 常量池中主要存放兩大類常量:字面量(Literal)和符号引用(Symbolic Reference)。
- 字面量:文本字元串、聲明為final的常量值等;
- 符号引用:類和接口的完全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符
JDK1.6之前字元串常量池位于方法區之中。
JDK1.7字元串常量池已經被挪到堆之中。
2.5:本地方法棧
和虛拟機棧功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C實作的。Java底層會調用C編寫的的類庫中的方法,在Java中調用本地方法使用native關鍵詞。而本地方法棧就是管理這些本地方法的。
下面這一部分是對象在Java虛拟機建立的一系列過程,看到有位部落客寫了篇關于這部分的内容,是以就轉載一下。寫的很具體
轉載:
https://blog.csdn.net/sc313121000/article/details/50819741一、對象的建立
new Animal();
1.類加載檢查:
檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類的加載過程。
2.為對象配置設定記憶體
對象所需記憶體的大小在類加載完成後便完全确定,為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。
2.1根據Java堆中是否規整有兩種記憶體的配置設定方式:
(Java堆是否規整由所采用的垃圾收集器是否帶有壓縮整理功能決定)
指針碰撞(Bump the pointer):
Java堆中的記憶體是規整的,所有用過的記憶體都放在一邊,空閑的記憶體放在另一邊,中間放着一個指針作為分界點的訓示器,配置設定記憶體也就是把指針向空閑空間那邊移動一段與記憶體大小相等的距離。例如:Serial、ParNew等收集器。
空閑清單(Free List):
Java堆中的記憶體不是規整的,已使用的記憶體和空閑的記憶體互相交錯,就沒有辦法簡單的進行指針碰撞了。虛拟機必須維護一張清單,記錄哪些記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄。例如:CMS這種基于Mark-Sweep算法的收集器。
2.2配置設定記憶體時解決并發問題的兩種方案:
對象建立在虛拟機中時非常頻繁的行為,即使是僅僅修改一個指針指向的位置,在并發情況下也并不是線程安全的,可能出現正在給對象A配置設定記憶體,指針還沒來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。
對配置設定記憶體空間的動作進行同步處理—實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性;
把記憶體配置設定的動作按照線程劃分為在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體,稱為本地線程配置設定緩沖(TLAB)。哪個線程要配置設定記憶體,就在哪個線程的TLAB上配置設定。隻有TLAB用完并配置設定新的TLAB時,才需要同步鎖定。
3.記憶體空間初始化
虛拟機将配置設定到的記憶體空間都初始化為零值(不包括對象頭),如果使用了TLAB,這一工作過程也可以提前至TLAB配置設定時進行。
記憶體空間初始化保證了對象的執行個體字段在Java代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型所對應的零值。
4.對象設定
虛拟機對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼、對象的GC分代年齡等資訊。這些資訊存放在對象的對象頭之中。
5.init
在上面的工作都完成之後,從虛拟機的角度看,一個新的對象已經産生了。
但是從Java程式的角度看,對象的建立才剛剛開始方法還沒有執行,所有的字段都還是零。
是以,一般來說(由位元組碼中是否跟随invokespecial指令所決定),執行new指令之後會接着執行init方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算産生出來。
二、對象的記憶體布局
在HotSpot虛拟機中,對象在記憶體中存儲的布局可以分為3塊區域:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。
1.對象頭:
HotSpot虛拟機的對象頭包括兩部分資訊。
1.1 第一部分用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等。
HotSpot虛拟機對象頭Mark Word
1.2 另外一個部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。
如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通Java對象的中繼資料資訊确定Java對象的大小,但是從數組的中繼資料中無法确定數組的大小。
(并不是所有的虛拟機實作都必須在對象資料上保留類型指針,換句話說,查找對象的中繼資料并不一定要經過對象本身,可參考 三對象的通路定位)
2.執行個體資料:
執行個體資料部分是對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類型的字段内容。無論是從父類
中繼承下來的,還是在子類中定義的,都需要記錄下來。
HotSpot虛拟機預設的配置設定政策為longs/doubles、ints、shorts/chars、bytes/booleans、oop,從配置設定政策中可以看出,相同寬度的字段總是配置設定到一起。
3.對齊填充:
對齊填充并不是必然存在的,也沒有特定的含義,僅僅起着占位符的作用。
由于HotSpot虛拟機的自動記憶體管理系統要求對象的起始位址必須是8位元組的整數倍,也就是對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數(1倍或者2倍),是以,當對象執行個體資料部分沒有對齊的時候,就需要通過對齊填充來補全。
三、對象的通路定位
建立對象是為了使用對象,我們的Java程式需要通過棧上的引用資料來操作堆上的具體對象。
對象的通路方式取決于虛拟機實作,目前主流的通路方式有使用句柄和直接指針兩種。
使用句柄:
如果使用句柄的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,引用中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊。
通過句柄通路對象
優勢:引用中存儲的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而引用本身不需要修改。
直接指針:
如果使用直接指針通路,那麼Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而引用中存儲的直接就是對象位址。
通過直接指針通路對象
優勢:速度更快,節省了一次指針定位的時間開銷。由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是非常可觀的執行成本。(例如HotSpot)