面试高频!JVM必备教程~
- 請(qǐng)你談?wù)勀銓?duì)JVM的理解?Java8虛擬機(jī)和之間的變化更新?
- JVM類加載器是怎么樣的?有幾種?
- 什么是OOM,什么是StackOverFlowError? 怎么分析?
- JVM常用調(diào)優(yōu)參數(shù)有哪寫(xiě)?
- GC有幾種算法?分別是怎么執(zhí)行的?
- 內(nèi)存快照如何抓取,怎么分析Dump文件?
目錄
- 一、簡(jiǎn)介
- 1. 什么是JVM
- 2. JVM的位置
- 3. JVM、JRE、JDK 的關(guān)系
- 4. 三種JVM
- 二、JVM體系結(jié)構(gòu)
- 三、類加載器
- 1. 作用
- 2. 回顧new對(duì)象的過(guò)程
- 3. 類加載器的等級(jí)
- 1. 啟動(dòng)類(根)加載器
- 2. 拓展類加載器
- 3. 應(yīng)用程序加載器
- 4. 用戶自定義類加載器
- 4. 類加載機(jī)制
- 1. 什么是雙親委派機(jī)制
- 2. 為什么要設(shè)計(jì)這種機(jī)制
- 3. 雙親委派機(jī)制的作用
- 拓展:沙箱安全機(jī)制
- 什么是沙箱?
- Java中的安全模型演進(jìn)
- 組成沙箱的基本組件
- 四、本地方法接口
- 五、PC寄存器
- 六、方法區(qū)
- 1. 方法區(qū)中有啥
- 2. 創(chuàng)建對(duì)象內(nèi)存分析
- 七、棧
- 1、棧中存放啥?
- 2、棧運(yùn)行原理
- 3、堆棧溢出StackOverflowError
- 八、堆
- 1. 堆的分區(qū)圖
- 2. 堆內(nèi)存詳解
- Young 年輕代
- Tenured 老年代
- Perm 元空間
- 3. GC垃圾回收
- 4. 堆內(nèi)存調(diào)優(yōu)
- 1. 查看并設(shè)置JVM堆內(nèi)存
- 2. 什么是OOM
- 3. 怎么排除OOM錯(cuò)誤?
- 1. 嘗試擴(kuò)大堆內(nèi)存看結(jié)果
- 2. 利用內(nèi)存快照工具JProfiler
- 3. 什么是Dump文件?如何分析?
- 附:安裝Jprofiler教程
- 1. idea中安裝插件
- 2. 下載客戶端
- 3. 安裝客戶端
- 4. IDEA中設(shè)置
- 九、GC算法
- 1. 引用計(jì)數(shù)算法(很少使用)
- 2. 復(fù)制算法
- 3. 標(biāo)記–清除算法
- 4. 標(biāo)記–壓縮算法
- 總結(jié)
一、簡(jiǎn)介
1. 什么是JVM
JVM是
Java Virtual Machine(Java虛擬機(jī))的縮寫(xiě),JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),是通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。
百度的解釋云里霧里,對(duì)于我們Java程序員,說(shuō)白了就是:
- JVM本質(zhì)上是一個(gè)
程序/軟件,它能識(shí)別.class字節(jié)碼文件(里面存放的是我們對(duì).java編譯后產(chǎn)生的二進(jìn)制代碼),并且能夠解析它的指令,最終調(diào)用操作系統(tǒng)上的函數(shù),完成我們想要的操作! - 關(guān)于Java語(yǔ)言的
跨平臺(tái)性,就是因?yàn)镴VM,我們可以將其想象為一個(gè)抽象層,只要這個(gè)抽象層JVM正確執(zhí)行了.class文件,就能運(yùn)行在各種操作系統(tǒng)之上了!這就是一次編譯,多次運(yùn)行
2. JVM的位置
JVM是運(yùn)行在操作系統(tǒng)之上的,與硬件沒(méi)有直接的交互
3. JVM、JRE、JDK 的關(guān)系
JDK(Java Development Kit):Java開(kāi)發(fā)工具包
JRE(Java Runtime Environment):Java運(yùn)行環(huán)境
JDK = JRE + javac/java/jar 等指令工具
JRE = JVM + Java基本類庫(kù)
4. 三種JVM
- Sun公司
HotSpot(我們都用的這個(gè))
- BEA公司
JRockit - IBM公司
J9 VM
二、JVM體系結(jié)構(gòu)
Java虛擬機(jī)主要分為五大模塊:
- 類裝載器子系統(tǒng)
- 運(yùn)行時(shí)數(shù)據(jù)區(qū)
- 執(zhí)行引擎
- 本地方法接口
- 垃圾收集模塊
- 方法區(qū)是一種特殊的堆
- 棧里面不會(huì)有垃圾,用完就彈出了,否則阻塞了main方法
- 垃圾幾乎都在堆里,所以JVM性能調(diào)優(yōu)%99都針對(duì)于堆和方法區(qū),主要是堆
詳細(xì)圖:
三、類加載器
1. 作用
Class Loader類加載器:加載.class文件
- 加載:加載
.Class字節(jié)碼文件加載到內(nèi)存中,創(chuàng)建代表這個(gè)類的java.lang.Class對(duì)象 - 鏈接:對(duì)類進(jìn)行鏈接,將類的二進(jìn)制代碼合并到Java運(yùn)行時(shí)環(huán)境
JRE中 - 初始化:然后交給
JVM對(duì)類進(jìn)行初始化
詳細(xì)過(guò)程圖示:
2. 回顧new對(duì)象的過(guò)程
public class Student {//私有屬性private String name;//構(gòu)造方法public Student(String name) {this.name = name;}
}
- 類是模板、模板是抽象的;對(duì)象是具體的,是對(duì)抽象的實(shí)例化
//運(yùn)行時(shí),JVM將Test的信息放入方法區(qū)
public class Test{//main方法本身放入方法區(qū)public static void main(String[] args){//s1、s2、s3為不同對(duì)象Student s1 = new Student("zsr"); //引用放在棧里,具體的實(shí)例放在堆里Student s2 = new Student("gcc");Student s3 = new Student("BareTH");System.out.println(s1.hashCode());System.out.println(s2.hashCode());System.out.println(s3.hashCode());//class1、class2、class3為同一個(gè)對(duì)象Class<? extends Student> class1 = s1.getClass();Class<? extends Student> class2 = s2.getClass();Class<? extends Student> class3 = s3.getClass();System.out.println(class1.hashCode());System.out.println(class2.hashCode());System.out.println(class3.hashCode());}
}
根據(jù)結(jié)果,我們發(fā)現(xiàn):
- s1、s2、s3的hashcode是不同的,因?yàn)槭侨齻€(gè)不同的對(duì)象,對(duì)象是具體的
- class1、class2、class3的hashcode是相同的,因?yàn)檫@是類模板,模板是抽象的
我們畫(huà)圖分析以下new一個(gè)對(duì)象的流程:
- 首先
Class Loader讀取字節(jié)碼Student類編譯生成的.class文件,加載初始化生成Student模板類也就是相應(yīng)的java.lang.Class對(duì)象 - 然后通過(guò)該
Student模板類創(chuàng)建出三個(gè)具體的實(shí)例對(duì)象s1、s2、s3,這三個(gè)對(duì)象的引用在棧中,具體的實(shí)例是在堆中
3. 類加載器的等級(jí)
我們編寫(xiě)這樣一個(gè)程序,輸出當(dāng)前的類以及其父類往上的加載器
根據(jù)返回結(jié)果,可以看到不同等級(jí)的加載器,接下來(lái)介紹這些不同等級(jí)的類加載器
1. 啟動(dòng)類(根)加載器
啟動(dòng)類/根加載器:
BootstrapClassLoader
-
c++編寫(xiě),用于加載 java核心庫(kù)java.*,構(gòu)造 拓展類加載器 和 應(yīng)用程序加載器 -
根加載器 加載 拓展類加載器,并且將 拓展類加載器 的父加載器設(shè)置為 根加載器,
然后再加載 應(yīng)用程序加載器,應(yīng)將 應(yīng)用程序加載器 的父加載器設(shè)置為 拓展類加載器
-
由于引導(dǎo)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),我們無(wú)法直接獲取到啟動(dòng)類加載器的引用;這就是上面那個(gè)程序我們第三個(gè)結(jié)果為
null的原因。 -
加載文件存在位置
rt.jar:Java基礎(chǔ)類庫(kù),也就是Java doc里面看到的所有的類的class文件
2. 拓展類加載器
拓展類加載器:
PlatformClassLoader
-
java編寫(xiě),加載擴(kuò)展庫(kù),開(kāi)發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類加載器。 -
java9之前為
ExtClassloader,Java9以后改名為PlatformClassLoader -
加載文件存在位置:
/jre/lib/ext/*.jar
3. 應(yīng)用程序加載器
應(yīng)用程序加載器:
AppClassLoader
java編寫(xiě),加載程序所在的目錄下的類,也就是ClassPath路徑下的類包- 是Java
默認(rèn)的類加載器
4. 用戶自定義類加載器
用戶自定義類加載器:
CustomClassLoader
- 開(kāi)發(fā)人員可以通過(guò)繼承
java.lang.ClassLoader類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求
4. 類加載機(jī)制
類加載器通過(guò) 雙親委派機(jī)制 交給其他加載器來(lái)加載指定的類
1. 什么是雙親委派機(jī)制
我們查看“java.lang”包下的ClassLoader類,找到其中的loadClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// // 首先,檢查是否已經(jīng)被類加載器加載過(guò)Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// 存在父加載器,遞歸的交由父加載器加載if (parent != null) {c = parent.loadClass(name, false);} else {// 直到最上面的Bootstrap類加載器c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statsPerfCounter.getParentDelegationTime().addTime(t1 - t0);PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}
這段源碼很清晰的解釋了雙親委派機(jī)制,下圖就是該過(guò)程的描述
當(dāng)一個(gè).class文件要被加載時(shí),不考慮我們自定義類加載器類,首先會(huì)在AppClassLoader中檢查是否加載過(guò),如果有那就無(wú)需再加載;如果沒(méi)有會(huì)交到父加載器,然后調(diào)用父加載器的loadClass方法。父加載器同樣也會(huì)先檢查自己是否已經(jīng)加載過(guò),如果沒(méi)有再往上,直到到達(dá)BootstrapClassLoader之前,都是在檢查是否加載過(guò),并不會(huì)選擇自己去加載。到了根加載器時(shí),才會(huì)開(kāi)始檢查是否能夠加載當(dāng)前類,能加載就結(jié)束,使用當(dāng)前的加載器;否則就通知子加載器進(jìn)行加載;子加載器重復(fù)該步驟。如果到最底層還不能加載,就拋出異常ClassNotFoundException
總結(jié):所有的加載請(qǐng)求都會(huì)傳送到根加載器去加載,只有當(dāng)父加載器無(wú)法加載時(shí),子類加載器才會(huì)去加載
2. 為什么要設(shè)計(jì)這種機(jī)制
這種設(shè)計(jì)有個(gè)好處是,如果有人想替換系統(tǒng)級(jí)別的類:String.java。篡改它的實(shí)現(xiàn),在這種機(jī)制下這些系統(tǒng)的類已經(jīng)被
BootstrapClassLoader加載過(guò)了,所以其他類加載器并沒(méi)有機(jī)會(huì)再去加載,從一定程度上防止了危險(xiǎn)代碼的植入。
測(cè)試一下:我們重寫(xiě)java.lang包下的String類,然后實(shí)例化出一個(gè)String對(duì)象進(jìn)行測(cè)試
發(fā)現(xiàn)報(bào)錯(cuò)了,這就是雙親委派機(jī)制作用,當(dāng)我們實(shí)例化出一個(gè)String的對(duì)象時(shí),類加載器收到String類加載的請(qǐng)求將這個(gè)請(qǐng)求向上委托給父類加載器去完成,從應(yīng)用程序加載器向上委托到拓展類加載器,直到根加載器,發(fā)現(xiàn)根加載器中已經(jīng)加載過(guò)String類(也就是rt.jar中的java.lang.String),于事就結(jié)束了,不會(huì)再向下加載,也就是這里的String不是當(dāng)前應(yīng)用我們寫(xiě)的類,就不會(huì)執(zhí)行我們編寫(xiě)的這個(gè)String類,因此報(bào)錯(cuò)
3. 雙親委派機(jī)制的作用
1?? 避免類的重復(fù)加載
通過(guò)不斷委托父加載器直到根加載器進(jìn)行加載,如果父加載器加載過(guò)了,子加載器就不用再加載
2?? 保證Java核心庫(kù)類型的安全
如保證了上述的java.lang.String類不能被篡改
拓展:沙箱安全機(jī)制
這里引用了這篇博文https://www.cnblogs.com/zhahu/p/12576351.html,了解即可
什么是沙箱?
Java安全模型的核心就是Java
沙箱(sandbox)
- 沙箱是一個(gè)限制程序運(yùn)行的環(huán)境。沙箱機(jī)制就是將 Java 代碼限定在虛擬機(jī)(JVM)特定的運(yùn)行范圍中,并且嚴(yán)格限制代碼對(duì)本地系統(tǒng)資源訪問(wèn),通過(guò)這樣的措施來(lái)保證對(duì)代碼的有效隔離,防止對(duì)本地系統(tǒng)造成破壞。
- 沙箱主要限制系統(tǒng)資源訪問(wèn),系統(tǒng)資源包括CPU、內(nèi)存、文件系統(tǒng)、網(wǎng)絡(luò)。不同級(jí)別的沙箱對(duì)這些資源訪問(wèn)的限制也可以不一樣。
所有的Java程序運(yùn)行都可以指定沙箱,可以定制安全策略。
Java中的安全模型演進(jìn)
? 在Java中將執(zhí)行程序分成本地代碼和遠(yuǎn)程代碼兩種
- 本地代碼
可信任,可以訪問(wèn)一切本地資源。 - 遠(yuǎn)程代碼
不可信任,在早期的Java實(shí)現(xiàn)中,安全依賴于沙箱 (Sandbox) 機(jī)制。
如此嚴(yán)格的安全機(jī)制也給程序的功能擴(kuò)展帶來(lái)障礙,比如當(dāng)用戶希望遠(yuǎn)程代碼訪問(wèn)本地系統(tǒng)的文件時(shí)候,就無(wú)法實(shí)現(xiàn)。
因此在后續(xù)的 Java1.1 版本中,針對(duì)安全機(jī)制做了改進(jìn),增加了安全策略,允許用戶指定代碼對(duì)本地資源的訪問(wèn)權(quán)限。
在Java1.2版本中,再次改進(jìn)了安全機(jī)制,增加了代碼簽名。
- 不論本地代碼或是遠(yuǎn)程代碼,都會(huì)按照用戶的安全策略設(shè)定,由類加載器加載到虛擬機(jī)中權(quán)限不同的運(yùn)行空間,來(lái)實(shí)現(xiàn)差異化的代碼執(zhí)行權(quán)限控制。
當(dāng)前最新的安全機(jī)制實(shí)現(xiàn),則引入了域 (Domain) 的概念。
- 虛擬機(jī)會(huì)把所有代碼加載到不同的
系統(tǒng)域和應(yīng)用域 系統(tǒng)域部分專門負(fù)責(zé)與關(guān)鍵資源進(jìn)行交互應(yīng)用域部分則通過(guò)系統(tǒng)域的部分代理來(lái)對(duì)各種需要的資源進(jìn)行訪問(wèn)。- 虛擬機(jī)中不同的受保護(hù)域 (Protected Domain),對(duì)應(yīng)不一樣的權(quán)限 (Permission)。存在于不同域中的類文件就具有了當(dāng)前域的全部權(quán)限下
組成沙箱的基本組件
1. 字節(jié)碼校驗(yàn)器bytecode verifier
確保Java類文件遵循Java語(yǔ)言規(guī)范。這樣可以幫助Java程序?qū)崿F(xiàn)內(nèi)存保護(hù)。但并不是所有的類文件都會(huì)經(jīng)過(guò)字節(jié)碼校驗(yàn),比如核心類(如上述java.lang.String)。
2. 類裝載器class loader
類裝載器在3個(gè)方面對(duì)Java沙箱起作用
- 它防止惡意代碼去干涉善意的代碼;
- 它守護(hù)了被信任的類庫(kù)邊界;
- 它將代碼歸入保護(hù)域,確定了代碼可以進(jìn)行哪些操作。
虛擬機(jī)為不同的類加載器載入的類提供不同的命名空間,命名空間由一系列唯一的名稱組成,每一個(gè)被裝載的類將有一個(gè)名字,這個(gè)命名空間是由Java虛擬機(jī)為每一個(gè)類裝載器維護(hù)的,它們互相之間甚至不可見(jiàn)。
類裝載器采用的機(jī)制是雙親委派模式
- 從最內(nèi)層JVM自帶類加載器開(kāi)始加載,外層惡意同名類得不到加載從而無(wú)法使用;
- 由于嚴(yán)格通過(guò)包來(lái)區(qū)分了訪問(wèn)域,外層惡意的類通過(guò)內(nèi)置代碼也無(wú)法獲得權(quán)限訪問(wèn)到內(nèi)層類,破壞代碼就自然無(wú)法生效。
存取控制器(access controller):存取控制器可以控制核心API對(duì)操作系統(tǒng)的存取權(quán)限,而這個(gè)控制的策略設(shè)定,可以由用戶指定。安全管理器(security manager):是核心API和操作系統(tǒng)之間的主要接口。實(shí)現(xiàn)權(quán)限控制,比存取控制器優(yōu)先級(jí)高。安全軟件包(security package):java.security下的類和擴(kuò)展包下的類,允許用戶為自己的應(yīng)用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 數(shù)字簽名
- 加密
- 鑒別
四、本地方法接口
JNI:Java Native Interface,本地方法接口
Java誕生的時(shí)候C/C++橫行,為了立足,必須要能調(diào)用C/C++的程序;于是在內(nèi)存區(qū)域中專門開(kāi)辟了一塊標(biāo)記區(qū)域:Native Method Stack,登記所有的native本地方法(只是登記并不能執(zhí)行);
什么是本地方法呢?
本地方法是由其它語(yǔ)言編寫(xiě)的,編譯成和處理器相關(guān)的機(jī)器代碼。
本地方法保存在動(dòng)態(tài)鏈接庫(kù)中,即.dll(windows系統(tǒng))文件中,格式是各個(gè)平臺(tái)專有的。
通過(guò)本地方法,JAVA程序可以直接訪問(wèn)底層操作系統(tǒng)的資源,但是這樣用,java程序就變成平臺(tái)相關(guān)了,
因?yàn)楸镜胤椒ǖ膭?dòng)態(tài)庫(kù)是與平臺(tái)相關(guān)的,此外使用本地方法還可能把程序變得和特定的JAVA平臺(tái)實(shí)現(xiàn)相關(guān)。
為了調(diào)用這些本地方法,就出現(xiàn)了JNI,讓Java可以在執(zhí)行引擎執(zhí)行的的時(shí)候通過(guò)JNI加載本地方法庫(kù)的方法,實(shí)現(xiàn)了與操作系統(tǒng)的交互
在多線程學(xué)習(xí)的時(shí)候,我們啟動(dòng)一個(gè)線程調(diào)用start()方法本質(zhì)上是調(diào)用了start0本地方法
private native void start0();
可以看到start0()是用native關(guān)鍵字修飾的,那么native關(guān)鍵字是什么意思呢?
凡是帶native關(guān)鍵字的,說(shuō)明java的作用范圍達(dá)不到了,會(huì)去調(diào)用底層本地方法庫(kù)!通過(guò)本地方法接口JNI調(diào)用本地方法,拓展Java的使用,融合不同的語(yǔ)言為Java所用
目前該方法使用的越來(lái)越少了,除非是與硬件有關(guān)的應(yīng)用,比如通過(guò)Java程序驅(qū)動(dòng)打印機(jī)或者Java系統(tǒng)管理生產(chǎn)設(shè)備,在企業(yè)級(jí)應(yīng)用中已經(jīng)比較少見(jiàn)。因?yàn)楝F(xiàn)在的異構(gòu)領(lǐng)域間通信很發(fā)達(dá),比如可以使用 Socket通信,也可以使用 Web service等等,了解即可!
五、PC寄存器
程序計(jì)數(shù)器:
Program Counter Register
每個(gè)線程都有一個(gè)程序計(jì)數(shù)器,是線程私有的,就是一個(gè)指針,指向方法區(qū)中的方法字節(jié)碼(用來(lái)存儲(chǔ)指向像一條指令的地址,也即將要執(zhí)行的指令代碼),在執(zhí)行引擎讀取下一條指令,是一個(gè)非常小的內(nèi)存空間,幾乎可以忽略不計(jì)
六、方法區(qū)
方法區(qū):
Method Area
- 方法區(qū)是被
所有線程共享,所有字段和方法字節(jié)碼,以及一些特殊方法,如構(gòu)造函數(shù),接口代碼也在此定義 - 簡(jiǎn)單說(shuō),所有定義的方法的信息都保存在該區(qū)域,此區(qū)域?qū)儆?code>共享區(qū)間;
- 方法區(qū)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- 雖然Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫做
Non-Heap(非堆),目的應(yīng)該是與Java 堆區(qū)分開(kāi)來(lái)
1. 方法區(qū)中有啥
- 靜態(tài)變量(static)
- 類的抽象模板Class信息(版本、字段、方法、接口等描述信息,也就是Class對(duì)象)
- 運(yùn)行時(shí)常量池
注意:實(shí)例變量在堆內(nèi)存中,與方法區(qū)無(wú)關(guān)
2. 創(chuàng)建對(duì)象內(nèi)存分析
- 創(chuàng)建一個(gè)對(duì)象時(shí),方法區(qū)中會(huì)生成對(duì)應(yīng)類的抽象模板;還有對(duì)應(yīng)的常量池、靜態(tài)變量、類信息、常量
- 我們通過(guò)類模板去new對(duì)象的時(shí)候
- 堆中存放實(shí)例對(duì)象
- 棧中存放對(duì)象的引用,每個(gè)對(duì)象對(duì)應(yīng)一個(gè)地址指向堆中相同地址的實(shí)例對(duì)象
例如這個(gè)例子中,生成了對(duì)應(yīng)的Person模板類,name常量“zsr”放在常量池中,三個(gè)對(duì)象的引用放在棧中,該引用指向放在堆中的三個(gè)實(shí)例對(duì)象。這就是堆、棧、方法區(qū)的交互關(guān)系
七、棧
又稱
棧內(nèi)存,主管程序的運(yùn)行,生命周期和線程同步
- 棧是線程級(jí)的:線程結(jié)束,棧內(nèi)存就釋放了,不存在垃圾回收
- 先進(jìn)后出
1、棧中存放啥?
- 8大基本類型
- 對(duì)象引用
- 實(shí)例的方法
2、棧運(yùn)行原理
- 棧表示Java方法執(zhí)行的內(nèi)存模型
- 每調(diào)用一個(gè)方法就會(huì)為每個(gè)方法生成一個(gè)
棧幀(Stack Frame),每個(gè)方法被調(diào)用和完成的過(guò)程,都對(duì)應(yīng)一個(gè)棧幀從虛擬機(jī)棧上入棧和出棧的過(guò)程。 - 程序正在執(zhí)行的方法一定在棧的頂部
3、堆棧溢出StackOverflowError
舉個(gè)例子:
public class Test {public static void main(String[] args) {new Test().a();}public void a() {b();}public void b() {a();}
}
最開(kāi)始,main()方法壓入棧中,然后執(zhí)行a(),a()壓入棧中;再調(diào)用b(),b()壓入棧中;以此往復(fù),a與b方法不斷被壓入棧中,最終導(dǎo)致棧溢出
八、堆
Heap,堆在JVM啟動(dòng)的時(shí)候創(chuàng)建,其空間大小也被創(chuàng)建,堆內(nèi)存的大小是可以調(diào)節(jié)的;不同于棧:
- 一個(gè)JVM只有一個(gè)堆內(nèi)存,而棧是線程級(jí)的
- 垃圾幾乎都在堆里,所以JVM性能調(diào)優(yōu)%99都針對(duì)于堆和方法區(qū),主要是堆
堆中存放什么:
- 類的實(shí)例化對(duì)象
1. 堆的分區(qū)圖
2. 堆內(nèi)存詳解
Young 年輕代
對(duì)象誕生、成長(zhǎng)甚至死亡的區(qū),分為 伊甸園區(qū) 和 幸存區(qū)
Eden Space伊甸園區(qū):所有的對(duì)象都是在此new出來(lái)的Survivor Space幸存區(qū)幸存0區(qū)(From Space)幸存1區(qū)(To Space)- 這兩個(gè)區(qū)是動(dòng)態(tài)的,會(huì)互相交換
Eden區(qū)占大容量,Survivor兩個(gè)區(qū)占小容量,默認(rèn)比例是8:1:1。
Tenured 老年代
在 年輕代 存活下來(lái)的 進(jìn)入 老年代
Perm 元空間
存儲(chǔ)的是Java運(yùn)行時(shí)的一些環(huán)境或類信息,這個(gè)區(qū)域不存在垃圾回收!關(guān)閉虛擬機(jī)就會(huì)釋放這個(gè)區(qū)域內(nèi)存!方法區(qū)在此
-
這個(gè)區(qū)域常駐內(nèi)存,用來(lái)存放JDK自身攜帶的Class對(duì)象、Interface元數(shù)據(jù)。
-
什么情況永久區(qū)會(huì)崩?
一個(gè)啟動(dòng)類加載了大量的第三方Jar包,Tomcat部署了過(guò)多應(yīng)用,或者大量動(dòng)態(tài)生成的反射類
這些東西不斷的被加載,直到內(nèi)存滿,就會(huì)出現(xiàn)
OOM -
名稱演變
jdk1.6之前: `永久代` jdk1.7: `永久代`慢慢退化 ==> `去永久代` jdk1.8之后: `永久代`改名為`元空間`
注意:元空間在邏輯上存在,在物理上不存在
新生代 + 老年代的內(nèi)存空間 = JVM分配的總內(nèi)存
3. GC垃圾回收
GC垃圾回收,主要在年輕代和老年代
- Minor GC:伊甸園區(qū)滿時(shí)觸發(fā);從年輕代回收內(nèi)存
- Full GC:老年代滿時(shí)觸發(fā);清理整個(gè)堆空間,包含年輕代和老年代
首先,對(duì)象出生再伊甸園區(qū)
- 假設(shè)
伊甸園區(qū)只能存一定數(shù)量的對(duì)象,則每當(dāng)存滿時(shí)就會(huì)觸發(fā)一次 輕GC(Minor GC) - 輕GC 清理后,有的對(duì)象可能還存在引用,就活下來(lái)了,活下來(lái)的對(duì)象就進(jìn)入
幸存區(qū);有的對(duì)象沒(méi)用了,就被GC清理掉了;每次 輕GC 都會(huì)使得伊甸園區(qū)為空 - 如果對(duì)象在
年輕代經(jīng)歷了15次GC還沒(méi)有死亡(可設(shè)置-XX:MaxTenuringThreshold=n新生代的對(duì)象最多經(jīng)歷n次GC, 就能晉升到老年代),則會(huì)進(jìn)入老年代,如果老年代滿了,就會(huì)觸發(fā)一次 重GC(FullGC),年輕代+老年代的對(duì)象都會(huì)清理一次,活下的對(duì)象就進(jìn)入老年代 - 如果
新生代和老年代都滿了,則OOM
何時(shí)進(jìn)入老年代
4. 堆內(nèi)存調(diào)優(yōu)
1. 查看并設(shè)置JVM堆內(nèi)存
查看我們jvm的堆內(nèi)存
public class Test {public static void main(String[] args) {//返回jvm試圖使用的最大內(nèi)存long max = Runtime.getRuntime().maxMemory();//返回jvm的初始化內(nèi)存總量long total = Runtime.getRuntime().totalMemory();//默認(rèn)情況下:分配的總內(nèi)存為電腦內(nèi)存的1/4,初始化內(nèi)存為電腦內(nèi)存的1/64System.out.println("max=" + max / (double) 1024 / 1024 / 1024 + "G");System.out.println("total=" + total / (double) 1024 / 1024 / 1024 + "G");}
}
默認(rèn)情況下:
- JVM最大分配內(nèi)存為電腦內(nèi)存的1/4
- JVM初始化內(nèi)存為電腦內(nèi)存的1/64
我們可以手動(dòng)調(diào)堆內(nèi)存大小
在VM options中可以指定jvm試圖使用的最大內(nèi)存和jvm初始化內(nèi)存大小
-Xms1024m -Xmx1024m -Xlog:gc*
-Xmx用來(lái)設(shè)置jvm試圖使用的最大內(nèi)存,默認(rèn)為1/4-Xms用來(lái)設(shè)置jvm初始化內(nèi)存,默認(rèn)為1/64-Xlog:gc*用來(lái)打印GC垃圾回收信息
2. 什么是OOM
內(nèi)存溢出:java.lang.OutOfMemoryError
產(chǎn)生原因:
- 分配的太少
- 用的太多
- 用完沒(méi)釋放
我們可以寫(xiě)一個(gè)實(shí)例模擬堆內(nèi)存溢出
package java.lang;import java.util.Random;public class Test{public static void main(String[] args){String str = "zsr";while(true){str += str + new Random().nextInt(88888888)+ new Random().nextInt(888888888);}}
}
3. 怎么排除OOM錯(cuò)誤?
1. 嘗試擴(kuò)大堆內(nèi)存看結(jié)果
利用上述方法指定
jvm試圖使用的最大內(nèi)存和jvm初始化內(nèi)存大小
2. 利用內(nèi)存快照工具JProfiler
例如:MAT(Eclipse)、JProfiler
作用:分析Dump內(nèi)存文件,快速定位內(nèi)存泄漏;獲得堆中的文件;獲得大的對(duì)象…
3. 什么是Dump文件?如何分析?
Dump文件是
進(jìn)程的內(nèi)存鏡像,可以把程序的執(zhí)行狀態(tài)通過(guò)調(diào)試器保存到dump文件中
舉個(gè)例子
import java.util.ArrayList;public class Test {byte[] array = new byte[1024 * 1024];//1Mpublic static void main(String[] args) {ArrayList<Test> list = new ArrayList<>();int count = 0;try {while (true) {list.add(new Test());count++;}} catch (Exception e) {System.out.println("count=" + count);e.printStackTrace();}}
}
運(yùn)行該程序,報(bào)錯(cuò)OOM
接下來(lái)我們?cè)O(shè)置以下堆內(nèi)存,并附加生成對(duì)應(yīng)的dump文件的指令
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError表示當(dāng)JVM發(fā)生OOM時(shí),自動(dòng)生成DUMP文件。
再次點(diǎn)擊運(yùn)行,下載了對(duì)應(yīng)的Dump文件
我們右鍵該類,點(diǎn)擊Show in Explorer
一直點(diǎn)擊上級(jí)目錄,直到找到.hprof文件,與src同級(jí)目錄下
每次打開(kāi)Dump文件查看完后,建議刪除,可以在idea中看到,打開(kāi)文件后生成了很多內(nèi)容,占內(nèi)存,建議刪除
附:安裝Jprofiler教程
1. idea中安裝插件
2. 下載客戶端
下載地址:https://www.ej-technologies.com/download/jprofiler/files
3. 安裝客戶端
選擇自定義安裝,注意:路徑不能有中文和空格
后續(xù)默認(rèn),安裝成功即可
4. IDEA中設(shè)置
安裝完成后,重啟IDEA,可以看到我們的內(nèi)存快照工具
打開(kāi)IDEA的設(shè)置,找到Tools里面的JProfiler,沒(méi)有設(shè)置位置則設(shè)置位置
此時(shí)則全部安裝完成
九、GC算法
Garbage Collection:垃圾回收我們已經(jīng)對(duì)GC的流程進(jìn)行了大概的講解,這里做一些總結(jié):
JVM在進(jìn)行GC時(shí),并不是對(duì)
年輕代、老年代統(tǒng)一回收;大部分時(shí)候,回收都是在年輕代GC分為兩種:
輕GC(清理年輕代)重GC(清理年輕代+老年代)
1. 引用計(jì)數(shù)算法(很少使用)
- 每個(gè)對(duì)象在創(chuàng)建的時(shí)候,就給這個(gè)對(duì)象綁定一個(gè)計(jì)數(shù)器
- 每當(dāng)有一個(gè)引用指向該對(duì)象時(shí),計(jì)數(shù)器加一;每當(dāng)有一個(gè)指向它的引用被刪除時(shí),計(jì)數(shù)器減一
- 這樣,當(dāng)沒(méi)有引用指向該對(duì)象時(shí),該對(duì)象死亡,計(jì)數(shù)器為0,這時(shí)就應(yīng)該對(duì)這個(gè)對(duì)象進(jìn)行垃圾回收操作
- 計(jì)數(shù)器本身也是一種消耗
2. 復(fù)制算法
復(fù)制算法主要發(fā)生在年輕代( 幸存0區(qū) 和 幸存1區(qū))
- 當(dāng)Eden區(qū)滿的時(shí)候,會(huì)觸發(fā)
輕GC,每觸發(fā)一次,活的對(duì)象就被轉(zhuǎn)移到幸存區(qū),死的就被GC清理掉了,所以每觸發(fā)輕GC時(shí),Eden區(qū)就會(huì)清空; - 對(duì)象被轉(zhuǎn)移到了幸存區(qū),幸存區(qū)又分為
From Space和To Space,這兩塊區(qū)域是動(dòng)態(tài)交換的,誰(shuí)是空的誰(shuí)就是To Space,然后From Space就會(huì)把全部對(duì)象轉(zhuǎn)移到To Space去; - 那如果兩塊區(qū)域都不為空呢?這就用到了
復(fù)制算法,其中一個(gè)區(qū)域會(huì)將存活的對(duì)象轉(zhuǎn)移到令一個(gè)區(qū)域去,然后將自己區(qū)域的內(nèi)存空間清空,這樣該區(qū)域?yàn)榭?#xff0c;又成為了To Space; - 所以每次觸發(fā)
輕GC后,Eden區(qū)清空,同時(shí)To區(qū)也清空了,所有的對(duì)象都在From區(qū)
這也就是
幸存0區(qū)和幸存1區(qū)總有一塊為空的原因
好處:沒(méi)有內(nèi)存的碎片(內(nèi)存集中在一塊)
壞處:
- 浪費(fèi)了內(nèi)存空間(浪費(fèi)了幸存區(qū)一半空間)
- 對(duì)象存活率較高的場(chǎng)景下(比如老年代那樣的環(huán)境),需要復(fù)制的東西太多,效率會(huì)下降。
最佳使用環(huán)境:對(duì)象存活度較低的時(shí)候,也就是年輕代
3. 標(biāo)記–清除算法
為每個(gè)對(duì)象存儲(chǔ)一個(gè)標(biāo)記位,記錄對(duì)象的生存狀態(tài)
- 標(biāo)記階段:這個(gè)階段內(nèi),為每個(gè)對(duì)象更新標(biāo)記位,檢查對(duì)象是否死亡;
- 清除階段:該階段對(duì)死亡的對(duì)象進(jìn)行清除,執(zhí)行 GC 操作。
缺點(diǎn):兩次掃描嚴(yán)重浪費(fèi)時(shí)間,會(huì)產(chǎn)生內(nèi)存碎片
優(yōu)點(diǎn):不需要額外的空間
4. 標(biāo)記–壓縮算法
標(biāo)記-整理法 是 標(biāo)記-清除法 的一個(gè)改進(jìn)版。
又叫做 標(biāo)記-清除-壓縮法
- 標(biāo)記階段,該算法也將所有對(duì)象標(biāo)記為存活和死亡兩種狀態(tài);
- 不同的是,在第二個(gè)階段,該算法并沒(méi)有直接對(duì)死亡的對(duì)象進(jìn)行清理,而是再次掃描,向一段移動(dòng)存活的對(duì)象,然后把剩下的所有對(duì)象全部清除。
缺點(diǎn):多了一次移動(dòng)成本
可以進(jìn)一步優(yōu)化,在內(nèi)存碎片不太多的情況下,就繼續(xù)標(biāo)記清除,到達(dá)一定量的時(shí)候再壓縮
總結(jié)
- 內(nèi)存(時(shí)間復(fù)雜度)效率:復(fù)制算法 > 標(biāo)記清除算法 > 標(biāo)記壓縮算法
- 內(nèi)存整齊度:復(fù)制算法 = 標(biāo)記壓縮法 > 標(biāo)記清除法
- 內(nèi)存利用率:標(biāo)記壓縮法 = 標(biāo)記清除法 > 復(fù)制算法
思考:有沒(méi)有最優(yōu)的算法?
沒(méi)有最優(yōu)的算法,只有最合適的算法,所以 GC 也稱為
分代收集算法,對(duì)于年輕代和老年代采用不同的算法
對(duì)于年輕代:
- 對(duì)象存活率低
- 用復(fù)制算法
對(duì)于老年代:
- 區(qū)域大,對(duì)象存活率高
- 用
標(biāo)記清除+標(biāo)記壓縮混合實(shí)現(xiàn)
總結(jié)
以上是生活随笔為你收集整理的面试高频!JVM必备教程~的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【快速上手mac必备】常用优质mac软件
- 下一篇: Git最新版从零开始详细教程(迅速搞定~