您好,登錄后才能下訂單哦!
本篇文章為大家展示了怎么用最通俗的方法講解JVM內(nèi)存模型,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
備注:本文講的基于JDK1.8,且1.8之前和之后差距略大,本文對1.8之前的版本只會略微介紹.
JVM說白了,就是個程序,而這個程序運(yùn)行起來后,就是臺計算機(jī),而且和我們平時使用的計算機(jī)非常相似,他就是一臺虛擬計算機(jī). 那什么是JVM內(nèi)存模型?就是幾個大神寫了一個在計算機(jī)上運(yùn)行的虛擬計算機(jī)的內(nèi)存模型. 那計算機(jī)的內(nèi)存模型是什么樣的?
各部分功能,相信不從事該行業(yè)的人都有相當(dāng)一部分知道他的大概作用,但我們還是粗略解釋一下 名稱|速度|介紹 --|--|-- 寄存器|速度特別快|暫存指令等短小精干的數(shù)據(jù). 棧|速度塊|空間連續(xù). 堆|速度慢|空間不連續(xù),但比硬盤可快多了. 硬盤|速度最慢|就是個倉庫.
那么!本篇文章就會在此圖中進(jìn)行講解 下面多圖慎入!
接下來,我們在這個電腦上添加一個虛擬機(jī) 既然我們說虛擬機(jī)和計算機(jī)是一樣的,那我們就把上述的堆棧等一堆東西都建一個放進(jìn)電腦里. 那么放到哪呢?
寄存器放不了.
棧太小.
堆可以,空間不連續(xù)我們可以自己搞.
硬盤是一個物理存儲,也不行.
由上得出,放到堆里,于是有了下面的樣子.
這個圖也很好懂,就是把寄存器,堆,棧,硬盤都放到操作系統(tǒng)的堆中了. OK,我們把虛擬機(jī)放進(jìn)來了,那么接下來呢?好像沒什么頭緒. 既然虛擬機(jī)有了,那我們把它運(yùn)行起來吧. 現(xiàn)在有兩個問題
它是怎么運(yùn)行的? JVM就是個C語言程序
這個程序的功能是什么? 運(yùn)行的是.class文件.
簡單的說,這個程序在運(yùn)行的時候,會啟動一個功能,叫類加載器,這個類加載器加載.class文件后,會把文件中的不同內(nèi)容,放入到堆棧這些不同的區(qū)域中. 那么這些區(qū)域都分別放了寫什么呢? 區(qū)域名稱|存儲內(nèi)容|特點(diǎn) :-|:-|:- 寄存器|代碼運(yùn)行到了哪一行(行話:當(dāng)前線程正在執(zhí)行的字節(jié)碼的行號指示器)|空間小,不會溢出,隨線程生滅 本地方法棧|JVM執(zhí)行的native方法|HotSpot虛擬機(jī)不區(qū)分虛擬機(jī)棧和本地方法棧,兩者是一塊的 棧|1.局部變量 2.操作棧 3,動態(tài)鏈接 4.返回地址|先進(jìn)后出,桶式結(jié)構(gòu) 堆|1.實(shí)例對象 2.數(shù)組 3.字符串常量池 4.靜態(tài)常量|垃圾回收器會回收沒被引用的對象和數(shù)組 元數(shù)據(jù)區(qū)(1.8前叫方法區(qū))|1.類信息 2.編譯后的代碼 3.運(yùn)行時常量池|1.7前叫方法區(qū),在堆中稱為非堆,1.7后放入了本地內(nèi)存,叫元數(shù)據(jù)區(qū) 接下來我一個個詳細(xì)解釋一下
這個知識點(diǎn)比較簡單,==本地方法棧服務(wù)的對象是JVM執(zhí)行的native方法== 總之,線程開始調(diào)用本地方法時,不受JVM約束.太多的nativa方法會影響虛擬機(jī)的可移植性.
為什么把堆放在棧前講,是因?yàn)檫@部分比較重要,而且是基礎(chǔ)部分.
堆中的內(nèi)容是線程共有的,所有線程訪問堆是同一個區(qū)域.
堆中存放的數(shù)據(jù)是對象實(shí)例和數(shù)組 例如:
User user = new User();//User是系統(tǒng)中常見的Model類 ↑ └─ new 出來的這個東西,就在堆中,controller同理 ↓ UserController uc = new UserController();//mvc模式下常見的類
堆最大,里面的東西也最多.里面的東西越放越多,但內(nèi)存就那么大,總有放滿的一天,于是,堆中沒用的東西就要被回收. 于是這群大神將堆分了幾個區(qū),分別為:
字符串常量池 : 其實(shí)是C++寫的一個hash表,所有的字符串都保存在常量池中. 在http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/9732f3600a48/src/share/vm/classfile/symbolTable.hpp定義 老年代 : 比例約為 2 新生代 : 比例約為 1 其中新生代又分為: Eden區(qū) : 占新生代的 8/10 Suivivor 0 區(qū) : 占新生代的 1/10 Suivivor 1 區(qū) : 占新生代的 1/10 //當(dāng)然大小和比例可以通過命令來修改
如圖:
再換一張官方的圖
這張圖可以使用JDK自帶的 : jdk/bin/jvisualvm.exe. 打開后選擇 - 工具 - 插件 - 可用插件 - 安裝VisualGC - 重啟軟件 - 左側(cè)選擇JVM進(jìn)程 - 右邊就會顯示Visual GC
那么JVM對各個區(qū)域是如何使用的呢?
絕大部分對象生成時都在Eden區(qū),當(dāng)Eden區(qū)裝填滿的時候,會觸發(fā)Young GC。
Young GC的時候,在Eden區(qū)執(zhí)行清除,沒有被引用的對象直接回收,依然存活的對象會被移送到Survivor區(qū).
Survivor 區(qū)分為S0和S1兩塊內(nèi)存空間,送到哪塊空間呢?
每次Young GC的時候,將存活的對象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清除,交換兩塊空間的使用狀態(tài).
如果Young GC要移送的對象大于Survivor區(qū)容量上限,則直接移交給老年代.
那會不會有頑強(qiáng)對象一直留在Surivivor區(qū)呢? 答案是不會的,每個對象都有一個計數(shù)器,每次YGC都會加1.計數(shù)器默認(rèn)為15,如果某個對象在Survivor 區(qū)交換14次之后,則晉升至老年代.
對象在堆的生命周期如下:
至于虛擬機(jī)如何將對象標(biāo)記為未被引用,可以查看 : GC算法.
為什么計算機(jī)學(xué)科中將這塊區(qū)域叫為堆(heap),而不是其他任何名詞呢? 其實(shí)是因?yàn)檫@里的數(shù)據(jù)是不連續(xù)的,也就是分配內(nèi)存地址是這里一個,那里一個. 如圖:
堆的內(nèi)存是不整齊的,是亂的.是非連續(xù)的,就是一堆雜亂的東西,所以稱之為堆.
棧中存放的是什么? 棧中其實(shí)就是和當(dāng)前執(zhí)行方法相關(guān)的數(shù)據(jù). 棧首先有個首要的特點(diǎn),他是桶狀的,是一個先入后出(FILO)的數(shù)據(jù)結(jié)構(gòu).如圖:
但棧是線程私有的,而我們的系統(tǒng)通常不只有一個線程,所以棧實(shí)際中應(yīng)當(dāng)是這樣的. 如圖:
那圖中這些都是什么呢?我們來結(jié)合圖來說:
空棧 : 首先棧中原本是空的
創(chuàng)建?! ? 在某個線程創(chuàng)建時,虛擬機(jī)會為線程創(chuàng)建一個該線程私有的棧.
創(chuàng)建棧幀 : 線程開始執(zhí)行到第一個方法時,就會在棧中創(chuàng)建一個棧幀,而最新創(chuàng)建的棧幀稱為當(dāng)前棧幀
棧幀中存儲的是該方法的一系列信息,包括如下:
1. 局部變量表 用于存放方法參數(shù)和方法內(nèi)部定義的局部變量 局部變量表的容量以變量槽 [Slot] 為最小單位。 在編譯期由Code屬性中的 [max_locals] 確定局部變量表的大小. 2. 操作數(shù)棧 可以理解成在哪里執(zhí)行當(dāng)前的這一行代碼. 3. 動態(tài)鏈接 在運(yùn)行時將類常量池中的符號引用轉(zhuǎn)換為直接引用. 簡單來說,就是我們的類在編譯好后,并不知道其中的代碼所調(diào)用的方法的地址是什么. 只有在執(zhí)行到該方法時,才知道調(diào)用的具體是哪個實(shí)例的方法. 4. 方法返回地址 其實(shí)就是標(biāo)記一個退出的指令,或是遇到異常.則返回到上層棧幀. 下面是術(shù)語,可以加深理解 當(dāng)一個方法開始執(zhí)行后,只有兩種方式可以退出,一種是遇到方法返回的字節(jié)碼指令;一種是遇見異常,并且這個異常沒有在方法體內(nèi)得到處理。 無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息,用來 幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者的PC計數(shù)器的值可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。 方法退出的過程實(shí)際上就等同于把當(dāng)前棧幀出棧,因此退出時可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。
Java在執(zhí)行時,,就是將各種指令往棧中寫入和提取
查看一段代碼的字節(jié)碼可以更好的理解JVM是如何對操作數(shù)棧和局部變量表進(jìn)行操作的.
package com.jasmine.Java高級.JVM.字節(jié)碼; public class TestJVMStack { public static int a = 123; public int simpleMethod(){ int x = 13; int y = 14; int z = x + y; return z; } public static void main(String[] args) { TestJVMStack s = new TestJVMStack(); System.out.println(s.simpleMethod()); } }
上述代碼的字節(jié)碼為:(略長,不想了解可直接到下面看對于操作棧的操作.)
Classfile /E:/WorkSpace/Idea/MyJava/target/classes/com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack.class Last modified 2019-8-27; size 854 bytes MD5 checksum 15fab830f998782e5087b8626274d45c Compiled from "TestJVMStack.java" public class com.jasmine.Java高級.JVM.字節(jié)碼.TestJVMStack minor version: 0 major version: 52 /* 類的訪問標(biāo)識 ACC_PUBLIC:代表public ACC_SUPER :用于兼容早期的編譯器,新編譯器都設(shè)置該標(biāo)記. */ flags: ACC_PUBLIC, ACC_SUPER // 類常量池,也叫 Class常量池 // 第一列為常量類型 // 第二列表示引用的常量或者utf8類型常量值 // 如#1的類型是class,引用的是#2的值 Constant pool: #1 = Class #2 // com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack #2 = Utf8 com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <clinit> //代表是類初始化階段 #8 = Utf8 ()V #9 = Utf8 Code #10 = Fieldref #1.#11 // com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack.a:I #11 = NameAndType #5:#6 // a:I #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 <init> // 代表是實(shí)例初始化階段,說白了就是構(gòu)造方法 #15 = Methodref #3.#16 // java/lang/Object."<init>":()V #16 = NameAndType #14:#8 // "<init>":()V #17 = Utf8 this #18 = Utf8 Lcom/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack; #19 = Utf8 simpleMethod #20 = Utf8 ()I #21 = Utf8 x #22 = Utf8 y #23 = Utf8 z #24 = Utf8 main #25 = Utf8 ([Ljava/lang/String;)V #26 = Methodref #1.#16 // com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack."<init>":()V #27 = Fieldref #28.#30 // java/lang/System.out:Ljava/io/PrintStream; #28 = Class #29 // java/lang/System #29 = Utf8 java/lang/System #30 = NameAndType #31:#32 // out:Ljava/io/PrintStream; #31 = Utf8 out #32 = Utf8 Ljava/io/PrintStream; #33 = Methodref #1.#34 // com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack.simpleMethod:()I #34 = NameAndType #19:#20 // simpleMethod:()I #35 = Methodref #36.#38 // java/io/PrintStream.println:(I)V #36 = Class #37 // java/io/PrintStream #37 = Utf8 java/io/PrintStream #38 = NameAndType #39:#40 // println:(I)V #39 = Utf8 println #40 = Utf8 (I)V #41 = Utf8 args #42 = Utf8 [Ljava/lang/String; #43 = Utf8 s #44 = Utf8 SourceFile #45 = Utf8 TestJVMStack.java { // 代表有一個靜態(tài)變量a,修飾是public static public static int a; descriptor: I flags: ACC_PUBLIC, ACC_STATIC static {}; descriptor: ()V flags: ACC_STATIC Code: // stack : 最大操作數(shù)棧,JVM運(yùn)行時會根據(jù)這個值來分配棧幀(Frame)中的操作棧深度,此處為1 // locals : 局部變量所需的存儲空間,單位為Slot,Slot是虛擬機(jī)為局部變量分配內(nèi)存時所使用的最小單位,為4個字節(jié)大小. // args_size : 方法參數(shù)的個數(shù),這里是0 stack=1, locals=0, args_size=0 0: bipush 123 2: putstatic #10 // Field a:I 5: return // LineNumberTable 該屬性的作用是描述源碼行號與字節(jié)碼行號(字節(jié)碼偏移量)之間的對應(yīng)關(guān)系。 LineNumberTable: line 60: 0 LocalVariableTable: Start Length Slot Name Signature public com.jasmine.Java高級.JVM.字節(jié)碼.TestJVMStack(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #15 // Method java/lang/Object."<init>":()V 4: return // LineNumberTable 該屬性的作用是描述源碼行號與字節(jié)碼行號(字節(jié)碼偏移量)之間的對應(yīng)關(guān)系。 LineNumberTable: line 6: 0 // LocalVariableTable 該屬性的作用是描述幀棧中局部變量與源碼中定義的變量之間的關(guān)系。 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack; public int simpleMethod(); descriptor: ()I flags: ACC_PUBLIC Code: // 這里普通的方法參數(shù)的個數(shù)為1是因?yàn)樗蓄愔械姆椒ǘ加袀€隱藏參數(shù)this stack=2, locals=4, args_size=1 /******************************************************* * 對操作數(shù)棧的操作主要看這里,下面有對這段的詳細(xì)描述 ******************************************************/ 0: bipush 13 2: istore_1 3: bipush 14 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: iload_3 11: ireturn LineNumberTable: line 62: 0 line 63: 3 line 64: 6 line 66: 10 LocalVariableTable: Start Length Slot Name Signature 0 12 0 this Lcom/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack; 3 9 1 x I 6 6 2 y I 10 2 3 z I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #1 // class com/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack 3: dup 4: invokespecial #26 // Method "<init>":()V 7: astore_1 8: getstatic #27 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: invokevirtual #33 // Method simpleMethod:()I 15: invokevirtual #35 // Method java/io/PrintStream.println:(I)V 18: return LineNumberTable: line 70: 0 line 71: 8 line 72: 18 LocalVariableTable: Start Length Slot Name Signature 0 19 0 args [Ljava/lang/String; 8 11 1 s Lcom/jasmine/Java高級/JVM/字節(jié)碼/TestJVMStack; } SourceFile: "TestJVMStack.java"
上述字節(jié)碼中的下段代碼就是JVM對操作棧的執(zhí)行順序.
// 對應(yīng)代碼 13; 0: bipush 13 // 將一個8位帶符號整數(shù) 13 壓入操作棧頂 // 對應(yīng)代碼 x = 13; 2: istore_1 // 從棧頂彈出,并將int類型值存入局部變量表的slot_1中 // 對應(yīng)代碼 14; 3: bipush 14 // 將一個8位帶符號整數(shù) 14 壓入操作棧頂 // 對應(yīng)代碼 y = 14; 5: istore_2 // 從棧頂彈出,并將int類型值存入局部變量表的slot_2中 // 對應(yīng)代碼 x; 6: iload_1 // 從局部變量表的slot_1中裝載int類型值,壓入操作棧頂 // 對應(yīng)代碼 y; 7: iload_2 // 從局部變量表的slot_2中裝載int類型值,壓入操作棧頂 // 對應(yīng)代碼 x + y; 8: iadd // 操作數(shù)棧中的前兩個int相加,并將結(jié)果壓入操作數(shù)棧頂 // 對應(yīng)代碼 z = x + y; 9: istore_3 // 從棧頂彈出,并將int類型值存入局部變量表的slot_3中 // 對應(yīng)代碼 z; 10: iload_3 // 從局部變量表的slot_3中裝載int類型值,壓入操作棧頂 // 對應(yīng)代碼 return z; 11: ireturn // 返回棧頂元素
由上可見,每次操作其實(shí)都是對棧頂或棧頂?shù)亩鄠€連續(xù)的操作棧進(jìn)行操作.方法執(zhí)行完后,會根據(jù)方法返回地址,返回上層方法,也就是上一個棧幀,如果全部棧幀都執(zhí)行完,就認(rèn)為該線程的內(nèi)容執(zhí)行完畢,線程結(jié)束生命周期.
JDK 1.7 之前 Java虛擬機(jī)規(guī)范中定義方法區(qū)是堆的一個邏輯部分,但是別名Non-Heap(非堆),以與Java堆區(qū)分. JDK 1.8 將方法區(qū)從堆中移了出來,==放入了本地內(nèi)存==中,并且改名為==元數(shù)據(jù)區(qū)==,這是不同版本虛擬機(jī)變化最大的地方.
元數(shù)據(jù)區(qū)和堆一樣,都是線程共享的.整個虛擬機(jī)中只有一個元數(shù)據(jù)區(qū). 元數(shù)據(jù)區(qū)的大小受到本機(jī)內(nèi)存容量限制,并且允許指定大小,若不指定,元數(shù)據(jù)區(qū)會根據(jù)應(yīng)用程序運(yùn)行時的需求動態(tài)設(shè)置大小 元數(shù)據(jù)區(qū)的大小如果達(dá)到參數(shù)[MaxMetaspaceSize]設(shè)置的值,將會觸發(fā)對死亡對象和類加載器的回收.
元數(shù)據(jù)區(qū)中存放已經(jīng)被虛擬機(jī)加載的 :
1. 運(yùn)行時常量池 是Class常量池的運(yùn)行時表現(xiàn)形式. 2. 字段和方法數(shù)據(jù) 3. 構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容 字面量和靜態(tài)變量被移到了堆中
如下圖:
元數(shù)據(jù)區(qū)其實(shí)是由一個個的類加載器存儲區(qū)組成的.當(dāng)類加載器不再存活,則該類加載器對應(yīng)的元數(shù)據(jù)區(qū)被回收.
每一個線程都包含自己的寄存器,保存當(dāng)前線程執(zhí)行到了哪一行.
還有一部分,順帶一提 CodeCache是代碼緩存區(qū) 主要存放JIT所編譯的代碼 還有Java所使用的本地方法代碼也會存儲在codecache中. 不同的jvm、不同的啟動方式codecache的默認(rèn)值大小也不盡相同。 ==他也獨(dú)立在堆之外,是線程共享的==
JIT : 在部分商用虛擬機(jī)中(如HotSpot),Java程序最初是通過解釋器(Interpreter)進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個方法或代碼塊的運(yùn)行特別頻繁時,就會把這些代碼認(rèn)定為“熱點(diǎn)代碼”。為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時,虛擬機(jī)將會把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化,完成這個任務(wù)的編譯器稱為即時編譯器.
到此
我們介紹了6個模塊,分別為:
1. PC寄存器(程序計數(shù)器) 2. 本地方法棧 3. 虛擬機(jī)棧 4. 堆 5. 元空間 6. CodeCache
那么,最開始那張圖就變成了這樣:
這就是Java的內(nèi)存模型了
上面說到的3個常量池
字符串常量池
運(yùn)行時常量池
類常量池
上述內(nèi)容就是怎么用最通俗的方法講解JVM內(nèi)存模型,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。