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