前言
只有光頭才能變強(qiáng)
JVM在準(zhǔn)備面試的時(shí)候就有看了姑曙,一直沒(méi)時(shí)間寫(xiě)筆記。現(xiàn)在到了一家公司實(shí)習(xí)伤靠,閑的時(shí)候就寫(xiě)寫(xiě),刷刷JVM博客授瘦,刷刷電子書(shū)醋界。
學(xué)習(xí)JVM的目的也很簡(jiǎn)單:
- 能夠知道JVM是什么,為我們干了什么提完,具體是怎么干的形纺。能夠理解到一些初學(xué)時(shí)不懂的東西
- 在面試的時(shí)候有談資
- 能裝逼
(圖片來(lái)源:https://zhuanlan.zhihu.com/p/25511795,侵刪)
聲明:全文默認(rèn)指的是HotSpot VM
一、簡(jiǎn)單聊聊JVM
1.1先來(lái)看看簡(jiǎn)單的Java程序
現(xiàn)在我有一個(gè)JavaBean:
public class Java3y {
// 姓名
private String name;
// 年齡
private int age;
//.....各種get/set方法/toString
}
一個(gè)測(cè)試類(lèi):
public class Java3yTest {
public static void main(String[] args) {
Java3y java3y = new Java3y();
java3y.setName("Java3y");
System.out.println(java3y);
}
}
我們?cè)诔鯇W(xué)的時(shí)候肯定用過(guò)javac
來(lái)編譯.java
文件代碼徒欣,用過(guò)java
命令來(lái)執(zhí)行編譯后生成的.class
文件逐样。
Java源文件:
在使用IDE點(diǎn)擊運(yùn)行的時(shí)候其實(shí)就是將這兩個(gè)命令結(jié)合起來(lái)了(編譯并運(yùn)行),方便我們開(kāi)發(fā)打肝。
生成class文件
解析class文件得到結(jié)果
1.2編譯過(guò)程
.java
文件是由Java源碼編譯器(上述所說(shuō)的javac.exe)來(lái)完成脂新,流程圖如下所示:
Java源碼編譯由以下三個(gè)過(guò)程組成:
- 分析和輸入到符號(hào)表
- 注解處理
- 語(yǔ)義分析和生成class文件
1.2.1編譯時(shí)期-語(yǔ)法糖
語(yǔ)法糖可以看做是編譯器實(shí)現(xiàn)的一些“小把戲”,這些“小把戲”可能會(huì)使得效率“大提升”粗梭。
最值得說(shuō)明的就是泛型了争便,這個(gè)語(yǔ)法糖可以說(shuō)我們是經(jīng)常會(huì)使用到的!
- 泛型只會(huì)在Java源碼中存在断医,編譯過(guò)后會(huì)被替換為原來(lái)的原生類(lèi)型(Raw Type滞乙,也稱(chēng)為裸類(lèi)型)了。這個(gè)過(guò)程也被稱(chēng)為:泛型擦除鉴嗤。
有了泛型這顆語(yǔ)法糖以后:
- 代碼更加簡(jiǎn)潔【不用強(qiáng)制轉(zhuǎn)換】
- 程序更加健壯【只要編譯時(shí)期沒(méi)有警告斩启,那么運(yùn)行時(shí)期就不會(huì)出現(xiàn)ClassCastException異常】
- 可讀性和穩(wěn)定性【在編寫(xiě)集合的時(shí)候醉锅,就限定了類(lèi)型】
了解泛型更多的知識(shí):
1.3JVM實(shí)現(xiàn)跨平臺(tái)
至此兔簇,我們通過(guò)javac.exe
編譯器編譯我們的.java
源代碼文件生成出.class
文件了!
這些.class
文件很明顯是不能直接運(yùn)行的,它不像C語(yǔ)言(編譯cpp后生成exe文件直接運(yùn)行)
這些.class
文件是交由JVM來(lái)解析運(yùn)行垄琐!
- JVM是運(yùn)行在操作系統(tǒng)之上的边酒,每個(gè)操作系統(tǒng)的指令是不同的,而JDK是區(qū)分操作系統(tǒng)的此虑,只要你的本地系統(tǒng)裝了JDK甚纲,這個(gè)JDK就是能夠和當(dāng)前系統(tǒng)兼容的口锭。
- 而class字節(jié)碼運(yùn)行在JVM之上朦前,所以不用關(guān)心class字節(jié)碼是在哪個(gè)操作系統(tǒng)編譯的,只要符合JVM規(guī)范鹃操,那么韭寸,這個(gè)字節(jié)碼文件就是可運(yùn)行的。
- 所以Java就做到了跨平臺(tái)--->一次編譯荆隘,到處運(yùn)行恩伺!
1.4class文件和JVM的恩怨情仇
1.4.1類(lèi)的加載時(shí)機(jī)
現(xiàn)在我們例子中生成的兩個(gè).class
文件都會(huì)直接被加載到JVM中嗎?椰拒?
虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(class文件加載到JVM中):
- 創(chuàng)建類(lèi)的實(shí)例(new 的方式)晶渠。訪問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值燃观,調(diào)用類(lèi)的靜態(tài)方法
- 反射的方式
- 初始化某個(gè)類(lèi)的子類(lèi)褒脯,則其父類(lèi)也會(huì)被初始化
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi),直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類(lèi)(包含main方法的那個(gè)類(lèi))
- 當(dāng)使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí)(....)
所以說(shuō):
- Java類(lèi)的加載是動(dòng)態(tài)的缆毁,它并不會(huì)一次性將所有類(lèi)全部加載后再運(yùn)行番川,而是保證程序運(yùn)行的基礎(chǔ)類(lèi)(像是基類(lèi))完全加載到j(luò)vm中,至于其他類(lèi)脊框,則在需要的時(shí)候才加載颁督。這當(dāng)然就是為了節(jié)省內(nèi)存開(kāi)銷(xiāo)。
1.4.2如何將類(lèi)加載到j(luò)vm
class文件是通過(guò)類(lèi)的加載器裝載到j(luò)vm中的浇雹!
Java默認(rèn)有三種類(lèi)加載器:
各個(gè)加載器的工作責(zé)任:
- 1)Bootstrap ClassLoader:負(fù)責(zé)加載$JAVA_HOME中jre/lib/rt.jar里所有的class沉御,由C++實(shí)現(xiàn),不是ClassLoader子類(lèi)
- 2)Extension ClassLoader:負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包昭灵,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
- 3)App ClassLoader:負(fù)責(zé)記載classpath中指定的jar包及目錄中class
工作過(guò)程:
- 1吠裆、當(dāng)AppClassLoader加載一個(gè)class時(shí),它首先不會(huì)自己去嘗試加載這個(gè)類(lèi)虎锚,而是把類(lèi)加載請(qǐng)求委派給父類(lèi)加載器ExtClassLoader去完成她我。
- 2、當(dāng)ExtClassLoader加載一個(gè)class時(shí)咒林,它首先也不會(huì)自己去嘗試加載這個(gè)類(lèi)买羞,而是把類(lèi)加載請(qǐng)求委派給BootStrapClassLoader去完成。
- 3柱徙、如果BootStrapClassLoader加載失敾和馈(例如在$JAVA_HOME/jre/lib里未查找到該class)奇昙,會(huì)使用ExtClassLoader來(lái)嘗試加載;
- 4敌完、若ExtClassLoader也加載失敗储耐,則會(huì)使用AppClassLoader來(lái)加載
- 5、如果AppClassLoader也加載失敗滨溉,則會(huì)報(bào)出異常ClassNotFoundException
其實(shí)這就是所謂的雙親委派模型什湘。簡(jiǎn)單來(lái)說(shuō):如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi)晦攒,而是把請(qǐng)求委托給父加載器去完成闽撤,依次向上。
好處:
- 防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼(安全性角度)
特別說(shuō)明:
- 類(lèi)加載器在成功加載某個(gè)類(lèi)之后脯颜,會(huì)把得到的
java.lang.Class
類(lèi)的實(shí)例緩存起來(lái)哟旗。下次再請(qǐng)求加載該類(lèi)的時(shí)候,類(lèi)加載器會(huì)直接使用緩存的類(lèi)的實(shí)例栋操,而不會(huì)嘗試再次加載闸餐。
1.4.2類(lèi)加載詳細(xì)過(guò)程
加載器加載到j(luò)vm中,接下來(lái)其實(shí)又分了好幾個(gè)步驟:
- 加載矾芙,查找并加載類(lèi)的二進(jìn)制數(shù)據(jù)舍沙,在Java堆中也創(chuàng)建一個(gè)java.lang.Class類(lèi)的對(duì)象。
- 連接蠕啄,連接又包含三塊內(nèi)容:驗(yàn)證场勤、準(zhǔn)備、初始化歼跟。
- 1)驗(yàn)證和媳,文件格式、元數(shù)據(jù)哈街、字節(jié)碼留瞳、符號(hào)引用驗(yàn)證;
- 2)準(zhǔn)備骚秦,為類(lèi)的靜態(tài)變量分配內(nèi)存她倘,并將其初始化為默認(rèn)值;
- 3)解析作箍,把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用
- 初始化硬梁,為類(lèi)的靜態(tài)變量賦予正確的初始值。
1.4.3JIT即時(shí)編輯器
一般我們可能會(huì)想:JVM在加載了這些class文件以后胞得,針對(duì)這些字節(jié)碼荧止,逐條取出,逐條執(zhí)行-->解析器解析。
但如果是這樣的話跃巡,那就太慢了危号!
我們的JVM是這樣實(shí)現(xiàn)的:
- 就是把這些Java字節(jié)碼重新編譯優(yōu)化,生成機(jī)器碼素邪,讓CPU直接執(zhí)行外莲。這樣編出來(lái)的代碼效率會(huì)更高。
- 編譯也是要花費(fèi)時(shí)間的兔朦,我們一般對(duì)熱點(diǎn)代碼做編譯偷线,非熱點(diǎn)代碼直接解析就好了。
熱點(diǎn)代碼解釋?zhuān)阂缓嬲馈⒍啻握{(diào)用的方法淋昭。二、多次執(zhí)行的循環(huán)體
使用熱點(diǎn)探測(cè)來(lái)檢測(cè)是否為熱點(diǎn)代碼安接,熱點(diǎn)探測(cè)有兩種方式:
- 采樣
- 計(jì)數(shù)器
目前HotSpot使用的是計(jì)數(shù)器的方式,它為每個(gè)方法準(zhǔn)備了兩類(lèi)計(jì)數(shù)器:
- 方法調(diào)用計(jì)數(shù)器(Invocation Counter)
- 回邊計(jì)數(shù)器(Back EdgeCounter)英融。
- 在確定虛擬機(jī)運(yùn)行參數(shù)的前提下盏檐,這兩個(gè)計(jì)數(shù)器都有一個(gè)確定的閾值,當(dāng)計(jì)數(shù)器超過(guò)閾值溢出了驶悟,就會(huì)觸發(fā)JIT編譯胡野。
1.4.4回到例子中
按我們程序來(lái)走,我們的Java3yTest.class
文件會(huì)被AppClassLoader加載器(因?yàn)镋xtClassLoader和BootStrap加載器都不會(huì)加載它[雙親委派模型])加載到JVM中痕鳍。
隨后發(fā)現(xiàn)了要使用Java3y這個(gè)類(lèi)硫豆,我們的Java3y.class
文件會(huì)被AppClassLoader加載器(因?yàn)镋xtClassLoader和BootStrap加載器都不會(huì)加載它[雙親委派模型])加載到JVM中
詳情參考:
- https://www.mrsssswan.club/2018/06/30/jvm-start1/---淺解JVM加載class文件
- https://zhuanlan.zhihu.com/p/28476709---JVM雜談之JIT
擴(kuò)展閱讀:
- https://www.ibm.com/developerworks/cn/java/j-lo-classloader/---深入探討 Java 類(lèi)加載器
- https://www.ibm.com/developerworks/cn/java/j-lo-just-in-time/---深入淺出 JIT 編譯器
- https://www.zhihu.com/question/46719811---Java 類(lèi)加載器(ClassLoader)的實(shí)際使用場(chǎng)景有哪些?
1.5類(lèi)加載完以后JVM干了什么笼呆?
在類(lèi)加載檢查通過(guò)后熊响,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。
1.5.1JVM的內(nèi)存模型
首先我們來(lái)了解一下JVM的內(nèi)存模型的怎么樣的:
- 基于jdk1.8畫(huà)的JVM的內(nèi)存模型--->我畫(huà)得比較細(xì)诗赌。
簡(jiǎn)單看了一下內(nèi)存模型汗茄,簡(jiǎn)單看看每個(gè)區(qū)域究竟存儲(chǔ)的是什么(干的是什么):
- 堆:存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存
- 虛擬機(jī)棧:虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表铭若、操作棧洪碳、動(dòng)態(tài)鏈接、方法出口等信息
- 本地方法棧:本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)叼屠。
- 方法區(qū):存儲(chǔ)已被虛擬機(jī)加載的類(lèi)元數(shù)據(jù)信息(元空間)
- 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器
1.5.2例子中的流程
我來(lái)宏觀簡(jiǎn)述一下我們的例子中的工作流程:
- 1瞳腌、通過(guò)
java.exe
運(yùn)行Java3yTest.class
,隨后被加載到JVM中镜雨,元空間存儲(chǔ)著類(lèi)的信息(包括類(lèi)的名稱(chēng)嫂侍、方法信息、字段信息..)。 - 2吵冒、然后JVM找到Java3yTest的主函數(shù)入口(main)纯命,為main函數(shù)創(chuàng)建棧幀,開(kāi)始執(zhí)行main函數(shù)
- 3痹栖、main函數(shù)的第一條命令是
Java3y java3y = new Java3y();
就是讓JVM創(chuàng)建一個(gè)Java3y對(duì)象亿汞,但是這時(shí)候方法區(qū)中沒(méi)有Java3y類(lèi)的信息,所以JVM馬上加載Java3y類(lèi)揪阿,把Java3y類(lèi)的類(lèi)型信息放到方法區(qū)中(元空間) - 4疗我、加載完Java3y類(lèi)之后,Java虛擬機(jī)做的第一件事情就是在堆區(qū)中為一個(gè)新的Java3y實(shí)例分配內(nèi)存, 然后調(diào)用構(gòu)造函數(shù)初始化Java3y實(shí)例南捂,這個(gè)Java3y實(shí)例持有著指向方法區(qū)的Java3y類(lèi)的類(lèi)型信息(其中包含有方法表吴裤,java動(dòng)態(tài)綁定的底層實(shí)現(xiàn))的引用
- 5、當(dāng)使用
java3y.setName("Java3y");
的時(shí)候溺健,JVM根據(jù)java3y引用找到Java3y對(duì)象麦牺,然后根據(jù)Java3y對(duì)象持有的引用定位到方法區(qū)中Java3y類(lèi)的類(lèi)型信息的方法表,獲得setName()
函數(shù)的字節(jié)碼的地址 - 6鞭缭、為
setName()
函數(shù)創(chuàng)建棧幀剖膳,開(kāi)始運(yùn)行setName()
函數(shù)
從微觀上其實(shí)還做了很多東西,正如上面所說(shuō)的類(lèi)加載過(guò)程(加載-->連接(驗(yàn)證岭辣,準(zhǔn)備吱晒,解析)-->初始化),在類(lèi)加載完之后jvm為其分配內(nèi)存(分配內(nèi)存中也做了非常多的事)沦童。由于這些步驟并不是一步一步往下走仑濒,會(huì)有很多的“混沌bootstrap”的過(guò)程,所以很難描述清楚偷遗。
- 擴(kuò)展閱讀(先有Class對(duì)象還是先有Object):https://www.zhihu.com/question/30301819
參考資料:
- http://www.cnblogs.com/qiumingcheng/p/5398610.html---Java程序編譯和運(yùn)行的過(guò)程
- https://zhuanlan.zhihu.com/p/25713880---Java JVM 運(yùn)行機(jī)制及基本原理
1.6簡(jiǎn)單聊聊各種常量池
在寫(xiě)這篇文章的時(shí)候墩瞳,原本以為我對(duì)String s = "aaa";
類(lèi)似這些題目已經(jīng)是不成問(wèn)題了,直到我遇到了String.intern()
這樣的方法與諸如String s1 = new String("1") + new String("2");
混合一起用的時(shí)候
- 我發(fā)現(xiàn)鹦肿,我還是太年輕了矗烛。
首先我是先閱讀了美團(tuán)技術(shù)團(tuán)隊(duì)的這篇文章:https://tech.meituan.com/in_depth_understanding_string_intern.html---深入解析String#intern
嗯,然后就懵逼了箩溃。我摘抄一下他的例子:
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
打印結(jié)果是
- jdk7,8下false true
調(diào)換一下位置后:
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
打印結(jié)果為:
- jdk7,8下false false
文章中有很詳細(xì)的解析瞭吃,但我簡(jiǎn)單閱讀了幾次以后還是很懵逼。所以我知道了自己的知識(shí)點(diǎn)還存在漏洞涣旨,后面閱讀了一下R大之前寫(xiě)過(guò)的文章:
- http://rednaxelafx.iteye.com/blog/774673#comments---請(qǐng)別再拿“String s = new String("xyz");創(chuàng)建了多少個(gè)String實(shí)例”來(lái)面試了吧
看完了之后歪架,就更加懵逼了。
后來(lái)霹陡,在zhihu上看到了這個(gè)回答:
- https://www.zhihu.com/question/55994121---Java 中new String("字面量") 中 "字面量" 是何時(shí)進(jìn)入字符串常量池的?
結(jié)合網(wǎng)上資料和自己的思考和蚪,下面整理一下對(duì)常量池的理解~~
1.6.1各個(gè)常量池的情況
針對(duì)于jdk1.7之后:
- 運(yùn)行時(shí)常量池位于堆中
- 字符串常量池位于堆中
常量池存儲(chǔ)的是:
- 字面量(Literal):文本字符串等---->用雙引號(hào)引起來(lái)的字符串字面量都會(huì)進(jìn)這里面
- 符號(hào)引用(Symbolic References)
- 類(lèi)和接口的全限定名(Full Qualified Name)
- 字段的名稱(chēng)和描述符(Descriptor)
- 方法的名稱(chēng)和描述符
常量池(Constant Pool Table)止状,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放--->來(lái)源:深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐(第二版)
現(xiàn)在我們的運(yùn)行時(shí)常量池只是換了一個(gè)位置(原本來(lái)方法區(qū)攒霹,現(xiàn)在在堆中),但可以明確的是:類(lèi)加載后怯疤,常量池中的數(shù)據(jù)會(huì)在運(yùn)行時(shí)常量池中存放!
別人總結(jié)的常量池:
它是Class文件中的內(nèi)容催束,還不是運(yùn)行時(shí)的內(nèi)容集峦,不要理解它是個(gè)池子,其實(shí)就是Class文件中的字節(jié)碼指
HotSpot VM里抠刺,記錄interned string的一個(gè)全局表叫做StringTable塔淤,它本質(zhì)上就是個(gè)HashSet<String>。注意它只存儲(chǔ)對(duì)java.lang.String實(shí)例的引用速妖,而不存儲(chǔ)String對(duì)象的內(nèi)容
字符串常量池只存儲(chǔ)引用高蜂,不存儲(chǔ)內(nèi)容!
再來(lái)看一下我們的intern方法:
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
- 如果常量池中存在當(dāng)前字符串罕容,那么直接返回常量池中它的引用备恤。
- 如果常量池中沒(méi)有此字符串, 會(huì)將此字符串引用保存到常量池中后, 再直接返回該字符串的引用!
1.6.2解析題目
本來(lái)打算寫(xiě)注釋的方式來(lái)解釋的杀赢,但好像挺難說(shuō)清楚的烘跺。我還是畫(huà)圖吧...
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);// false
System.out.println("-----------關(guān)注公眾號(hào):Java3y-------------");
}
第一句:String s = new String("1");
第二句:s.intern();
發(fā)現(xiàn)字符串常量池中已經(jīng)存在"1"字符串對(duì)象,直接返回字符串常量池中對(duì)堆的引用(但沒(méi)有接收)-->此時(shí)s引用還是指向著堆中的對(duì)象
第三句:String s2 = "1";
發(fā)現(xiàn)字符串常量池已經(jīng)保存了該對(duì)象的引用了脂崔,直接返回字符串常量池對(duì)堆中字符串的引用
很容易看到,兩條引用是不一樣的梧喷!所以返回false砌左。
public static void main(String[] args) {
System.out.println("-----------關(guān)注公眾號(hào):Java3y-------------");
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4); // true
}
第一句:String s3 = new String("1") + new String("1");
注意:此時(shí)"11"對(duì)象并沒(méi)有在字符串常量池中保存引用。
第二句:s3.intern();
發(fā)現(xiàn)"11"對(duì)象并沒(méi)有在字符串常量池中铺敌,于是將"11"對(duì)象在字符串常量池中保存當(dāng)前字符串的引用汇歹,并返回當(dāng)前字符串的引用(但沒(méi)有接收)
第三句:String s4 = "11";
發(fā)現(xiàn)字符串常量池已經(jīng)存在引用了,直接返回(拿到的也是與s3相同指向的引用)
根據(jù)上述所說(shuō)的:最后會(huì)返回true~~~
如果還是不太清楚的同學(xué)偿凭,可以試著接收一下intern()
方法的返回值产弹,再看看上述的圖,應(yīng)該就可以理解了弯囊。
下面的就由各位來(lái)做做痰哨,看是不是掌握了:
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);//false
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);//false
}
還有:
public static void main(String[] args) {
String s1 = new String("he") + new String("llo");
String s2 = new String("h") + new String("ello");
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1 == s3);// true
System.out.println(s1 == s4);// true
}
1.7GC垃圾回收
可以說(shuō)GC垃圾回收是JVM中一個(gè)非常重要的知識(shí)點(diǎn),應(yīng)該非常詳細(xì)去講解的匾嘱。但在我學(xué)習(xí)的途中斤斧,我已經(jīng)發(fā)現(xiàn)了有很好的文章去講解垃圾回收的了。
所以霎烙,這里我只簡(jiǎn)單介紹一下垃圾回收的東西撬讽,詳細(xì)的可以到下面的面試題中查閱和最后給出相關(guān)的資料閱
讀吧~
1.7.1JVM垃圾回收簡(jiǎn)單介紹
在C++中蕊连,我們知道創(chuàng)建出的對(duì)象是需要手動(dòng)去delete掉的。我們Java程序運(yùn)行在JVM中游昼,JVM可以幫我們“自動(dòng)”回收不需要的對(duì)象甘苍,對(duì)我們來(lái)說(shuō)是十分方便的。
雖然說(shuō)“自動(dòng)”回收了我們不需要的對(duì)象烘豌,但如果我們想變強(qiáng)载庭,就要變禿..不對(duì),就要去了解一下它究竟是怎么干的扇谣,理論的知識(shí)有哪些昧捷。
首先,JVM回收的是垃圾罐寨,垃圾就是我們程序中已經(jīng)是不需要的了靡挥。垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要確定這些對(duì)象之中哪些還“存活”著鸯绿,哪些已經(jīng)“死去”跋破。判斷哪些對(duì)象“死去”常用有兩種方式:
- 引用計(jì)數(shù)法-->這種難以解決對(duì)象之間的循環(huán)引用的問(wèn)題
- 可達(dá)性分析算法-->主流的JVM采用的是這種方式
現(xiàn)在已經(jīng)可以判斷哪些對(duì)象已經(jīng)“死去”了,我們現(xiàn)在要對(duì)這些“死去”的對(duì)象進(jìn)行回收瓶蝴,回收也有好幾種算法:
- 標(biāo)記-清除算法
- 復(fù)制算法
- 標(biāo)記-整理算法
- 分代收集算法
(這些算法詳情可看下面的面試題內(nèi)容)~
無(wú)論是可達(dá)性分析算法毒返,還是垃圾回收算法,JVM使用的都是準(zhǔn)確式GC舷手。JVM是使用一組稱(chēng)為OopMap的數(shù)據(jù)結(jié)構(gòu)拧簸,來(lái)存儲(chǔ)所有的對(duì)象引用(這樣就不用遍歷整個(gè)內(nèi)存去查找了,空間換時(shí)間)男窟。
并且不會(huì)將所有的指令都生成OopMap盆赤,只會(huì)在安全點(diǎn)上生成OopMap,在安全區(qū)域上開(kāi)始GC歉眷。
- 在OopMap的協(xié)助下牺六,HotSpot可以快速且準(zhǔn)確地完成GC Roots枚舉(可達(dá)性分析)。
上面所講的垃圾收集算法只能算是方法論汗捡,落地實(shí)現(xiàn)的是垃圾收集器:
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- G1收集器
上面這些收集器大部分是可以互相組合使用的
1.8JVM參數(shù)與調(diào)優(yōu)
很多做過(guò)JavaWeb項(xiàng)目(ssh/ssm)這樣的同學(xué)可能都會(huì)遇到過(guò)OutOfMemory這樣的錯(cuò)誤淑际。一般解決起來(lái)也很方便,在啟動(dòng)的時(shí)候加個(gè)參數(shù)就行了扇住。
上面也說(shuō)了很多關(guān)于JVM的東西--->JVM對(duì)內(nèi)存的劃分啊春缕,JVM各種的垃圾收集器啊。
內(nèi)存的分配的大小啊台囱,使用哪個(gè)收集器啊淡溯,這些都可以由我們根據(jù)需求,現(xiàn)實(shí)情況來(lái)指定的簿训,這里就不詳細(xì)說(shuō)了咱娶,等真正用到的時(shí)候才回來(lái)填坑吧~~~~
參考資料:
- http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html---JVM系列三:JVM參數(shù)設(shè)置米间、分析
二、JVM面試題
拿些常見(jiàn)的JVM面試題來(lái)做做膘侮,加深一下理解和查缺補(bǔ)漏:
- 1屈糊、詳細(xì)jvm內(nèi)存模型
- 2、講講什么情況下回出現(xiàn)內(nèi)存溢出琼了,內(nèi)存泄漏逻锐?
- 3、說(shuō)說(shuō)Java線程棧
- 4雕薪、JVM 年輕代到年老代的晉升過(guò)程的判斷條件是什么呢昧诱?
- 5、JVM 出現(xiàn) fullGC 很頻繁所袁,怎么去線上排查問(wèn)題盏档?
- 6、類(lèi)加載為什么要使用雙親委派模式燥爷,有沒(méi)有什么場(chǎng)景是打破了這個(gè)模式蜈亩?
- 7、類(lèi)的實(shí)例化順序
- 8前翎、JVM垃圾回收機(jī)制稚配,何時(shí)觸發(fā)MinorGC等操作
- 9、JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的
- 10港华、各種回收器道川,各自?xún)?yōu)缺點(diǎn),重點(diǎn)CMS立宜、G1
- 11愤惰、各種回收算法
- 12、OOM錯(cuò)誤赘理,stackoverflow錯(cuò)誤,permgen space錯(cuò)誤
題目來(lái)源:
2.1詳細(xì)jvm內(nèi)存模型
根據(jù) JVM 規(guī)范扇单,JVM 內(nèi)存共分為虛擬機(jī)棧商模、堆、方法區(qū)蜘澜、程序計(jì)數(shù)器施流、本地方法棧五個(gè)部分。
具體可能會(huì)聊聊jdk1.7以前的PermGen(永久代)鄙信,替換成Metaspace(元空間)
- 原本永久代存儲(chǔ)的數(shù)據(jù):符號(hào)引用(Symbols)轉(zhuǎn)移到了native heap瞪醋;字面量(interned strings)轉(zhuǎn)移到了java heap;類(lèi)的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap
- Metaspace(元空間)存儲(chǔ)的是類(lèi)的元數(shù)據(jù)信息(metadata)
- 元空間的本質(zhì)和永久代類(lèi)似装诡,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)银受。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中践盼,而是使用本地內(nèi)存。
- 替換的好處:一宾巍、字符串存在永久代中咕幻,容易出現(xiàn)性能問(wèn)題和內(nèi)存溢出耍贾。二流椒、永久代會(huì)為 GC 帶來(lái)不必要的復(fù)雜度,并且回收效率偏低
圖片來(lái)源:https://blog.csdn.net/tophawk/article/details/78704074
參考資料:
2.2講講什么情況下回出現(xiàn)內(nèi)存溢出得湘,內(nèi)存泄漏选浑?
內(nèi)存泄漏的原因很簡(jiǎn)單:
- 對(duì)象是可達(dá)的(一直被引用)
- 但是對(duì)象不會(huì)被使用
常見(jiàn)的內(nèi)存泄漏例子:
public static void main(String[] args) {
Set set = new HashSet();
for (int i = 0; i < 10; i++) {
Object object = new Object();
set.add(object);
// 設(shè)置為空蓝厌,這對(duì)象我不再用了
object = null;
}
// 但是set集合中還維護(hù)這obj的引用,gc不會(huì)回收object對(duì)象
System.out.println(set);
}
解決這個(gè)內(nèi)存泄漏問(wèn)題也很簡(jiǎn)單古徒,將set設(shè)置為null拓提,那就可以避免上訴內(nèi)存泄漏問(wèn)題了。其他內(nèi)存泄漏得一步一步分析了描函。
內(nèi)存泄漏參考資料:
內(nèi)存溢出的原因:
- 內(nèi)存泄露導(dǎo)致堆棧內(nèi)存不斷增大崎苗,從而引發(fā)內(nèi)存溢出。
- 大量的jar舀寓,class文件加載胆数,裝載類(lèi)的空間不夠,溢出
- 操作大量的對(duì)象導(dǎo)致堆內(nèi)存空間已經(jīng)用滿(mǎn)了互墓,溢出
- nio直接操作內(nèi)存必尼,內(nèi)存過(guò)大導(dǎo)致溢出
解決:
- 查看程序是否存在內(nèi)存泄漏的問(wèn)題
- 設(shè)置參數(shù)加大空間
- 代碼中是否存在死循環(huán)或循環(huán)產(chǎn)生過(guò)多重復(fù)的對(duì)象實(shí)體、
- 查看是否使用了nio直接操作內(nèi)存篡撵。
參考資料:
2.3說(shuō)說(shuō)線程棧
這里的線程棧應(yīng)該指的是虛擬機(jī)棧吧...
JVM規(guī)范讓每個(gè)Java線程擁有自己的獨(dú)立的JVM棧判莉,也就是Java方法的調(diào)用棧。
當(dāng)方法調(diào)用的時(shí)候育谬,會(huì)生成一個(gè)棧幀券盅。棧幀是保存在虛擬機(jī)棧中的,棧幀存儲(chǔ)了方法的局部變量表膛檀、操作數(shù)棧锰镀、動(dòng)態(tài)連接和方法返回地址等信息
線程運(yùn)行過(guò)程中,只有一個(gè)棧幀是處于活躍狀態(tài)咖刃,稱(chēng)為“當(dāng)前活躍棧幀”泳炉,當(dāng)前活動(dòng)棧幀始終是虛擬機(jī)棧的棧頂元素。
通過(guò)jstack工具查看線程狀態(tài)
參考資料:
- http://wangwengcn.iteye.com/blog/1622195
- https://www.cnblogs.com/Codenewbie/p/6184898.html
- https://blog.csdn.net/u011734144/article/details/60965155
2.4JVM 年輕代到年老代的晉升過(guò)程的判斷條件是什么呢嚎杨?
- 部分對(duì)象會(huì)在From和To區(qū)域中復(fù)制來(lái)復(fù)制去,如此交換15次(由JVM參數(shù)MaxTenuringThreshold決定,這個(gè)參數(shù)默認(rèn)是15),最終如果還是存活,就存入到老年代花鹅。
- 如果對(duì)象的大小大于Eden的二分之一會(huì)直接分配在old,如果old也分配不下枫浙,會(huì)做一次majorGC刨肃,如果小于eden的一半但是沒(méi)有足夠的空間古拴,就進(jìn)行minorgc也就是新生代GC。
- minor gc后之景,survivor仍然放不下斤富,則放到老年代
- 動(dòng)態(tài)年齡判斷 ,大于等于某個(gè)年齡的對(duì)象超過(guò)了survivor空間一半 锻狗,大于等于某個(gè)年齡的對(duì)象直接進(jìn)入老年代
2.5JVM 出現(xiàn) fullGC 很頻繁满力,怎么去線上排查問(wèn)題
這題就依據(jù)full GC的觸發(fā)條件來(lái)做:
- 如果有perm gen的話(jdk1.8就沒(méi)了),要給perm gen分配空間轻纪,但沒(méi)有足夠的空間時(shí)油额,會(huì)觸發(fā)full gc。
- 所以看看是不是perm gen區(qū)的值設(shè)置得太小了刻帚。
-
System.gc()
方法的調(diào)用- 這個(gè)一般沒(méi)人去調(diào)用吧~~~
- 當(dāng)統(tǒng)計(jì)得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間潦嘶,則會(huì)觸發(fā)full gc(這就可以從多個(gè)角度上看了)
- 是不是頻繁創(chuàng)建了大對(duì)象(也有可能eden區(qū)設(shè)置過(guò)小)(大對(duì)象直接分配在老年代中,導(dǎo)致老年代空間不足--->從而頻繁gc)
- 是不是老年代的空間設(shè)置過(guò)小了(Minor GC幾個(gè)對(duì)象就大于老年代的剩余空間了)
2.6類(lèi)加載為什么要使用雙親委派模式崇众,有沒(méi)有什么場(chǎng)景是打破了這個(gè)模式掂僵?
雙親委托模型的重要用途是為了解決類(lèi)載入過(guò)程中的安全性問(wèn)題。
- 假設(shè)有一個(gè)開(kāi)發(fā)者自己編寫(xiě)了一個(gè)名為
java.lang.Object
的類(lèi)顷歌,想借此欺騙JVM∶膛睿現(xiàn)在他要使用自定義ClassLoader
來(lái)加載自己編寫(xiě)的java.lang.Object
類(lèi)。 - 然而幸運(yùn)的是眯漩,雙親委托模型不會(huì)讓他成功芹扭。因?yàn)镴VM會(huì)優(yōu)先在
Bootstrap ClassLoader
的路徑下找到java.lang.Object
類(lèi),并載入它
Java的類(lèi)加載是否一定遵循雙親委托模型赦抖?
- 在實(shí)際開(kāi)發(fā)中舱卡,我們可以通過(guò)自定義ClassLoader,并重寫(xiě)父類(lèi)的loadClass方法队萤,來(lái)打破這一機(jī)制轮锥。
- SPI就是打破了雙親委托機(jī)制的(SPI:服務(wù)提供發(fā)現(xiàn))。SPI資料:
參考資料:
2.7類(lèi)的實(shí)例化順序
- 1. 父類(lèi)靜態(tài)成員和靜態(tài)初始化塊 要尔,按在代碼中出現(xiàn)的順序依次執(zhí)行
- 2. 子類(lèi)靜態(tài)成員和靜態(tài)初始化塊 交胚,按在代碼中出現(xiàn)的順序依次執(zhí)行
- 3. 父類(lèi)實(shí)例成員和實(shí)例初始化塊 ,按在代碼中出現(xiàn)的順序依次執(zhí)行
- 4. 父類(lèi)構(gòu)造方法
- 5. 子類(lèi)實(shí)例成員和實(shí)例初始化塊 盈电,按在代碼中出現(xiàn)的順序依次執(zhí)行
- 6. 子類(lèi)構(gòu)造方法
檢驗(yàn)一下是不是真懂了:
class Dervied extends Base {
private String name = "Java3y";
public Dervied() {
tellName();
printName();
}
public void tellName() {
System.out.println("Dervied tell name: " + name);
}
public void printName() {
System.out.println("Dervied print name: " + name);
}
public static void main(String[] args) {
new Dervied();
}
}
class Base {
private String name = "公眾號(hào)";
public Base() {
tellName();
printName();
}
public void tellName() {
System.out.println("Base tell name: " + name);
}
public void printName() {
System.out.println("Base print name: " + name);
}
}
輸出數(shù)據(jù):
Dervied tell name: null
Dervied print name: null
Dervied tell name: Java3y
Dervied print name: Java3y
第一次做錯(cuò)的同學(xué)點(diǎn)個(gè)贊,加個(gè)關(guān)注不過(guò)分吧(hahaha
2.8JVM垃圾回收機(jī)制杯活,何時(shí)觸發(fā)MinorGC等操作
當(dāng)young gen中的eden區(qū)分配滿(mǎn)的時(shí)候觸發(fā)MinorGC(新生代的空間不夠放的時(shí)候).
2.9JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的
這題不是很明白意思(水平有限...如果知道這題的意思可在評(píng)論區(qū)留言呀~~)
- 因?yàn)榘次业睦斫猓簣?zhí)行fgc是不會(huì)執(zhí)行ygc的呀~~
YGC和FGC是什么
- YGC :對(duì)新生代堆進(jìn)行g(shù)c匆帚。頻率比較高,因?yàn)榇蟛糠謱?duì)象的存活壽命較短旁钧,在新生代里被回收吸重。性能耗費(fèi)較小互拾。
- FGC :全堆范圍的gc。默認(rèn)堆空間使用到達(dá)80%(可調(diào)整)的時(shí)候會(huì)觸發(fā)fgc嚎幸。以我們生產(chǎn)環(huán)境為例颜矿,一般比較少會(huì)觸發(fā)fgc,有時(shí)10天或一周左右會(huì)有一次嫉晶。
什么時(shí)候執(zhí)行YGC和FGC
- a.eden空間不足,執(zhí)行 young gc
- b.old空間不足骑疆,perm空間不足,調(diào)用方法
System.gc()
替废,ygc時(shí)的悲觀策略, dump live的內(nèi)存信息時(shí)(jmap –dump:live)箍铭,都會(huì)執(zhí)行full gc
2.10各種回收算法
GC最基礎(chǔ)的算法有三種:
- 標(biāo)記 -清除算法
- 復(fù)制算法
- 標(biāo)記-壓縮算法
- 我們常用的垃圾回收器一般都采用分代收集算法(其實(shí)就是組合上面的算法,不同的區(qū)域使用不同的算法)椎镣。
具體:
- 標(biāo)記-清除算法诈火,“標(biāo)記-清除”(Mark-Sweep)算法,如它的名字一樣状答,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象冷守,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象。
- 復(fù)制算法惊科,“復(fù)制”(Copying)的收集算法拍摇,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊译断。當(dāng)這一塊的內(nèi)存用完了授翻,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉孙咪。
- 標(biāo)記-壓縮算法堪唐,標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理翎蹈,而是讓所有存活的對(duì)象都向一端移動(dòng)淮菠,然后直接清理掉端邊界以外的內(nèi)存
- 分代收集算法,“分代收集”(Generational Collection)算法荤堪,把Java堆分為新生代和老年代合陵,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/li>
2.11各種回收器,各自?xún)?yōu)缺點(diǎn)澄阳,重點(diǎn)CMS拥知、G1
圖來(lái)源于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》,圖中兩個(gè)收集器之間有連線碎赢,說(shuō)明它們可以配合使用.
- Serial收集器低剔,串行收集器是最古老,最穩(wěn)定以及效率高的收集器,但可能會(huì)產(chǎn)生較長(zhǎng)的停頓襟齿,只使用一個(gè)線程去回收姻锁。
- ParNew收集器,ParNew收集器其實(shí)就是Serial收集器的多線程版本猜欺。
- Parallel收集器位隶,Parallel Scavenge收集器類(lèi)似ParNew收集器,Parallel收集器更關(guān)注系統(tǒng)的吞吐量开皿。
- Parallel Old收集器涧黄,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程“標(biāo)記-整理”算法
- CMS收集器副瀑,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器弓熏。它需要消耗額外的CPU和內(nèi)存資源,在CPU和內(nèi)存資源緊張糠睡,CPU較少時(shí)挽鞠,會(huì)加重系統(tǒng)負(fù)擔(dān)。CMS無(wú)法處理浮動(dòng)垃圾狈孔。CMS的“標(biāo)記-清除”算法信认,會(huì)導(dǎo)致大量空間碎片的產(chǎn)生。
- G1收集器均抽,G1 (Garbage-First)是一款面向服務(wù)器的垃圾收集器,主要針對(duì)配備多顆處理器及大容量?jī)?nèi)存的機(jī)器. 以極高概率滿(mǎn)足GC停頓時(shí)間要求的同時(shí),還具備高吞吐量性能特征嫁赏。
2.12stackoverflow錯(cuò)誤,permgen space錯(cuò)誤
stackoverflow錯(cuò)誤主要出現(xiàn):
- 在虛擬機(jī)棧中(線程請(qǐng)求的棧深度大于虛擬機(jī)棧鎖允許的最大深度)
permgen space錯(cuò)誤(針對(duì)jdk之前1.7版本):
- 大量加載class文件
- 常量池內(nèi)存溢出
三油挥、總結(jié)
總的來(lái)說(shuō)潦蝇,JVM在初級(jí)的層面上還是偏理論多,可能要做具體的東西才會(huì)有更深的體會(huì)深寥。這篇主要是入個(gè)門(mén)吧~
這篇文章懶懶散散也算把JVM比較重要的知識(shí)點(diǎn)理了一遍了攘乒,后面打算學(xué)學(xué),寫(xiě)寫(xiě)SpringCloud的東西惋鹅。
參考資料:
- 《深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐(第二版)》
- 純潔的微笑jvm專(zhuān)欄:https://zhuanlan.zhihu.com/p/25511795
- SexyCode jvm專(zhuān)欄:https://blog.csdn.net/column/details/15618.html?&page=1
- javaGC流程:https://blog.csdn.net/yangyang12345555/article/details/79257171
如果文章有錯(cuò)的地方歡迎指正则酝,大家互相交流。習(xí)慣在微信看技術(shù)文章闰集,想要獲取更多的Java資源的同學(xué)沽讹,可以關(guān)注微信公眾號(hào):Java3y。為了大家方便武鲁,剛新建了一下qq群:742919422爽雄,大家也可以去交流交流。謝謝支持了沐鼠!希望能多介紹給其他有需要的朋友
文章的目錄導(dǎo)航: