JVM執行時資料區(堆、虛擬機器棧、本地方法棧、方法區、程式計數器...)
Java虛擬機器規範執行時資料區相關地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
執行時資料區模型
在Java虛擬機器中把記憶體分為若干個不同的資料區域。這些區域有各自的用途,有些區域隨著虛擬機器程序啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立和銷燬。在JVM中主要分為以下幾個區域:
- 程式計數器
- 方法區
- 虛擬機器棧
- 本地方法棧
- Java堆
程式計數器
作用
程式計數器記憶體佔用較小,是當前執行緒執行的位元組碼的行號指示器。位元組碼直譯器就是通過這個改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、迴圈、調整、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
為什麼是執行緒私有的?
Java虛擬機器的多執行緒是通過執行緒輪流切換、分配處理器執行時間的方式來實現的,在任何一個確定時刻,一個處理器(對於多核CPU來說是一個核心)只會執行一條執行緒的指令。因此為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要一個私有的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存。
為什麼需要計數器?
多執行緒允許的情況下,一個執行緒中有多個指令,為了使執行緒切換可以恢復到正確的位置,每個執行緒都具有各自獨立的程式計數器,所以該區域是執行緒私有的。
如果執行的是Java方法,計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果執行的是Native方法,計數器儲存為空。這塊區域是虛擬機器規範中唯一沒有OutOfMemoryError的區域。
作用
方法區(No-Heap)
方法區是一個抽象的概念,JDK7及之前被稱為“永久代”,JDK8及以後被稱為“元空間”,它用於儲存虛擬機器載入的型別資訊、常量、靜態變數(JDK7及之前,JDK8及之後就把靜態變數與Class物件放到了堆中)、即時編譯器編譯(JIT)後的程式碼等資料,是各個執行緒的共享記憶體區域。雖然《Java虛擬機器規範》中把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫作“非堆”(Non-Heap),目的是與Java堆區分開來。
JDK8之前,很多人把方法區又稱為永久代(Permanent Generation),或將兩者混為一談,本質上是不對等的,因為僅僅是當時的HotSpot虛擬機器設計團隊把收集器的分代擴充套件至方法區,或者說使用永久代來實現方法區而已,這樣使得HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分記憶體,省去了專門為方法區編寫記憶體管理程式碼的工作。但是這種設計更會導致Java應用更容易遇到記憶體溢位的問題(永久代有-XX:MaxPermSize的上限,即使不設定也有預設大小,而像J9和JRockit只要沒有觸碰到程序可用記憶體的上限,就不會有問題),在JDK6的時候HotSpot團隊就有放棄方法區,逐步改為採用本地記憶體(Native Memory)來實現方法區的計劃,到了JDK7已經把原本放在永久代的字串常量池、靜態變數等移出,到了JDK8就完全廢除了永久代的概念,改用JRockit、J9一樣在本地記憶體中實現的元空間(Meta-space),把JDK7中永久代剩餘的內容(主要是型別資訊)全部移到元空間中。
方法區儲存每個類資訊,如:
- Classloader Reference
- Run Time Constant Pool
- Numeric constants
- Field references
- Method References
- Attributes
- Field data
- Per field
- Name
- Type
- Modifiers
- Attributes
- Per field
- Method data
- Per method
- Name
- Return Type
- Parameter Types (in order)
- Modifiers
- Attributes
- Per method
- Method code
- Per method
- Bytecodes
- Operand stack size
- Local variable size
- Local variable table
- Exception table
- Per exception handler
- Start point
- End point
- PC offset for handler code
- Constant pool index for exception class being caught
- 類載入器參考 執行時常量池 * * * * *數字常量 *欄位引用 *方法引用 *屬性
- *領域資料 *每個欄位 *名字 *型別 *修飾符 *屬性
- *方法資料 *每個方法 *名字 *返回型別 *引數型別(按順序) *修飾符 *屬性
- *方法程式碼 *每個方法 *位元組碼 *運算元堆疊大小 *區域性變數大小 *區域性變量表 *例外表
- Per exception handler
- Per method
在JDK8之前的HotSpot JVM,存放這些“永久的”區域叫做“永久代(permanent generation)”。永久代是一片連續的堆空間,在JVM啟動前通過在命令列設定引數-XX:MaxPermSize來設定永久代最大可分配的記憶體空間,預設大小為64M(64位的JVM預設是85M)。
方法區或永生代相關引數配置
-XX:PermSize=64MB
最小尺寸,初始分配-XX:MaxPermSize=256
最大允許分配尺寸- 按需分配
-XX:+CMSClassUnloadingEnabled
-XX:+CMSPermGenSweepingEnabled
設定垃圾不回收-server
選項下預設MaxPermSize為64MB
,-client
選項下預設MaxPermSize
為32MB
Java虛擬機器規範堆方法區限制非常的寬鬆,可選擇不垃圾回收,以及不需要連續的記憶體和可擴充套件的大小。這個區域主要是針對於常量池的回收以及對型別的解除安裝,當方法區無法分配到足夠的記憶體的時候也會報OOM。
常量池
Class檔案常量池
以下使用實際程式碼及反編譯Class檔案講解
反編譯命令:javap -verbose StringTest.class
public class StringTest {
private static String s1 = "static";
public static void main(String[] args) {
String hello1 = new String("hell") + new String("o");
String hello2 = new String("he") + new String("llo");
String hello3 = hello1.intern();
String hello4 = hello2.intern();
System.out.println(hello1 == hello3);
System.out.println(hello1 == hello4);
}
}
Classfile /E:/workspace/VariousCases/target/classes/cn/onenine/jvm/constantpool/StringTest.class
Last modified 2021-8-3; size 1299 bytes
MD5 checksum 338bd0034155ec3bf8d608540a31761c
Compiled from "StringTest.java"
public class cn.onenine.jvm.constantpool.StringTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // cn/onenine/jvm/constantpool/StringTest
#2 = Utf8 cn/onenine/jvm/constantpool/StringTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 s1
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = String #11 // static
#11 = Utf8 static
#12 = Fieldref #1.#13 // cn/onenine/jvm/constantpool/StringTest.s1:Ljava/lang/String;
#13 = NameAndType #5:#6 // s1:Ljava/lang/String;
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 <init>
#17 = Methodref #3.#18 // java/lang/Object."<init>":()V
#18 = NameAndType #16:#8 // "<init>":()V
#19 = Utf8 this
#20 = Utf8 Lcn/onenine/jvm/constantpool/StringTest;
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Class #24 // java/lang/StringBuilder
#24 = Utf8 java/lang/StringBuilder
#25 = Class #26 // java/lang/String
#26 = Utf8 java/lang/String
#27 = String #28 // hell
#28 = Utf8 hell
#29 = Methodref #25.#30 // java/lang/String."<init>":(Ljava/lang/String;)V
#30 = NameAndType #16:#31 // "<init>":(Ljava/lang/String;)V
#31 = Utf8 (Ljava/lang/String;)V
#32 = Methodref #25.#33 // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#33 = NameAndType #34:#35 // valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#34 = Utf8 valueOf
#35 = Utf8 (Ljava/lang/Object;)Ljava/lang/String;
#36 = Methodref #23.#30 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#37 = String #38 // o
#38 = Utf8 o
#39 = Methodref #23.#40 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = NameAndType #41:#42 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#41 = Utf8 append
#42 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = Methodref #23.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#44 = NameAndType #45:#46 // toString:()Ljava/lang/String;
#45 = Utf8 toString
#46 = Utf8 ()Ljava/lang/String;
#47 = String #48 // he
#48 = Utf8 he
#49 = String #50 // llo
#50 = Utf8 llo
#51 = Methodref #25.#52 // java/lang/String.intern:()Ljava/lang/String;
#52 = NameAndType #53:#46 // intern:()Ljava/lang/String;
#53 = Utf8 intern
#54 = Fieldref #55.#57 // java/lang/System.out:Ljava/io/PrintStream;
#55 = Class #56 // java/lang/System
#56 = Utf8 java/lang/System
#57 = NameAndType #58:#59 // out:Ljava/io/PrintStream;
#58 = Utf8 out
#59 = Utf8 Ljava/io/PrintStream;
#60 = Methodref #61.#63 // java/io/PrintStream.println:(Z)V
#61 = Class #62 // java/io/PrintStream
#62 = Utf8 java/io/PrintStream
#63 = NameAndType #64:#65 // println:(Z)V
#64 = Utf8 println
#65 = Utf8 (Z)V
#66 = Utf8 args
#67 = Utf8 [Ljava/lang/String;
#68 = Utf8 hello1
#69 = Utf8 hello2
#70 = Utf8 hello3
#71 = Utf8 hello4
#72 = Utf8 StackMapTable
#73 = Class #67 // "[Ljava/lang/String;"
#74 = Utf8 SourceFile
#75 = Utf8 StringTest.java
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #10 // String static
2: putstatic #12 // Field s1:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
public cn.onenine.jvm.constantpool.StringTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #17 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/onenine/jvm/constantpool/StringTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=5, args_size=1
0: new #23 // class java/lang/StringBuilder
3: dup
4: new #25 // class java/lang/String
7: dup
8: ldc #27 // String hell
10: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
13: invokestatic #32 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #36 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: new #25 // class java/lang/String
22: dup
23: ldc #37 // String o
25: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #39 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #43 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: new #23 // class java/lang/StringBuilder
38: dup
39: new #25 // class java/lang/String
42: dup
43: ldc #47 // String he
45: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
48: invokestatic #32 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
51: invokespecial #36 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
54: new #25 // class java/lang/String
57: dup
58: ldc #49 // String llo
60: invokespecial #29 // Method java/lang/String."<init>":(Ljava/lang/String;)V
63: invokevirtual #39 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
66: invokevirtual #43 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
69: astore_2
70: aload_1
71: invokevirtual #51 // Method java/lang/String.intern:()Ljava/lang/String;
74: astore_3
75: aload_2
76: invokevirtual #51 // Method java/lang/String.intern:()Ljava/lang/String;
79: astore 4
81: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream;
84: aload_1
85: aload_3
86: if_acmpne 93
89: iconst_1
90: goto 94
93: iconst_0
94: invokevirtual #60 // Method java/io/PrintStream.println:(Z)V
97: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream;
100: aload_1
101: aload 4
103: if_acmpne 110
106: iconst_1
107: goto 111
110: iconst_0
111: invokevirtual #60 // Method java/io/PrintStream.println:(Z)V
114: return
LineNumberTable:
line 13: 0
line 14: 35
line 15: 70
line 16: 75
line 17: 81
line 18: 97
line 20: 114
LocalVariableTable:
Start Length Slot Name Signature
0 115 0 args [Ljava/lang/String;
35 80 1 hello1 Ljava/lang/String;
70 45 2 hello2 Ljava/lang/String;
75 40 3 hello3 Ljava/lang/String;
81 34 4 hello4 Ljava/lang/String;
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 93
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "StringTest.java"
執行時常量池
執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等資訊外,還有一項資訊是常量表(Constant Pool Table),用於存放編譯器生成的各種字面量和引用符號,這部分內容將在類載入後放到執行時常量池中。
執行時常量池相對於Class檔案常量池的另外一個特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是說並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池,執行時期也可以將新的常量放入執行時常量池,如String#intern()
方法。
既然執行時常量池是方法區的一部分,自然受到方法區的記憶體的限制,當常量池無法再申請到記憶體時,就會丟擲OutOfMemoryErro異常。
全域性字串常量池
HotSpot VM裡,記錄intered字串的一個全域性表叫做String Table,它本質上就是一個HashSet
只儲存對java.lang.String例項的引用,而不儲存實際的String物件,根據這個引用可以找到實際的String物件。
更多關於String與常量池相關的知識,單獨開一篇文章記錄,String與字串常量池的恩怨情仇
虛擬機器棧
虛擬機器棧是每個Java方法的記憶體模型:每個方法被執行的時候都會建立一個"棧幀",用於儲存區域性變量表(包括引數)、操作棧、方法出口等資訊。每個方法被呼叫到執行完成的過程,就對應著一個棧幀在虛擬機器棧從入棧到出棧的過程。
平時說的棧一般指的是區域性變量表部分。區域性變量表存放了編譯期可知的各種Java虛擬機器基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別,它並不等同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或者其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址),這些資料型別在區域性變量表中以槽(slot)來表示,其中64位長度的long和double型別的資料會佔用兩個變數槽,其餘的資料型別只佔用一個。
區域性變量表所需要的空間在編譯期完成分配,當執行一個方法的時候,該方法需要在棧幀中分配多大的區域性變量表的空間完全是可以確定的,因此在方法執行的期間不會改變區域性變量表的大小,這裡說的“大小”是指變數槽的數量,虛擬機器真正使用多大的記憶體空間來實現一個變數槽是由具體的虛擬機器實現自行決定的事情。
該區域就是我們常說的Java記憶體中的棧區域,該區域的區域性變量表儲存的是基本型別、物件的引用型別,在物件的引用型別中儲存的是指向物件的堆空間的地址。
該區域會出現兩種異常
- 當執行緒請求的棧深度超過虛擬機器允許的深度,丟擲StackOverflowError異常(遞迴!!!)
- 一般虛擬機器的棧容量都是可以動態擴充套件的,當棧擴張時申請不多足夠的記憶體,就會丟擲OOM異常(HotSpot虛擬機器的棧容量是不允許動態擴充套件的,所以HotSpot虛擬機器上是不會由於虛擬機器棧無法擴充套件而導致OOM異常的----只要執行緒申請棧空間成功了就不會出現OOM,但是如果申請失敗了,仍然是會出現OOM異常的)。
本地方法棧
本地方法棧(Native Method Stacks)與虛擬機器棧發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java方法(位元組碼)服務,本地方法棧為虛擬機器使用到的native方法分為,底層呼叫的是C或者C++的方法。
《Java虛擬機器規範》中對本地方法棧中方法使用的語言、使用方式與資料結構沒有任何強制規定,因此具體的虛擬機器可以根據需要自由實現它,HotSpot虛擬機器直接就把本地方法棧和虛擬機器棧合二為一來使用,與虛擬機器棧一樣本地方法棧也會在棧深度溢位或者棧擴充套件失敗時分別丟擲StackOverflowError和OutOfMemoryError異常。
Java堆
Java堆(Java Heap)是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的記憶體區域,在虛擬機器啟動的時候建立。
此記憶體區域的目的就是儲存物件例項,幾乎所有的物件例項都在這裡分配記憶體。從回收記憶體的角度看,由於現代垃圾收集器大部分都是基於分代理論設計的,所以Java堆中經常出現“新生代”、“老年代”、“永久代”、“Eden空間”、“From Survivor空間”、“To Survivor空間”等名詞,這些區域劃分僅僅是部分垃圾收集器的共同特性或者設計風格,而非某個Java虛擬機器的具體實現的固有記憶體佈局,更不是《Java虛擬機器規範》裡對Java堆的進一步劃分。後邊講到的G1垃圾收集器就不是基於分代理論設計的。
Java堆是執行緒共享的,它的目的是存放物件例項。同時它也是GC所管理的主要區域,因此常被稱為GC堆。根據虛擬機器規範,Java堆可以存在物理上不連續的記憶體空間,就像磁碟空間邏輯上是連續的即可。它的記憶體大小可以設定為固定大小,也可以擴充套件。當前主流的虛擬機器如HotSpot都能按擴充套件實現(通過設定 **-Xmx**
和-Xms
,預設堆記憶體大小為伺服器記憶體的1/4),如果堆中沒有記憶體完成例項分配,而且堆無法擴充套件,則會報OOM錯誤(OutOfMemoryError)。
新生代又分為:Eden空間、From Survivor
、To Survivor
空間,。進一步劃分的目的是為了更好的回收記憶體或者更快的分配記憶體。
非堆(No-Heap)
直接記憶體
直接記憶體並不是虛擬機器執行時資料區的一部分,也不是《Java虛擬機器規範》中定義的記憶體區域,但是這部分記憶體也被頻繁使用,而且也可能導致OOM。
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區
(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆裡面的
DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了
在Java堆和Native堆中來回複製資料。
本機記憶體直接記憶體分配不會受到Java堆大小的限制,但是既然是記憶體,肯定會受到物理機記憶體的限制,當我們通過-Xmx設定堆的最大記憶體時,不要忘了還有直接記憶體,如果堆記憶體設定過大,將會導致直接記憶體不夠用,導致動態擴充套件時發生OOM。
直接記憶體的容量大小可以通過-XX:MaxDirectMemorySize引數來指定,如果不指定,則預設與Java堆的最大值(-Xmx)一致。
直接記憶體導致的OOM不會在Heap Dump檔案中看到什麼明顯的異常,如果發現記憶體溢位後的Dump檔案很小而程式中又直接或間接使用了DirectMemory(典型的間接使用就是NIO),那就可以考慮重點檢查一下直接記憶體方面的原因了。
本文由部落格一文多發平臺 OpenWrite 釋出!