1. 程式人生 > >[JVM-1]Java內存模型

[JVM-1]Java內存模型

維護 啟動 將在 分享 垃圾收集器 堆外內存 java內存模型 選擇 技術分享

Java虛擬機(JVM)內部定義了程序在運行時需要使用到的內存區域

技術分享圖片

1、線程間共享的內存區域

(1)HEAP (堆)

大多數應用,堆都是Java虛擬機所管理的內存中最大的一塊,它在虛擬機啟動時創建,此內存唯一的目的就是存放對象實例。由於現在垃圾收集器采用的基本都是分代收集算法,所以堆還可以細分為新生代和老年代,再細致一點還有Eden區、From Survivior區、To Survivor區,這個後面都會講到的。

(2)METHOD AREA (方法區)

這塊區域用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,虛擬機規範是把這塊區域描述為堆的一個邏輯部分的,但實際它應該是要和堆區分開的。從上面提到的分代收集算法的角度看,HotSpot中,方法區≈永久代。不過JDK 7之後,我們使用的HotSpot應該就沒有永久代這個概念了,會采用Native Memory來實現方法區的規劃了。

(3)RUNTIME CONSTANT POOL (運行時常量池)

上面的圖中沒有畫出來,因為它是方法區的一部分。Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存放編譯期間生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中,另外翻譯出來的直接引用也會存儲在這個區域中。這個區域另外一個特點就是動態性,Java並不要求常量就一定要在編譯期間才能產生,運行期間也可以在這個區域放入新的內容,String.intern()方法就是這個特性的應用。

2、線程獨有的內存區域

(1)PROGRAM COUNTER REGISTER (程序計數器)

這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。Java方法這個計數器才有值,如果執行的是一個Native方法,那這個計數器是空的。

(2)JAVA STACK (虛擬機棧)

生命周期和線程相同。每個方法執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用直至執行完畢的過程,就對應著一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在256K~756K之間。

(3)NATIVE METHOD STACK (本地方法棧)

和虛擬機棧起的作用一樣,只不過本地方法棧為虛擬機使用到的Native方法服務。虛擬機規範並沒有對這個區域有什麽強制規定,因此我們使用的HotSpot虛擬機,就幹脆沒有這塊區域了,它和虛擬機棧是一起的。

3、直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致內存溢出問題。JDK1.4中新增加了NIO,引入了一種基於通道與緩沖區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數據。顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括RAM、SWAP區)大小以及處理器尋址空間的限制。

對象創建

Java是一門面向對象的語言,Java程序運行過程中無時無刻都有對象被創建出來。在語言層面上,創建對象(克隆、反序列化)就是一個new關鍵字而已,但是虛擬機層面上卻不是如此。看一下在虛擬機層面上創建對象的步驟:

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

2、類加載檢查通過後,虛擬機為新生對象分配內存。對象所需內存大小在類加載完成後便可以完全確定,為對象分配空間無非就是從Java堆中劃分出一塊確定大小的內存而已。這個地方會有兩個問題:

(1)如果內存是規整的,那麽虛擬機將采用的是指針碰撞法來為對象分配內存。意思是所有用過的內存在一邊,空閑的內存在另外一邊,中間放著一個指針作為分界點的指示器,分配內存就僅僅是把指針向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的,虛擬機采用這種分配方式。

(2)如果內存不是規整的,已使用的內存和未使用的內存相互交錯,那麽虛擬機將采用的是空閑列表法來為對象分配內存。意思是虛擬機維護了一個列表,記錄上哪些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。如果垃圾收集器選擇的是CMS這種基於標記-清除算法的,虛擬機采用這種分配方式。

另外一個問題及時保證new對象時候的線程安全性。因為可能出現虛擬機正在給對象A分配內存,指針還沒有來得及修改,對象B又同時使用了原來的指針來分配內存的情況。虛擬機采用了CAS配上失敗重試的方式保證更新更新操作的原子性和TLAB兩種方式來解決這個問題。

3、內存分配結束,虛擬機將分配到的內存空間都初始化為零值(不包括對象頭)。這一步保證了對象的實例字段在Java代碼中可以不用賦初始值就可以直接使用,程序能訪問到這些字段的數據類型所對應的零值。

4、對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象的對象頭中。

5、執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來。

以上這部分內容,如果有下載OpenJDK的源代碼的話,可以通過參考hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp文件,從1939行開始。1939行的代碼是CASE(_new):{...},意思是當代碼中遇見new這個關鍵字,虛擬機做的事情。實際虛擬機可能並不是執行的這段代碼,但是通過這段代碼來了解new對象的時候虛擬機的運作過程基本上是沒問題的。

對象定位方式

建立對象是為了使用對象,Java程序需要通過棧上的reference(引用)數據來操作堆上的具體對象。比如我們寫了一句

Object obj = new Object()

而new Object()之後其實有兩部分內容,一部分是類數據(比如代表類的Class對象)、一部分是實例數據

由於reference在Java虛擬機規範中只是一個指向對象new Object()的引用obj,並沒有規定obj應該通過何種方式去定位、訪問堆中對象的具體位置,所以對象訪問方式也是取決於虛擬機而定的。主流方式有兩種:

1、句柄訪問。Java堆中劃分出一塊句柄池,obj指向的是對象的句柄地址,句柄中則包含了類數據的地址和實例數據的地址

2、指針訪問。對象中存儲所有的實例數據和類數據的地址,obj指向的是這個對象

HotSpot虛擬機采用的是後者,不過前者的對象訪問方式也是十分常見的。

[JVM-1]Java內存模型