JVM內存模型分析

JVM會將Java進程所管理的內存劃分為若干不同的數據區域. 這些區域有各自的用途、創建/銷燬時間

JVM內存數據:棧管運行,堆管存儲

第一章 線程私有區域

線程私有數據區域生命週期與線程相同, 依賴用戶線程的啟動/結束而創建/銷燬(在Hotspot VM內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區域的存/否跟隨本地線程的生/死)

1.1 Native Method Stack本地方法棧

是在Native Method Stack中登記native方法,在Execution Engine執行時加載native libraies。

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

1.2 PC Register程序計數器

每個線程都有一個程序計算器,就是一個指針,指向方法區中的方法字節碼(下一個將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記。

作用是當前線程所執行字節碼的行號指示器(類似於傳統CPU模型中的PC), PC在每次指令執行後自增, 維護下一個將要執行指令的地址. 在JVM模型中, 字節碼解釋器就是通過改變PC值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴PC完成(僅限於Java方法, Native方法該計數器值為undefined).

不同於OS以進程為單位調度, JVM中的併發是通過線程切換並分配時間片執行來實現的. 在任何一個時刻, 一個處理器內核只會執行一條線程中的指令. 因此, 為了線程切換後能恢復到正確的執行位置, 每條線程都需要有一個獨立的程序計數器, 這類內存被稱為“線程私有”內存.

1.3 Java Stack(虛擬機棧)

棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致,是線程私有的。

基本類型的變量和對象的引用變量都是在函數的棧內存中分配。

棧幀中主要保存3類數據:

本地變量(Local Variables):輸入參數和輸出參數以及方法內的變量;

棧操作(Operand Stack):記錄出棧、入棧的操作;

棧幀數據(Frame Data):包括類文件、方法等等。

棧運行原理

棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀F1,並被壓入到棧中,A方法又調用了B方法,於是產生棧幀F2也被壓入棧,B方法又調用了C方法,於是產生棧幀F3也被壓入棧…… 依次執行完畢後,先彈出後進......F3棧幀,再彈出F2棧幀,再彈出F1棧幀。

遵循“先進後出”/“後進先出”原則。

第二章 線程共享區域

隨虛擬機的啟動/關閉而創建/銷燬

2.1 Method Area方法區

常說的永久代(Permanent Generation), 用於存儲被JVM加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分內存, 而不必為方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)。

1.7和1.8的異同

不過在1.7的HotSpot已經將原本放在永久代的字符串常量池移出

而在1.8中, 永久區已經被徹底移除, 取而代之的是元數據區Metaspace(這一點在查看GC日誌和使用jstat -gcutil查看GC情況時可以觀察到),與永久代不同, 如果不指定Metaspace大小, 如果方法區持續增長, VM會默認耗盡所有系統內存.

運行時常量池

方法區的一部分. Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項常量池(Constant Pool Table)用於存放編譯期生成的各種字面量和符號引用, 這部分內容會存放到方法區的運行時常量池中(如前面從test方法中讀到的signature信息). 但Java語言並不要求常量一定只能在編譯期產生, 即並非預置入Class文件中常量池的內容才能進入方法區運行時常量池, 運行期間也可能將新的常量放入池中, 如String的intern()方法.

2.2 Heap(Java堆)

堆這塊區域是JVM中最大的,應用的對象和數據都是存在這個區域,這塊區域也是線程共享的,也是 gc 主要的回收區,一個 JVM 實例只存在一個堆類存,堆內存的大小是可以調節的。類加載器讀取了類文件後,需要把類、方法、常變量放到堆內存中,以方便執行器執行,堆內存分為三部分:

新生區

新生區是類的誕生、成長、消亡的區域,一個類在這裡產生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Eden space)和倖存者區(Survivor pace),所有的類都是在伊甸區被new出來的。倖存區有兩個:0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園進行垃圾回收(Minor GC),將伊甸園中的剩餘對象移動到倖存0區。若倖存0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1去也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生Major GC(FullGCC),進行養老區的內存清理。若養老區執行Full GC 之後發現依然無法進行對象的保存,就會產生OOM異常“OutOfMemoryError”。

如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內存不夠。原因有二:

a.Java虛擬機的堆內存設置不夠,可以通過參數-Xms、-Xmx來調整。

b.代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)。

養老區

養老區用於保存從新生區篩選出來的 JAVA 對象,一般池對象都在這個區域活躍。

永久區

永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所佔用的內存。

如果出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機對永久代Perm內存設置不夠。原因有二:

a. 程序啟動需要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。

b. 大量動態反射生成的類不斷被加載,最終導致Perm區被佔滿。

說明:

Jdk1.6及之前:常量池分配在永久代 。

Jdk1.7:有,但已經逐步“去永久代” 。

Jdk1.8及之後:無(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出現在JDK1.8中)。

說明:方法區和堆內存的異議:

實際而言,方法區和堆一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯後的代碼等等,雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。

對於HotSpot虛擬機,很多開發者習慣將方法區稱之為“永久代(Parmanent Gen)”,但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而已,永久代是方法區的一個實現,jdk1.7的版本中,已經將原本放在永久代的字符串常量池移走。