前言
前一陣子在公司內(nèi)部做了一次技術(shù)分享尘盼,主要講的就是JVM核心知識(shí)购对。由于JVM涉及的知識(shí)太多太廣岂座,所以我就以個(gè)人的經(jīng)驗(yàn)把內(nèi)容做了一下精簡,只保留最核心的內(nèi)容堰汉,并且把核心的內(nèi)容都給抽出來辽社,讓大家記住最重要的部分。現(xiàn)在翘鸭,我把分享的內(nèi)容總結(jié)出來滴铅。(文中如有紕漏,還望您批評指正就乓,謝謝)
什么是JVM汉匙?
JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫拱烁,是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。由一套字節(jié)碼指令集噩翠、一組寄存器戏自、一個(gè)棧、一個(gè)垃圾回收堆和一個(gè)存儲(chǔ)方法域等組成伤锚。JVM屏蔽了與操作系統(tǒng)平臺(tái)相關(guān)的信息浦妄,使得Java程序只需要生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),就可在多種平臺(tái)上不加修改的運(yùn)行见芹,這也是Java能夠“一次編譯剂娄,到處運(yùn)行的”原因。
Java文件被加載到JVM的過程
Java類的加載
1.四大主動(dòng)引用
調(diào)用類的構(gòu)造方法 new Test();
調(diào)用類的靜態(tài)變量玄呛、靜態(tài)方法已經(jīng)反射
初始化子類(java規(guī)定:初始化子類的時(shí)候會(huì)先初始化其父類阅懦,所以父類會(huì)被加載)
-
含有main方法類會(huì)被提前加載
注意:調(diào)用常量不會(huì)引起類的加載
2.類的加載過程
類加載過程都做好了什么?
準(zhǔn)備:給靜態(tài)變量開辟內(nèi)存空間徘铝,同時(shí)設(shè)置初始化的值(注意:這里不是代碼設(shè)置的初始化值耳胎,而是靜態(tài)默認(rèn)的缺省值,比如 int的默認(rèn)值是0)
解析:把常量的符號(hào)引用改成直接引用(直接引用即為內(nèi)存地址)
初始化:執(zhí)行靜態(tài)變量和靜態(tài)代碼塊(當(dāng)存在多個(gè)靜態(tài)變量和靜態(tài)代碼塊的時(shí)候惕它,按照從上到下的順序執(zhí)行)
總結(jié):當(dāng)一個(gè)類被主動(dòng)引用的時(shí)候怕午,會(huì)進(jìn)行類的加載,類的加載過程中會(huì)進(jìn)行靜態(tài)變量的初始化和靜態(tài)代碼塊的執(zhí)行淹魄,并且會(huì)把常量的符號(hào)引用替換成直接引用郁惜。整個(gè)類的加載過程網(wǎng)上一搜有一大堆,黑子加粗的是需要大家必須知道和記住甲锡。
為什么靜態(tài)方法中不能引入非靜態(tài)變量兆蕉?
答:當(dāng)類被加載的時(shí)候,非靜態(tài)變量還沒有得到初始化缤沦,而靜態(tài)方法不需要擁有對象實(shí)例就可以得到執(zhí)行虎韵,所以靜態(tài)方法中不能引用非靜態(tài)變量。
為什么可以使用 private static final Singleton instance = new Singleton();這種方式做單例模式缸废?
答:因?yàn)轭愔粫?huì)被初始化加載一次包蓝,當(dāng)類被加載的時(shí)候靜態(tài)變量會(huì)得到初始化,Java的枚舉類就是利用這種方式企量,里面的枚舉類型的數(shù)據(jù)就是靜態(tài)常量
類加載器
啟動(dòng)類加載器: 負(fù)責(zé)加載存放在
<JAVA_HOME>\lib
目錄中的類测萎;被-Xbootclasspath
參數(shù)所指定路徑中、并且是被虛擬機(jī)識(shí)別的類庫擴(kuò)展類加載器:負(fù)責(zé)加載
<JAVA_HOME>\lib\ext
目錄中的類庫梁钾;被java.ext.dirs
系統(tǒng)變量所指定的路徑中的所有類庫-
應(yīng)用類加載器:負(fù)責(zé)加載 用戶類路徑(
ClassPath
)上所指定的類庫(如無特殊指定绳泉,我們的寫的在項(xiàng)目里寫的java代碼都是被這個(gè)類加載器進(jìn)行加載的)
類加載器.png
4.雙親委派模型
當(dāng)一個(gè)類被加載的時(shí)候逊抡,所有的類加載器都會(huì)把加載任務(wù)交給父加載器姆泻,結(jié)合上這張圖來看零酪,也就說雙親委派模型最后都會(huì)交給啟動(dòng)類加載器進(jìn)行加載,只有當(dāng)父類加載器加載不了的時(shí)候才會(huì)交給子類進(jìn)行加載拇勃。
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1);
if (var4 == null) {
long var5 = System.nanoTime();
try {
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false);//先讓父類進(jìn)行加載
} else {
var4 = this.findBootstrapClassOrNull(var1);//當(dāng)parent為null的時(shí)候使用啟動(dòng)類加載器加載
}
} catch (ClassNotFoundException var10) {
}
if (var4 == null) {
long var7 = System.nanoTime();
var4 = this.findClass(var1);//當(dāng)父類加載不了的時(shí)候才會(huì)交給子類進(jìn)行加載
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}
優(yōu)點(diǎn):保證了類的唯一性
注意:雙親委派模型是java推薦的一種技術(shù)模型四苇,不是自定義ClassLoader加載器必須遵守的,另外這里的雙親指的就是父類方咆,別問我我也不知道為啥非要叫雙親
JVM內(nèi)存結(jié)構(gòu)
運(yùn)行時(shí)數(shù)據(jù)區(qū)
JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)主要5個(gè)部分組成月腋,分別是方法區(qū)、程序計(jì)數(shù)器瓣赂、方法棧榆骚、本地方法棧以及堆內(nèi)存。其中用紅色標(biāo)注的堆內(nèi)存和方法區(qū)是線程共享煌集,剩下的三個(gè)是線程私有的妓肢。
方法區(qū)
方法區(qū),又叫永久代苫纤,jdk1.6以后也叫元空間碉钠。是JVM內(nèi)存區(qū)域中比較穩(wěn)定的一塊,很少發(fā)生GC卷拘。在方法區(qū)中存放類信息喊废、靜態(tài)變量以及常量等信息。方法區(qū)是線程共享的栗弟,存在線程安全問題污筷。
棧內(nèi)存
也叫線程棧,每一個(gè)線程都有線程棧乍赫,每一個(gè)方法都是一個(gè)棧幀颓屑,在方法從被調(diào)用到結(jié)束的過程就是對應(yīng)棧幀入棧以及出棧的過程。在棧幀中又有4個(gè)區(qū)域
- 局部變量表:存放方法中的局部變量
- 操作數(shù)棧:執(zhí)行方法中的各種操作耿焊,例如:賦值之類的操作
- 動(dòng)態(tài)鏈接:把方法中的符號(hào)引用轉(zhuǎn)化為直接引用(直接引用即內(nèi)存地址)
- 方法出口:記錄返回值以
及程序當(dāng)前執(zhí)行的位置
本地方法棧
同方法棧類似揪惦,存放的是本地調(diào)用方法(即native修飾的,如public static native void yield();
程序計(jì)數(shù)器
線程私有罗侯,每個(gè)線程會(huì)有一個(gè)區(qū)域存放程序計(jì)數(shù)器
作用:記錄所在線程執(zhí)行到了哪一步
場景:當(dāng)某個(gè)所有的線程的時(shí)間片被搶走時(shí)器腋,過了一段時(shí)間再被恢復(fù)以后,該線程的代碼不會(huì)從頭執(zhí)行钩杰,而是會(huì)按照程序計(jì)數(shù)器所記錄的位置執(zhí)行
堆內(nèi)存
運(yùn)行時(shí)數(shù)據(jù)區(qū)中棧內(nèi)存最大纫塌,線程共線,是主要發(fā)生GC的位置讲弄,存放的是Java實(shí)例對象措左,也就是說所有通過new的對象都存在這里。在堆內(nèi)存中避除,有劃分了成了新生代和老年代怎披,默認(rèn)比例1:2胸嘁,在新生代中又劃分為了eden、sur0和sur1,默認(rèn)的比例是8:1:1凉逛,所有剛new的出來的對象都會(huì)放到eden區(qū)域中性宏。
總結(jié)
JVM內(nèi)存結(jié)構(gòu)是非常重要的一個(gè)部分,有幾個(gè)關(guān)鍵的點(diǎn)一定要記鬃捶伞:
所有new的對象一定存在堆內(nèi)存中
-
非靜態(tài)成員變量存在堆內(nèi)存中毫胜,局部變量都在棧內(nèi)存中
通過代碼再來加固一下
public class Test {
public static User user = new User();//static修飾 user這個(gè)實(shí)例存在方法區(qū) new User()生成的對象在堆區(qū),此時(shí)user引用了堆區(qū)的對象
private int a = 10;//a存在了堆區(qū)诬辈,引用了棧里面的整型變量10
private Date date = new Date();//date存在堆區(qū)酵使,new Date()生成的對象也在堆區(qū),date引用了生成的對象
private int compute() {
int a = 1;
int b = 2;
User user = new User();//注意此時(shí)user是局部變量焙糟,所以存在棧區(qū)凝化,而new User()生成的對象在堆區(qū),此時(shí)棧區(qū)的user引用了堆區(qū)生成的對象
int c = (a + b) * 10;
int d = com();
return c;
}
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.compute());
}
}
垃圾回收
垃圾回收即GC酬荞,剛才講堆內(nèi)存的時(shí)候搓劫,提到了堆內(nèi)存是存放的java對象,是主要發(fā)生GC的位置混巧。
這里需要注意的是枪向,垃圾回收器在判斷一個(gè)對象是否存活時(shí)用的是可達(dá)性分析法∵值常可達(dá)性分析法是判斷一個(gè)對象有沒有被GC ROOT直接或者間接持有秘蛔,只要被GC ROOT直接或者間接持有,那么該對象就是存活狀態(tài)傍衡,或者即為死亡深员。
GC ROOT:
1.方法棧(局部變量表)中引用的對象
2.本地方法棧 中 JNI引用的對象
3.方法區(qū) 中常量、類靜態(tài)屬性引用的對象
總結(jié)
本文主要講了類的加載過程蛙埂、JVM的內(nèi)存結(jié)構(gòu)以及垃圾回收倦畅。由于我個(gè)人讀過很多JVM的文章都寫得太多太詳盡,反正讀多了容易掌握不到重點(diǎn)绣的,而本文就是對核心知識(shí)一個(gè)總結(jié)叠赐,雖然不夠詳盡,但是無論是工作還是面試當(dāng)中應(yīng)該都夠用了屡江。如果文章有錯(cuò)誤的內(nèi)容或者漏講了一些重要的知識(shí)點(diǎn)芭概,歡迎大家在評論區(qū)給我留言,謝謝~