一 JVM介紹
1.1 什么是JVM
1.2 jvm構(gòu)成
jvm由三個(gè)主要的子系統(tǒng)組成:
- 類(lèi)裝載器子系統(tǒng)(將字節(jié)碼.class文件裝載到運(yùn)行時(shí)數(shù)據(jù)區(qū)中去)
- 運(yùn)行時(shí)數(shù)據(jù)區(qū)(java虛擬機(jī)對(duì)應(yīng)的內(nèi)存區(qū)域叫運(yùn)行時(shí)數(shù)據(jù)區(qū))
- 執(zhí)行引擎(執(zhí)行java程序再扭,輸出結(jié)果脏款,包含垃圾收集器模塊赠幕,用于在程序運(yùn)行的時(shí)候不斷清理內(nèi)存區(qū)域中垃圾)
下面圖是整個(gè)jvm的結(jié)構(gòu)
java源代碼文件會(huì)被編譯成class文件殖妇,然后被jvm的類(lèi)裝載器裝載到j(luò)vm里面买猖。
所有的數(shù)據(jù)都在我們運(yùn)行時(shí)數(shù)據(jù)區(qū)改橘,所有的數(shù)據(jù)都在運(yùn)行時(shí)數(shù)據(jù)區(qū)以后,包括代碼玉控,方法都進(jìn)來(lái)以后飞主,就由我們jvm的執(zhí)行引擎來(lái)負(fù)責(zé)執(zhí)行:
- 如果是執(zhí)行方法,那么它就會(huì)在虛擬機(jī)棧里面進(jìn)行方法的依次進(jìn)棧高诺,出棧等等操作
- 如果要調(diào)用一些本地方法(指的是我們操作系統(tǒng)暴露的本地方法庫(kù)/本地庫(kù)接口)
- 包括程序執(zhí)行到哪一行了:程序計(jì)數(shù)器
線程隔離的數(shù)據(jù)區(qū):線程私有碌识,每個(gè)線程里面都有:
- 當(dāng)前線程調(diào)用到哪了(程序計(jì)數(shù)器)
- 當(dāng)前線程調(diào)用到哪個(gè)方法了(虛擬機(jī)棧)
- 當(dāng)前線程調(diào)用的是本地接口的哪個(gè)方法(本地方法棧),每個(gè)線程都是相互隔離的虱而,自己保存自己的
java8以后就有了元數(shù)據(jù)區(qū)筏餐,元數(shù)據(jù)區(qū)直接操作我們物理內(nèi)存的
還有JIT編譯產(chǎn)物:我們代碼運(yùn)行期間,我們哪個(gè)代碼沒(méi)編譯薛窥,編譯期間所有的代碼緩存都在這里胖烛,這塊的調(diào)節(jié)也是有的,但是我們更多的關(guān)注的堆
- 方法區(qū)是JVM規(guī)范的一個(gè)概念定義诅迷,并不是一個(gè)具體的實(shí)現(xiàn)佩番,每一個(gè)JVM的實(shí)現(xiàn)都可以有各自的實(shí)現(xiàn);
- 在Java官方的HotSpot 虛擬機(jī)中罢杉,Java8版本以后趟畏,是用元空間來(lái)實(shí)現(xiàn)的方法區(qū);在Java8之前的版本滩租,則是用永久代實(shí)現(xiàn)的方法區(qū)赋秀;
- 也就是說(shuō),“元空間” 和 “方法區(qū)”律想,一個(gè)是HotSpot 的具體實(shí)現(xiàn)技術(shù)猎莲,一個(gè)是JVM規(guī)范的抽象定義;
元空間的存儲(chǔ)位置是在計(jì)算機(jī)的內(nèi)存當(dāng)中技即,而永久代的存儲(chǔ)位置是在JVM的堆(Heap)中
之所以移除permGen永久代在java8中因?yàn)?/p>
- This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
這是JRockit和Hotspot融合工作的一部分著洼。JRockit的客戶(hù)不需要配置永久代(因?yàn)镴Rockit沒(méi)有永久代),并且習(xí)慣于不配置永久代而叼。- 隨著Java在Web領(lǐng)域的發(fā)展身笤,Java程序變得越來(lái)越大,需要加載的內(nèi)容也越來(lái)越多葵陵,如果使用永久代實(shí)現(xiàn)方法區(qū)液荸,那么需要手動(dòng)擴(kuò)大堆的大小,而使用元空間之后脱篙,就可以直接存儲(chǔ)在內(nèi)存當(dāng)中娇钱,不用手動(dòng)去修改堆的大小伤柄。
以上兩部分引用轉(zhuǎn)自:https://www.zhihu.com/question/358312524
二 運(yùn)行時(shí)數(shù)據(jù)區(qū)(JVM內(nèi)存結(jié)構(gòu))
java程序運(yùn)行的時(shí)候在一個(gè)進(jìn)程中運(yùn)行,進(jìn)程中有很多線程文搂,這些線程是真正去執(zhí)行我們代碼的最小單元响迂,線程運(yùn)行的時(shí)候會(huì)使用到一些數(shù)據(jù)因此是會(huì)跟內(nèi)存進(jìn)行交互的,內(nèi)存有可能是所有線程共享的细疚,也有可能是每個(gè)線程獨(dú)自占有的。
線程私有的內(nèi)存區(qū)域
2.1 棧內(nèi)存
- 棧是每個(gè)線程獨(dú)有的川梅,不被其他線程共享
- 棧是先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)
- 方法進(jìn)棧以后稱(chēng)之為棧幀疯兼,一個(gè)棧幀包括四個(gè)部分:局部變量表,操作數(shù)棧贫途,動(dòng)態(tài)鏈接吧彪,方法出口
案例
public class Math {
private 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("finished");
}
}
main方法先進(jìn)棧,compute()后進(jìn)棧丢早,各自維護(hù)了四部分姨裸,當(dāng)compute方法執(zhí)行完畢后,compute棧幀彈出怨酝,然后main方法彈出棧
執(zhí)行上面的java代碼傀缩,生成字節(jié)碼class文件Math.class,然后我們采用jdk提供的方便閱讀字節(jié)碼文件的javap命令來(lái)查看生成的易讀的字節(jié)碼文件到txt文件中
javap -c Math.class > math.txt
Compiled from "Math.java"
public class org.radient.jvm.Math {
public org.radient.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1 # 將int類(lèi)型常量1壓入棧(操作數(shù)棧)农猬,即1
1: istore_1 # 將int類(lèi)型值存入局部變量1赡艰,即a
2: iconst_2 # 將int類(lèi)型常量2壓入棧
3: istore_2 # 將int類(lèi)型值存入局部變量2
4: iload_1 # 從局部變量1中裝載int類(lèi)型值1(注意:裝載的是值1,非變量引用a=锎小慷垮!或a=1)
5: iload_2 # 從局部變量2中裝載int類(lèi)型值2
6: iadd # 執(zhí)行int類(lèi)型的加法,即a+b
7: bipush 10 # 將a+b的結(jié)果存回操作數(shù)棧揍堕,
9: imul # 執(zhí)行int類(lèi)型的乘法料身,即a+b的結(jié)果*10
10: istore_3 # 將int類(lèi)型值存入局部變量3,即變量c
11: iload_3 # 從局部變量3中裝載int類(lèi)型值
12: ireturn # 返回結(jié)果
public static void main(java.lang.String[]);
Code:
0: new #2 // class org/radient/jvm/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 finished
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
}
根據(jù)上面可讀的字節(jié)碼文件來(lái)分析一下整個(gè)的執(zhí)行過(guò)程衩茸,上面每行序號(hào)后面對(duì)應(yīng)的是JVM指令芹血,具體每個(gè)指令的意思,參閱:https://www.cnblogs.com/lsy131479/p/11201241.html
根據(jù)上面的指令, 可以分析一下compute()執(zhí)行過(guò)程递瑰,具體的解析指令已經(jīng)在上面代碼中標(biāo)注出來(lái)了
案例代碼執(zhí)行過(guò)程:(操作數(shù)棧用于臨時(shí)存放操作中的值祟牲,局部變量表存放變量和變量的值)
(1)先為局部變量例如a開(kāi)辟內(nèi)存空間,入局部變量表a抖部,變量的值1進(jìn)入操作數(shù)棧中進(jìn)行運(yùn)算说贝,運(yùn)算完將結(jié)果更新到局部變量表a,有了a=1
(2)同上慎颗,處理變量b=2
(3)將局部變量表中要運(yùn)算的兩個(gè)變量的值1和2放入操作數(shù)棧乡恕,然后因?yàn)椴僮鲾?shù)棧也是棧結(jié)構(gòu)言询,遵循后進(jìn)先出,因此看到iadd(int類(lèi)型加法)的時(shí)候傲宜,先將2彈出操作數(shù)棧运杭,然后將1彈出操作數(shù)棧,然后對(duì)這兩個(gè)元素進(jìn)行加法運(yùn)算函卒,獲得結(jié)果3
(4)將上一步獲得的結(jié)果3寫(xiě)回操作數(shù)棧辆憔,并執(zhí)行bipush 10,將10壓如操作數(shù)棧报嵌,然后彈出10和3虱咧,執(zhí)行乘法運(yùn)算,將結(jié)果30寫(xiě)回操作數(shù)棧
(5)istore_3(將int類(lèi)型值存入局部變量3)將30寫(xiě)入局部變量表給c锚国,c=30
(6)iload_3(將int類(lèi)型值放到操作數(shù)棧)將30放入操作數(shù)棧腕巡,然后執(zhí)行ireturn,從方法中返回int類(lèi)型的30血筑,如果要打印的話(huà)绘沉,就返回給system.out方法所屬的棧禎,棧禎順序=>>>main->system.out->compute
除了上面的查看方式豺总,也可以是用jclasslib工具來(lái)方便查看车伞,它有idea的插件版本,也有可安裝版本喻喳,github地址:https://github.com/ingokegel/jclasslib/releases
- Name:方法名
- Description:描述了方法的參數(shù)類(lèi)型以及返回值類(lèi)型帖世。比如<([Ljava/lang/String;)V>,說(shuō)明參數(shù)類(lèi)型是Ljava/lang/String;返回值類(lèi)型是-V沸枯,表示的是void類(lèi)型日矫。
- Access flags:訪問(wèn)標(biāo)識(shí)。public static绑榴。
其他詳細(xì)參數(shù)說(shuō)明:
https://www.cnblogs.com/yuexiaoyun/articles/13998443.html
2.1.1 局部變量表
main方法中上來(lái)就創(chuàng)建了個(gè)math對(duì)象哪轿,即main()的局部變量是一個(gè)對(duì)象類(lèi)型,不是基本類(lèi)型翔怎,根據(jù)我們的常識(shí)窃诉,對(duì)象類(lèi)型new出來(lái)的對(duì)象是放在堆內(nèi)存的,那么相比于compute()中的基本變量赤套,main中創(chuàng)建的math對(duì)象在局部變量中怎么保存呢飘痛?
math()的局部變量表保存math對(duì)象的引用,math對(duì)象的引用指向的是堆內(nèi)存中的math對(duì)象實(shí)體
2.1.2 方法出口
上面圖中我們剛才看了局部變量表和操作數(shù)棧容握,那么方法出口是什么呢宣脉?
方法出口就是compute方法執(zhí)行完以后返回main方法,怎么知道要執(zhí)行下面的打印syso語(yǔ)句呢剔氏?就是根據(jù)這個(gè)方法出口塑猖,類(lèi)似于導(dǎo)游的作用
2.1.3 動(dòng)態(tài)鏈接
動(dòng)態(tài)鏈接就是存儲(chǔ)當(dāng)前線程很多不同方法的指令碼竹祷,只在程序運(yùn)行的時(shí)候創(chuàng)建
我們可以通過(guò)下述代碼助于理解
public static void main(String[] args) {
Math math = new Math();
math.compute();
Math math2 = new Math();
math2.compute();
System.out.println("finished");
}
如上創(chuàng)建了兩個(gè)對(duì)象,math和math2羊苟,都是來(lái)源于模板類(lèi)Math塑陵,每new出來(lái)一個(gè)對(duì)象,該對(duì)象頭里就有個(gè)指針指向?qū)ο笏鶎俚牡哪莻€(gè)類(lèi)(math.class)蜡励,為什么要指向呢令花?為什么就知道執(zhí)行的compute()的代碼就是上面那幾行呢?是math類(lèi)的呢凉倚?
這并不是理所當(dāng)然彭则!是因?yàn)閯?chuàng)建對(duì)象的時(shí)候,對(duì)象里面存儲(chǔ)了類(lèi)元信息(比如:這個(gè)類(lèi)有哪些方法都是屬于這個(gè)類(lèi)的類(lèi)元信息)
一旦有了這個(gè)指針以后占遥,再去調(diào)用這個(gè)對(duì)象的compute()的時(shí)候,底層做的就是根據(jù)math對(duì)象的頭指針找到對(duì)應(yīng)的Math類(lèi)的那塊(指令碼)
所以對(duì)象1和對(duì)象2都找到了Math類(lèi)對(duì)應(yīng)的compute()的代碼输瓜,math.compute()
是符號(hào)引用
更深層次理解什么叫動(dòng)態(tài)鏈接:
我們生成更加復(fù)雜的可讀的指令碼文件瓦胎,采用命令:
javap -v Math.class > math.txt
Classfile /H:/package/??????/java-study/target/classes/org/radient/jvm/Math.class
Last modified 2019-10-14; size 809 bytes
MD5 checksum 5ce39fe60ec15465be0bf023228c71f6
Compiled from "Math.java"
public class org.radient.jvm.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#31 // java/lang/Object."<init>":()V
#2 = Class #32 // org/radient/jvm/Math
#3 = Methodref #2.#31 // org/radient/jvm/Math."<init>":()V
#4 = Methodref #2.#33 // org/radient/jvm/Math.compute:()I
#5 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #36 // finished
#7 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #39 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lorg/radient/jvm/Math;
#16 = Utf8 compute
#17 = Utf8 ()I
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 c
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8 math
#27 = Utf8 math2
#28 = Utf8 MethodParameters
#29 = Utf8 SourceFile
#30 = Utf8 Math.java
#31 = NameAndType #9:#10 // "<init>":()V
#32 = Utf8 org/radient/jvm/Math
#33 = NameAndType #16:#17 // compute:()I
#34 = Class #40 // java/lang/System
#35 = NameAndType #41:#42 // out:Ljava/io/PrintStream;
#36 = Utf8 finished
#37 = Class #43 // java/io/PrintStream
#38 = NameAndType #44:#45 // println:(Ljava/lang/String;)V
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/System
#41 = Utf8 out
#42 = Utf8 Ljava/io/PrintStream;
#43 = Utf8 java/io/PrintStream
#44 = Utf8 println
#45 = Utf8 (Ljava/lang/String;)V
{
public org.radient.jvm.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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/radient/jvm/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 10: 0
line 11: 2
line 12: 4
line 13: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lorg/radient/jvm/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=3, args_size=1
0: new #2 // class org/radient/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: new #2 // class org/radient/jvm/Math
16: dup
17: invokespecial #3 // Method "<init>":()V
20: astore_2
21: aload_2
22: invokevirtual #4 // Method compute:()I
25: pop
26: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
29: ldc #6 // String finished
31: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
34: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 13
line 20: 21
line 21: 26
line 22: 34
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 args [Ljava/lang/String;
8 27 1 math Lorg/radient/jvm/Math;
21 14 2 math2 Lorg/radient/jvm/Math;
MethodParameters:
Name Flags
args
}
SourceFile: "Math.java"
這個(gè)#4是一個(gè)引用,指向常量池中的
而#2,#32又是引用尤揣,指向Math的Class和
這樣我們就將靜態(tài)的compute方法轉(zhuǎn)化為實(shí)際的指令碼存放位置
根據(jù)堆中對(duì)象的頭指針搔啊,找到方法區(qū)中加載的Math.class類(lèi)的元信息(指令碼的內(nèi)存地址),將這個(gè)內(nèi)存地址放到棧中的動(dòng)態(tài)鏈接內(nèi)存中
再執(zhí)行compute方法的時(shí)候北戏,會(huì)根據(jù)動(dòng)態(tài)鏈接负芋,返回一條線在方法區(qū)中找到放到方法區(qū)這塊內(nèi)存中的指令碼,這塊指令碼是程序運(yùn)行過(guò)程中生成的
2.2 程序計(jì)數(shù)器
(1)程序計(jì)數(shù)器就是用來(lái)存儲(chǔ)將要執(zhí)行那一行JVM指令碼的行號(hào)(內(nèi)存地址)
(2)程序計(jì)數(shù)器同棧結(jié)構(gòu)一樣是每個(gè)線程自己的嗜愈,不被其他線程共享
(3)執(zhí)行第一行代碼的時(shí)候旧蛾,程序計(jì)數(shù)器就有值了,而且每執(zhí)行完一行蠕嫁,jvm的執(zhí)行引擎锨天,會(huì)將當(dāng)前線程的程序計(jì)數(shù)器的值改為下一行的行號(hào),根據(jù)這個(gè)程序計(jì)數(shù)器剃毒,知道我們將要執(zhí)行的下一行代碼
如上圖病袄,可讀字節(jié)碼文件每一行指令前的行號(hào)就是程序計(jì)數(shù)器中記錄的東西
2.3 本地方法棧
帶native的方法,不是java實(shí)現(xiàn)赘阀,是c語(yǔ)言實(shí)現(xiàn)的益缠,Java執(zhí)行到這一行的時(shí)候,會(huì)去java底層的c庫(kù)里面基公,找xx.dll結(jié)尾的文件(類(lèi)似于java中的jar包)幅慌,在這個(gè)dll文件中有start0方法的實(shí)現(xiàn)
執(zhí)行引擎會(huì)利用本地方法接口來(lái)真正調(diào)用底層c語(yǔ)言的接口
線程共享的內(nèi)存區(qū)域
2.4 方法區(qū)
方法區(qū)的基本介紹在上面的前言部分已經(jīng)闡述,下面來(lái)詳細(xì)講講方法區(qū)
首先是方法區(qū)的一些概念
2.4.1 方法區(qū)的演變史
勘誤:下面圖中的permGen旁邊的文字都是永久代實(shí)現(xiàn)
- jdk1.6以及1.6以前
有永久代轰豆,靜態(tài)變量存放在永久代
- jdk1.7
有永久代欠痴,但是已經(jīng)開(kāi)始著手移除永久代迄靠,首當(dāng)其沖將靜態(tài)變量和字符串常量池移動(dòng)到堆中
- jdk1.8
隨著JDK8的到來(lái),JVM不再有PermGen喇辽。但類(lèi)的元數(shù)據(jù)信息(metadata)還在掌挚,只不過(guò)不再是存儲(chǔ)在連續(xù)的堆空間上,而是移動(dòng)到叫做“Metaspace”的本地內(nèi)存(Native memory)中菩咨。
2.4.2 class文件常量池吠式、運(yùn)行時(shí)常量池、字符串常量池
- class文件常量池
已加載的每個(gè)class文件中抽米,都維護(hù)著一個(gè)常量池特占,里面存放著編譯時(shí)期生成的各種字面值和符號(hào)引用。标捺,
class文件常量池在類(lèi)被加載的時(shí)候亡容,會(huì)被復(fù)制到方法區(qū)中的運(yùn)行時(shí)常量池嗤疯,池中的數(shù)據(jù)項(xiàng)類(lèi)似數(shù)組項(xiàng)一樣,是通過(guò)索引訪問(wèn)的
類(lèi)的加載過(guò)程中的鏈接部分的解析步驟就是
以下內(nèi)容節(jié)選自https://blog.csdn.net/luanlouis/article/details/39960815
- 運(yùn)行時(shí)常量池
jvm會(huì)將各個(gè)class文件中的常量池載入到運(yùn)行時(shí)常量池中闺兢,即編譯期間產(chǎn)生的字面量茂缚、符號(hào)引用。類(lèi)的加載過(guò)程中的鏈接部分的解析步驟就是把符號(hào)引用替換為直接引用屋谭,即把那些描述符(名字)替換為能直接定位到字段脚囊、方法的引用或句柄(地址)。即除了保存class文件中的符號(hào)引用桐磁,還會(huì)把翻譯出來(lái)的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池
同時(shí)凑术,運(yùn)行時(shí)常量池允許在運(yùn)行期間將新的變量放入常量池中。最主要的運(yùn)用便是String類(lèi)的intern()方法:檢查字符串常量池中是否存在String并返回池里的字符串引用所意;若池中不存在淮逊,則將其加入池中,并返回其引用扶踊。這樣做主要是為了避免在堆中不斷地創(chuàng)建新的字符串對(duì)象 - 字符串常量池
https://zhuanlan.zhihu.com/p/160770086
https://www.cnblogs.com/tiancai/p/9321338.html
2.5 堆
所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配泄鹏。堆是垃圾收集器管理的主要區(qū)域,也被稱(chēng)為“GC堆”秧耗;也是我們優(yōu)化最多考慮的地方备籽。
堆可以細(xì)分為:
- 新生代
- Eden 空間
- From Survivor 空間
- To Survivor 空間
- 老年代
- 永久代/元空間
- Java8 以前永久代,受 jvm 管理,java8 以后元空間,直接使用物理內(nèi)存。因此葫笼,
默認(rèn)情況下咸产,元空間的大小僅受本地內(nèi)存限制肌割。
- Java8 以前永久代,受 jvm 管理,java8 以后元空間,直接使用物理內(nèi)存。因此葫笼,
虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,用于存放實(shí)例對(duì)象,幾乎所有的對(duì)象都在堆上分配內(nèi)存,對(duì)象無(wú)法在堆中申請(qǐng)到內(nèi)存的時(shí)候拋出oom異常坛悉,同時(shí)也是GC管理的主要區(qū)域,可通過(guò) -Xmx -Xms參數(shù)來(lái)分別制定最大堆和最小堆
由上圖可以看到承绸,堆由兩部分組成裸影,年輕代和老年代,年輕代又由eden區(qū)和survivor區(qū)組成军熏,年輕代占了1/3的堆內(nèi)存空間轩猩,老年代占據(jù)2/3的堆內(nèi)存空間,eden區(qū)又占據(jù)年輕代8/10的內(nèi)存空間
new出來(lái)的對(duì)象都在eden區(qū)(亞當(dāng)和夏娃在伊甸園造人)
2.6 堆上的GC
新創(chuàng)建的對(duì)象荡澎,要分配內(nèi)存均践,會(huì)先去新生代里面,具體呢就是先看eden區(qū)衔瓮,判斷eden區(qū)內(nèi)存空間夠不夠,夠的話(huà)直接在eden區(qū)分配內(nèi)存抖甘,如果不夠热鞍,此時(shí)就要進(jìn)行一次minorGC,這次gc主要是清理新生代空間衔彻,怎么清理呢薇宠?
比如之前eden區(qū)有10個(gè)對(duì)象,其中1個(gè)對(duì)象還在用艰额,其他9個(gè)沒(méi)用了澄港,我們就把這1個(gè)對(duì)象放到survivor區(qū),剩下的9個(gè)剔除出去
然后再次判斷minorGC完后eden能不能放下柄沮,如果能放下最好回梧,還是放不下的話(huà),我們認(rèn)為這是一個(gè)大對(duì)象祖搓,我們嘗試將它放在老年代
能放下最好狱意,還是放不下的話(huà),那我們就要進(jìn)行一次FullGC拯欧,大屠殺详囤,把老年代存的數(shù)據(jù),新生代存的數(shù)據(jù)镐作,全看一看哪些沒(méi)用了藏姐,全部剔除出去隆箩,再來(lái)看老年代能不能放下,能放下就分配內(nèi)存羔杨, 放不下就OOM內(nèi)存溢出
一句話(huà):在新生代沒(méi)法處理的話(huà)捌臊,才進(jìn)入老年代,所以老年代里面存的總是那些生命力持久的對(duì)象和那些大對(duì)象
minorGC觸發(fā)時(shí)機(jī):eden區(qū)內(nèi)存不夠了
fullGC觸發(fā)時(shí)機(jī):老年代內(nèi)存不夠了
survivor區(qū)有from和to问畅,這兩個(gè)是來(lái)回交換娃属,目的就是總要騰出一個(gè)大白片空間
對(duì)象存活超過(guò)閾值:每次小屠殺minorGC完后,還存活的對(duì)象我們可以認(rèn)為它年齡大了一歲护姆,如果它15歲了矾端,就把這個(gè)對(duì)象搬到老年區(qū)
如果幸存者區(qū)能放下,就放里面了卵皂,如果放不下秩铆,那就會(huì)把這些存活的對(duì)象直接搬家放到老年代里面,這也是一個(gè)流程
即:一次小的minorGC總是要把我們的eden區(qū)清理干凈灯变,能放幸存者區(qū)就放幸存者區(qū)殴玛,放不了了就放老年代
為什么年輕代有2個(gè)survivor
為了保證任何時(shí)候總有一個(gè)survivor是空的。
因?yàn)閷den區(qū)的存活對(duì)象復(fù)制到survivor區(qū)時(shí)添祸,必須保證survivor區(qū)是空的滚粟,如果survivor區(qū)中已有上次復(fù)制的存活對(duì)象時(shí),這次再?gòu)?fù)制的對(duì)象肯定和上次的內(nèi)存地址是不連續(xù)的刃泌,會(huì)產(chǎn)生內(nèi)存碎片凡壤,浪費(fèi)survivor空間。
如果只有一個(gè)survivor區(qū)耙替,第一次GC后亚侠,survivor區(qū)非空,eden區(qū)空俗扇,為了保證第二次能復(fù)制到一個(gè)空的區(qū)域硝烂,新的對(duì)象必須在survivor區(qū)中出生,而survivor區(qū)是很小的铜幽,很容易就會(huì)再次引發(fā)GC滞谢。
而如果有兩個(gè)survivor區(qū),第一次GC后除抛,把eden區(qū)和survivor0區(qū)一起復(fù)制到survivor1區(qū)爹凹,然后清空survivor0和eden區(qū),此時(shí)survivor1非空镶殷,survivor0和eden區(qū)為空禾酱,下一次GC時(shí)把survivor0和survivor1交換,這樣就能保證向survivor區(qū)復(fù)制時(shí)始終都有一個(gè)survivor區(qū)是空的,也就能保證新對(duì)象能始終在eden區(qū)出生了颤陶。
關(guān)于年輕代的GC流程
(1)創(chuàng)建的新對(duì)象都放到eden區(qū)颗管,當(dāng)eden區(qū)滿(mǎn)以后,執(zhí)行一次minor gc滓走,將這次gc結(jié)束后垦江,仍然存活的對(duì)象放入s0即from中去
(2)后面new出來(lái)的對(duì)象繼續(xù)往eden區(qū)放,當(dāng)eden區(qū)再次放滿(mǎn)搅方,將再進(jìn)行一次minor gc比吭,又有一些對(duì)象放入from,這樣from區(qū)最終也有放滿(mǎn)的時(shí)候
(3)隨著eden區(qū)再次滿(mǎn)姨涡,再次往from區(qū)中遷移對(duì)象衩藤,終有一天經(jīng)過(guò)eden區(qū)結(jié)束minor gc再次往from區(qū)放對(duì)象的時(shí)候,放不下了涛漂,from區(qū)滿(mǎn)了赏表,就執(zhí)行from區(qū)的minor gc,from區(qū)回收的時(shí)候匈仗,同樣回收的是沒(méi)有引用的對(duì)象瓢剿,from中仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來(lái)決定去向。年齡達(dá)到一定值(年齡閾值悠轩,可以通過(guò)-XX:MaxTenuringThreshold來(lái)設(shè)置间狂,默認(rèn)是15次)的對(duì)象會(huì)被移動(dòng)到老年代中,沒(méi)有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到“To”區(qū)域火架,而Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到“To”鉴象,
(4)經(jīng)過(guò)這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空距潘。這個(gè)時(shí)候炼列,“From”和“To”會(huì)交換他們的角色只搁,也就是新的“To”就是上次GC前的“From”音比,新的“From”就是上次GC前的“To”
(5)不管怎樣,都會(huì)保證名為T(mén)o的Survivor區(qū)域是空的氢惋。Minor GC會(huì)一直重復(fù)這樣的過(guò)程洞翩,直到“To”區(qū)被填滿(mǎn),“To”區(qū)被填滿(mǎn)之后焰望,在進(jìn)行一次GC的時(shí)候骚亿,會(huì)將所有對(duì)象移動(dòng)到年老代中⌒芾担總有一天老年代也會(huì)放滿(mǎn)来屠,到時(shí)候就會(huì)觸發(fā)老年代GC-full gc