新聞中心
一文詳解JVM內(nèi)存模型,從線程共享到本地方法棧再到Java堆
作者:老男孩的成長之路 2019-12-12 11:19:33
云計算
虛擬化 JVM 內(nèi)存模型與 JAVA 內(nèi)存模型不是同一個概念。JVM 內(nèi)存模型是從運行時數(shù)據(jù)區(qū)的結(jié)構(gòu)的角度描述的概念;而 JAVA 內(nèi)存模型是從主內(nèi)存和線程私有內(nèi)存角度的描述。

成都創(chuàng)新互聯(lián)專注于天橋企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站開發(fā),商城建設(shè)。天橋網(wǎng)站建設(shè)公司,為天橋等地區(qū)提供建站服務(wù)。全流程按需定制開發(fā),專業(yè)設(shè)計,全程項目跟蹤,成都創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)
前言
在正式學(xué)習(xí) JVM 內(nèi)存模型之前,先注意以下幾個是問題:
JVM 內(nèi)存模型與 JAVA 內(nèi)存模型不是同一個概念。JVM 內(nèi)存模型是從運行時數(shù)據(jù)區(qū)的結(jié)構(gòu)的角度描述的概念;而 JAVA 內(nèi)存模型是從主內(nèi)存和線程私有內(nèi)存角度的描述。從以下兩張圖可以看出:
? JAVA內(nèi)存模型
? JVM內(nèi)存模型
- Java虛擬機總共由三大模塊組成:類加載器子系統(tǒng)運行時數(shù)據(jù)區(qū)執(zhí)行引擎本篇我們介紹第二大模塊——運行時數(shù)據(jù)區(qū)(JVM內(nèi)存模型)。
- 其實虛擬機的這些模塊并不是獨立的,都是相互聯(lián)系的。java 文件編譯為 class 文件,通過類加載子系統(tǒng)加載,信息再到 JVM 托管的內(nèi)存中(部分操作會與本地內(nèi)存交互)的流轉(zhuǎn),再到垃圾回收等等,都是一系列的操作。
概覽
運行時數(shù)據(jù)區(qū)分為幾大模塊(如上圖所示):
線程共享區(qū):
- JAVA堆
- 方法區(qū)
線程私有區(qū):
- JAVA棧
- 本地方法棧
- 程序計數(shù)器
本文中,我們將從以下幾個方法面來分析各個區(qū)域:
- 功能
- 存儲的內(nèi)容
- 是否有內(nèi)存溢出和內(nèi)存泄露
- 是否進行垃圾回收
- 對應(yīng)的垃圾回收算法
- 垃圾回收流程
- 性能調(diào)優(yōu)
線程私有區(qū)
程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間,它的作用可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解釋器工作時通過該計數(shù)器的值來選擇選取下一條需要執(zhí)行的字節(jié)碼的指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)都需要依賴該區(qū)域。
通俗點講,該區(qū)域存放的就是一個指針,指向方法區(qū)的方法字節(jié)碼,用來存儲指向下一條指令的地址,也就是即將要執(zhí)行的指令代碼。
如果線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,這個計數(shù)器值則為空(Undefined)。
當執(zhí)行完一行指令碼,JVM執(zhí)行引擎會更新程序計數(shù)器的值。
由于Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間的計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。(方法的調(diào)用,方法中又調(diào)用另外一個方法,正式滿足棧的“先進先出,后進后出”的模型)。
OutOfMemoryError:無
虛擬機棧
它描述的是java方法執(zhí)行的內(nèi)存模型,其生命周期與線程相同。
每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(StackFrame),每一個棧幀又包括局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等。方法的調(diào)用,方法中又調(diào)用另外一個方法,正式滿足棧的“先進先出,后進后出”的模型。即每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。
以上都只是幾個很機械的概念,難以深入理解。下面我通過一個示例,來分析虛擬機棧的存儲內(nèi)容。
首先創(chuàng)建一個簡單的程序:
- package com.sunwin.robotcloud.test;
- /**
- * Created by 追夢1819 on 2019-11-01.
- */
- public class CalculateMain {
- public int calculate(){
- int a = 3;
- int b=4;
- int c = a+b;
- return c;
- }
- public static void main(String[] args) {
- CalculateMain main = new CalculateMain();
- int d = main.calculate();
- System.out.println(d);
- }
對于以上程序,線程啟動時,虛擬機會給主線程 main 分配一個大的內(nèi)存空間,然后給main方法分配一個棧幀,存放該方法的局部變量;
執(zhí)行calculate()方法時又分配一個calculate()的棧幀,存放對應(yīng)方法的局部變量。
要注意的是,一個方法分配一個單獨的內(nèi)存區(qū)域,即棧幀。
Java 屬于高級語言,難以直接通過代碼看出它的執(zhí)行過程。我們通過底層的字節(jié)碼,反解析出執(zhí)行的指令碼,來分析底層執(zhí)行過程。
進入 CalculateMain.class 文件目錄,執(zhí)行命令:
將指令碼直接輸出到文件 CalculateMain.txt:
- Compiled from "CalculateMain.java"
- public class com.sunwin.robotcloud.test.CalculateMain {
- public com.sunwin.robotcloud.test.CalculateMain();
- Code:
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."
":()V - 4: return
- public int calculate();
- Code:
- 0: iconst_3
- 1: istore_1
- 2: iconst_4
- 3: istore_2
- 4: iload_1
- 5: iload_2
- 6: iadd
- 7: istore_3
- 8: iload_3
- 9: ireturn
- public static void main(java.lang.String[]);
- Code:
- 0: new #2 // class com/sunwin/robotcloud/test/CalculateMain
- 3: dup
- 4: invokespecial #3 // Method "
":()V - 7: astore_1
- 8: aload_1
- 9: invokevirtual #4 // Method calculate:()I
- 12: istore_2
- 13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
- 16: iload_2
- 17: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
- 20: return
- }
先看看calculate()方法,根據(jù)以上指令,查詢JVM指令手冊,可以得到以上程序的執(zhí)行流程:
0.將int類型常量3壓入(操作數(shù))棧;
1.將int類型值3存入局部變量1(1是數(shù)組下標),也就是在局部變量表中給a分配一塊內(nèi)存(用以存儲3);
2.將int類型常量4壓入(操作數(shù))棧;
3.將int類型值4存入局部變量2;
4.從局部變量1中裝載int類型值,也就是將局部變量表的值3,拿出來加載到操作數(shù)棧;
5.從局部變量2中裝載int類型值;
6.兩值相加;
7.(將數(shù)存入到操作數(shù)棧?)將int類型值7存入局部變量3;
8.從局部變量3中裝載int類型值;
9.返回計算值。
以上是方法執(zhí)行時的局部變量在內(nèi)存中的流轉(zhuǎn)過程??偨Y(jié)就是:
操作數(shù)棧相當于數(shù)據(jù)在操作時的臨時中轉(zhuǎn)站
局部變量表:局部變量存放空間。是一個字長為單位、從0開始計數(shù)的數(shù)組。類型為int、float、reference、retrueAddress的值,只占據(jù)一項。類型為byte、short、char的值存入數(shù)組前都被轉(zhuǎn)化為int值。類型為long、double的值在其中占據(jù)連續(xù)的兩項。索引指向第一個值即可。
不過需要注意的是,虛擬機對byte、short、char是直接支持的,只不過在局部變量表和操作數(shù)棧中是被轉(zhuǎn)化為了int值,在堆和方法區(qū)中,依然是原來的類型。
操作數(shù)棧:數(shù)據(jù)操作的臨時空間。與局部變量表類似。唯一不同的是,它并非是通過索引來訪問的,而是通過壓棧和出棧來訪問的。
動態(tài)鏈接:存放的是方法的jvm指令碼的內(nèi)存地址,運行時動態(tài)生成的。
對象有對象頭,其中一個類型指針指向方法區(qū)的類元信息
方法出口:存放的是出該方法,進入下一個方法的程序計數(shù)器的值。
JAVA棧結(jié)構(gòu)
異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError 異常;如果虛擬機??梢詣討B(tài)擴展(當前大部分的Java 虛擬機都可動態(tài)擴展,只不過Java 虛擬機規(guī)范中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError 異常。
本地方法棧
本地方法棧其實與java虛擬機棧極其相似。唯一的區(qū)別就是java虛擬機棧是為java方法服務(wù),本地方法棧是為本地方法服務(wù),虛擬機規(guī)范中對本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定,因此具體的虛擬機可以自由實現(xiàn)它。
也會拋出StackOverflowError和OutOfMemoryError異常。
線程共享區(qū)
方法區(qū)
該區(qū)域是存儲虛擬機加載的類信息(字段方法的字節(jié)碼、部分方法的構(gòu)造器)、常量、靜態(tài)變量、編譯后的代碼信息等,類的所有字段和方法字節(jié)碼。以及一些特殊方法如構(gòu)造函數(shù),接口的代碼也在此定義。簡而言之,所有定義的方法的信息都保存在該區(qū)域。靜態(tài)變量+常量+類信息(構(gòu)造方法/接口定義)+運行時常量池都存在。
可不連續(xù),可固定大小,可擴展,也可不選擇垃圾回收器。垃圾回收存在在該區(qū)域,但是出現(xiàn)較少。
方法區(qū)是一種定義,概念,而永久代或者元空間是一種實現(xiàn)機制。
OutOfMemoryError:有
運行時常量池
Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
OutOfMemoryError:有
JAVA堆
堆是Java虛擬機所管理的內(nèi)存中最大的一塊,它唯一的功能就是存儲對象實例。幾乎所有的對象(包含常量池),都會在堆上分配內(nèi)存。
如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。
垃圾回收器的主要管理區(qū)域。
該區(qū)域,從垃圾回收的角度看,又分為新生代和老年代,新生代又分為 伊甸區(qū)(Eden space)和幸存者區(qū)(Survivor pace) ,Survivor 區(qū)又分為Survivor From 區(qū)和 Survivor To 區(qū)。如下圖所示:
以上區(qū)域的大小分配是:
新生代:堆的 1/3
老年代:堆的 2/3
Eden 區(qū): 新生代的 8/10
Survivor From 區(qū):新生代的 1/10
Survivor To區(qū):新生代的 1/10
如果是從內(nèi)存分配的角度來看,可以劃分多個線程私有的分配緩沖區(qū)。
對于堆空間來說,本質(zhì)都是存儲對象實例。不過如何分區(qū),都只是為了更好地分配和管理對象實例。關(guān)于堆空間對對象實例的管理和回收,在下一章節(jié)闡述。
同時,物理上可以不連續(xù),但是邏輯上必須是連續(xù)的。
以下是JVM內(nèi)存模型整體結(jié)構(gòu):
對象回收流程
下圖摘自網(wǎng)絡(luò):
所有的類都是在伊甸區(qū)被 new 出來的,等到 Eden 區(qū)滿的時候,會觸發(fā) Minor GC,將不需要再被其他對象引用的對象進行銷毀,將剩余的對象移動到 From Survivor 區(qū),每觸發(fā)一次 Minor GC,對象的分代年齡會+1(分代年齡是存放在對象頭里面的),F(xiàn)rom Survivor 區(qū)滿的時候, From Survivor 區(qū)觸發(fā) Minor GC,未被回收的對象,分代年齡會繼續(xù)+1,會移至 to survior 區(qū),此時Eden的未被回收的對象也是移至 To Survivor 區(qū),To Survivor 區(qū)滿的時候,被移至 From Survivor 區(qū),以此類推。
對象的分代年齡到15的時候,對象會進入到老年代(靜態(tài)變量(對象類型)、數(shù)據(jù)庫連接池等)。若老年代也滿了,這個時候會產(chǎn)生 Major GC(Full GC),進行老年區(qū)的內(nèi)存清理。若老年區(qū)執(zhí)行了 Full GC之后發(fā)現(xiàn)依然無法進行對象的保存,就會產(chǎn)生OOM 異常 OutOfMemoryError。
注意事項
- 運行時數(shù)據(jù)區(qū),版本不同,會有細微的差別,具體如下:元數(shù)據(jù)區(qū):元數(shù)據(jù)區(qū)取代了永久代(jdk1.8以前),本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn),區(qū)別在于元數(shù)據(jù)區(qū)并不在虛擬機中,而是使用本地物理內(nèi)存,永久代在虛擬機中,永久代邏輯結(jié)構(gòu)上屬于堆,但是物理上不屬于堆,堆大小=新生代+老年代。元數(shù)據(jù)區(qū)也有可能發(fā)生OutOfMemory異常;jdk1.6及以前:有永久代,常量池在方法區(qū);jdk1.7:有永久代,但已經(jīng)逐步“去永久代”,常量池在堆;jdk1.8及以后:無永久代,常量池在元空間(用的是計算機的直接內(nèi)存,而不是虛擬機管理的內(nèi)存)。
- 為什么jdk1.8用元數(shù)據(jù)區(qū)取代了永久代?官方解釋:移除永久代是為融合HotSpot JVM與JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代。(簡單說,就是兩者競爭,誰贏了就聽誰的。)
- 元數(shù)據(jù)區(qū)的動態(tài)擴展,默認–XX:MetaspaceSize值為21MB的高水位線。一旦觸及則Full GC將被觸發(fā)并卸載沒有用的類(類對應(yīng)的類加載器不再存活),然后高水位線將會重置。新的高水位線的值取決于GC后釋放的元空間。如果釋放的空間少,這個高水位線則上升。如果釋放空間過多,則高水位線下降。
文章題目:一文詳解JVM內(nèi)存模型,從線程共享到本地方法棧再到Java堆
標題URL:http://m.5511xx.com/article/cocsppo.html


咨詢
建站咨詢
