JVM總體結構圖以下:java
先貼一個代碼:數據結構
package com.jvm.jvmCourse2; public class Math { public static int INITDATA = 666; public Math() { } public int compute() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Math math = new Math(); math.compute(); System.out.println("test"); } }
棧:java虛擬機只要開始運行一個程序的時候就會給這段程序分配一個棧內存區域,專屬於這個線程,因此說棧(線程)主要存儲的就是屬於本身的局部變量的內存區域。以上圖代碼爲例,當咱們運行這段代碼的時候,java虛擬機就給我分配了一個棧。棧跟數據結構相似,都是先進後出FILO.當我執行以上代碼的時候我main方法先進棧,而後是compute方法,compute方法執行完成之後須要回到main方法,此時compute方法棧幀就會被移除。在jvm中有一個參數Xss,這個參數就是設置棧的大小,默認是1M.這1M是否是虛擬機中的全部的棧的大小,而是當前線程所佔用的棧的大小。當一個線程裏面調用的方法過多,可能會致使棧溢出。溢出代碼以下:jvm
package com.jvm.jvmCourse2; public class StackOverFlowError { static int count=0; static void redo(){ count++; redo(); } public static void main(String[] args) { redo(); } }
JVM內存分配是必定的,當XSS的值越小說明能夠啓動的線程越多,值越大說明啓動的線程越少。測試
棧幀:棧裏面又有不少複雜的結構,首先就是棧幀,每一個線程在執行一個方法的時候就會給這個方法分配一個內存區域,這個內存區域就叫作棧幀。當運行main方法的時候就會有this
在棧中給main方法分配一個棧幀,當使用方法compute()的時候就會給compute分配一個棧幀,以下圖所示:spa
每個方法分配的棧幀裏面都會有本身的結構,以下:線程
對於以上數據結構,咱們根據JVM的指令文檔來看看:3d
注意:局部變量0(iconst_0)下標通常是留給this使用,其餘的不能使用。指針
Compiled from "Math.java"
public class com.jvm.jvmCourse2.Math {
public static int INITDATA;code
public com.jvm.jvmCourse2.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();//compute方法
Code:
0: iconst_1 //將int類型常量1壓入操做數棧,即a=1的1壓入操做數棧
1: istore_1 //將int類型值存入局部變量1,即將1存到變量a中,到這裏compute中的a=1執行完成。
2: iconst_2 //將int類型常量2壓入操做數棧,即b=2的1壓入操做數棧
3: istore_2 //將int類型值存入局部變量2,即將1存到變量a中,到這裏compute中的b=2執行完成。
4: iload_1 //從局部變量1中裝載int類型值,即將a=1放入操做數棧中
5: iload_2 //從局部變量2中裝載int類型值,即將b=2放入操做數棧中
6: iadd // 執行int類型的加法
7: bipush 10 //將加法的結果壓入操做數棧中
9: imul //乘
10: istore_3 //乘以常量10,將30壓入操做數棧
11: iload_3 //c=30
12: ireturn //從方法中返回int類型的數據
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/jvm/jvmCourse2/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
static {};
Code:
0: sipush 666
3: putstatic #8 // Field INITDATA:I
6: return
}
以上1-12的字節碼執行過程圖以下:
根據代碼,在main方法執行完成compute()方法之後是須要返回main方法的。compute方法執行完成之後就會去方法出口裏面查看應該返回到哪一個方法,即方法出口記錄的是compute()方法執行完成之後應該返回的方法的對應位置。
main方法也有局部變量,math,可是math是一個對象,對象是存放在堆中的,因此這裏的局部變量是對象的一個引用。對象的值是放在堆中的。有的對象的值也會放在棧中。
本地方法棧主要是之前用來調用底層C語言的時候使用的,通常該方法用native來修飾,如今基本上不用了,本地方法棧和棧的存在是同樣的。
堆中存儲的所有是對象,(並非全部對象都存儲在堆中,部分對象會存儲在棧中)每一個對象都包含一個與之對應的class的信息。(class的目的是獲得操做指令)。jvm只有一個堆區(heap)被全部線程共享,堆中不存放基本類型和對象引用,只存放對象自己。main()方法中的局部變量math,math是一個對象,該對象存儲在堆中。局部變量表裏面只是對堆中對象的一個引用。根據對象的內存結構(以下圖)
(對象內存結構圖)
根據上圖,對象頭中有MetaData元數據指針,該指針指向的方法區中的類元信息。所以局部變量與堆的關係是引用,堆與方法區的關係是對象頭中的元數據指針。以下圖所示:
類裝載系統主要就是將類裝載到方法區裏面去,會把類的信息裝載成類元信息,即類的組成部分。又叫靜態區,跟堆同樣,被全部的線程共享。方法區包含全部的class和static變量。方法區中包含的都是在整個程序中永遠惟一的元素。方法區中主要存儲常量、靜態變量、類元信息。當靜態變量有對象類型的時候,該對象存儲在堆中,所以方法區會有指針指向堆。
類在加載的過程當中有個一個步驟叫解析,在這個解析的過程當中主要是將符號引用替換爲直接引用,什麼是符號引用,來看一下本文中的代碼轉換成符號引用的字節碼文件以下:
Classfile /E:/IDEA_SPACE/jvm-full-gc/target/classes/com/jvm/jvmCourse2/Math.class
Last modified 2019-11-20; size 857 bytes
MD5 checksum cdee9660b546aa31c2473cba0a594765
Compiled from "Math.java"
public class com.jvm.jvmCourse2.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#33 // java/lang/Object."<init>":()V
#2 = Class #34 // com/jvm/jvmCourse2/Math
#3 = Methodref #2.#33 // com/jvm/jvmCourse2/Math."<init>":()V
#4 = Methodref #2.#35 // com/jvm/jvmCourse2/Math.compute:()I
#5 = Fieldref #36.#37 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #38 // test
#7 = Methodref #39.#40 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Fieldref #2.#41 // com/jvm/jvmCourse2/Math.INITDATA:I
#9 = Class #42 // java/lang/Object
#10 = Utf8 INITDATA
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/jvm/jvmCourse2/Math;
#19 = Utf8 compute
#20 = Utf8 ()I
#21 = Utf8 a
#22 = Utf8 b
#23 = Utf8 c
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 math
#29 = Utf8 MethodParameters
#30 = Utf8 <clinit>
#31 = Utf8 SourceFile
#32 = Utf8 Math.java
#33 = NameAndType #12:#13 // "<init>":()V
#34 = Utf8 com/jvm/jvmCourse2/Math
#35 = NameAndType #19:#20 // compute:()I
#36 = Class #43 // java/lang/System
#37 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#38 = Utf8 test
#39 = Class #46 // java/io/PrintStream
#40 = NameAndType #47:#48 // println:(Ljava/lang/String;)V
#41 = NameAndType #10:#11 // INITDATA:I
#42 = Utf8 java/lang/Object
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 java/io/PrintStream
#47 = Utf8 println
#48 = Utf8 (Ljava/lang/String;)V
{
public static int INITDATA;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public com.jvm.jvmCourse2.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jvm/jvmCourse2/Math;
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
line 9: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/jvm/jvmCourse2/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c 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 #2 // class com/jvm/jvmCourse2/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String test
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 12: 0
line 13: 8
line 14: 13
line 15: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 math Lcom/jvm/jvmCourse2/Math;
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: sipush 666
3: putstatic #8 // Field INITDATA:I
6: return
LineNumberTable:
line 4: 0
}
SourceFile: "Math.java" Const pool中的#8呀什麼的就是咱們符號引用。跟本文以前的那個字節碼文件相比,這個字節碼文件複雜得多。這些符號都放在常量池裏面。以compute方法爲例,Math經過類加載器將指令碼都加載到了方法區
中的某一個地方,當java代碼在執行compute方法,怎麼找到方法區中的compute方法的指令碼呢?主要就是經過堆中對象的對象頭中的類型指針找到compute方法JVM指令碼的入口地址,將入口地址放入到動態
連接這個位置來。根據以上的講解,你們對JVM內存結構有了一個大體的印象了,接下來咱們來重點說明一下JVM內存結構中比較重要的堆。
堆
堆的內部主要包含4個區域:Eden、From、To、老年代。其中前三個屬於年輕代(Eden、From、To),其中From、To 合併稱爲Survivor區。具體的結構以下圖所示:
其中(8/10,1/10,1/10,2/3)是默認的堆空間分配比例。
堆結構圖
若是須要調整堆的空間大小,或者其餘的大小,具體每一個區域的調整參數以下:
假設咱們的堆默認分配空間大小爲600M,那麼老年代就有400M,Eden區有160M,From,To兩個區域各佔20M。通常狀況下咱們new出來的對象都是存放在Eden區域,也有可能存儲在老年代。隨着程序的執行,Eden區域會存放不少不少對象,Eden區域會被放滿。當Eden區域放滿之後就會觸發一次GC,這個GC叫minor GC.主要是對Eden+Survovor區的無引用對象進行回收。即當個人棧幀裏面沒有對堆裏的對象進行引用的時候,堆裏面的這個對象就是垃圾對象,這時候就應該被minor GC回收。對於存活的對象就會放到Survivor區域中,當我繼續new對象,Eden區又滿的時候,minorGC就會將Eden,From區域的對象清理,並將存活對象移動到To區域去,此時Eden區域以及From區域就清空了,程序繼續運行,當Eden區域再次滿了時候,MinorGC就去清理Eden和To區域,將存活對象移動到From區域,此時Eden,To區域就清空了。如此循環往復。存活的對象每經歷一次minorGC,對象頭中的分代年齡就會增長一次,當它的分代年齡達到15的時候,這個對象還存在的話,就會被移動到老年代。對於哪些對象會被挪到老年代呢,好比靜態變量,線程池,@Controller等。
如此總有一天個人老年代也會放滿,當老年代存滿的時候就會觸發fullGC,fullGC在進行回收的時候,回收的對象比較多,耗時比較長。不管是minorGC仍是fullGC都會作同一件事情就是STW(Stop The World)
下面咱們來看一段代碼:
package com.example.seckill; import net.bytebuddy.implementation.bytecode.Throw; import java.util.ArrayList; public class HeapTest { byte[] bytes=new byte[1024*100]; public static void main(String[] args) { ArrayList<HeapTest> arrays=new ArrayList<>(); while (true){ arrays.add(new HeapTest()); try { Thread.sleep(10); }catch (Exception e){ e.printStackTrace(); } } } }
這段代碼在執行的時候,在命令窗口(cmd打開),輸入jvisualvm.而後會打開以下的窗口:
Visual GC窗口正常狀況下是沒有的,須要本身安裝。
根據以上圖形咱們能夠看到整個對象在堆中的流轉狀況Eden,Survivor 0 ,Survivor 1中的循環往復的進行回收。老年代(Old Gen)在不斷的增加。由於在這個程序中每個對象都是存在引用的,所以老年代滿的時候,程序就會報錯(OOM,堆溢出),以下圖所示
下面來看一個StackOverFlowError的測試案例。
將-XSS128k,即將棧設置爲128k,這樣的話就會致使堆棧異常,由於棧分配的空間越小說明棧幀分配的空間越小,可是對整個JVM來講能夠可運行的線程就越多。
package com.jvm.jvmCourse2; public class StackOverFlowError { static int count=0; static void redo(){ count++; redo(); } public static void main(String[] args) { try{ redo(); }catch (Throwable e){ e.printStackTrace(); System.out.println(count); } } }
運行結果:
java.lang.StackOverflowError at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:6) at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:7) at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:7) at com.jvm.jvmCourse2.StackOverFlowError.redo(StackOverFlowError.java:7)