文章首發(fā)我的博客,歡迎訪問:https://blog.itzhouq.cn/jvm
首先基本的面試題都是下面的奪命連環(huán)問,感受一下。
- 請你談?wù)勀銓?JVM 的理解。java8 虛擬機(jī)和之前有什么變化?
- 什么是 OOM, 什么是棧溢出 StackOverFlowError? 怎么分析?
- JVM 的常用調(diào)優(yōu)參數(shù)有哪些?
- 內(nèi)存快照如何抓取,怎么分析 Dump 文件?你知道嗎?
- 談?wù)?JVM 中,你對類加載器的認(rèn)識?
這篇文章先大體梳理一下相關(guān)的知識點(diǎn),后面再整理一篇基本面試題相關(guān)的,先挖個坑。要說明的是,文章中很多地方關(guān)于概念是一帶而過的,難免有部分內(nèi)容沒有說明白。對于不明白的點(diǎn),建議自己動手查查相關(guān)資料,決不能指望一篇筆記就能把 JVM 搞明白,這顯然也是不可能的。
1、JVM 的位置

可以看到 JVM 是 JRE 的一部分。主要工作是解釋自己的字節(jié)碼并映射到本地的 CPU 指令集和 OS 的系統(tǒng)調(diào)用。Java 語言是跨平臺的,不同的操作系統(tǒng)會有不同的 JVM 映射規(guī)則,這就使得 Java 語言與操作系統(tǒng)無關(guān)。
2、JVM 的體系結(jié)構(gòu)



3、類加載器
作用:加載 Class 文件,比如我們 new Student() 的時候,Student 是類,是抽象的,使用 new 關(guān)鍵詞創(chuàng)建對象實(shí)例,實(shí)例的引用是在棧中,而具體的人是放在堆中。
3.1、類的實(shí)例化和雙親委派機(jī)制
public class Car {
public static void main(String[] args) {
// 類是模板,對象是具體的
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode()); // 460141958
System.out.println(car2.hashCode()); // 1163157884
System.out.println(car3.hashCode()); // 1956725890
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode()); // 685325104
System.out.println(aClass2.hashCode()); // 685325104
System.out.println(aClass3.hashCode()); // 685325104
}
}

3.2、類加載
類加載器:
- 虛擬機(jī)自帶的加載器
- 啟動類(根)加載器
- 擴(kuò)展類加載器
- 應(yīng)用程序加載器
試驗(yàn):自己定義一個String類,看是否能執(zhí)行
package java.lang;
public class String {
// 雙親委派機(jī)制:安全
// BOOT --> EXT --> APP (最終執(zhí)行)
// BOOT
// EXT
// APP
public String toString () {
return "Hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.toString());
}
// 錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
// public static void main(String[] args)
//否則 JavaFX 應(yīng)用程序類必須擴(kuò)展javafx.application.Application
/**
* 類加載的流程
* 1. 類加載器收到類加載的請求
* 2. 將這個請求向上委托給父類加載器去完成,一直向上委托,直到啟動類加載器
* 3. 啟動節(jié)加載器檢查是否能夠加載當(dāng)前這個類,能加載就結(jié)束,使用當(dāng)前的加載器,
否則,拋出異常通知子加載器進(jìn)行加載
* 4. 重復(fù)步驟3
*/
}
百度:雙親委派機(jī)制
4、沙盒安全機(jī)制
Java安全的模型的核心就是 Java 沙箱 (sandbox),什么是沙箱?沙箱是一個限制程序運(yùn)行的環(huán)境。沙箱機(jī)制就是將 Java 代碼限制在虛擬機(jī)特定的運(yùn)行環(huán)境中,并且嚴(yán)格限制代碼對本地系統(tǒng)資源訪問,通過這樣的措施來保證對代碼的有效隔離,防止對本地系統(tǒng)操作破壞。沙箱主要限制系統(tǒng)資源訪問。
5、Native
開啟一個多線程啟動類:
public static void main(String[] args) {
new Thread(() -> {
}, "my thread name").start();
}
點(diǎn)進(jìn)去查看start()方法的源碼:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0(); // 調(diào)用start0()方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
可以看到源碼中使用了特殊的方法 start0(),使用了 native關(guān)鍵詞。
native :凡是帶了 native 關(guān)鍵詞,說明 java 的作用范圍達(dá)不到了,會調(diào)用底層 C 語言的庫!
native 的方法會進(jìn)入本地方法棧,調(diào)用本地方法接口 JNI(Java Native Interface 本地方法接口),其他的就是 Java 棧。
JNI的作用:擴(kuò)展 java 的使用,融合不同的編程語言為 java 所用!最初的時候需要融合 C 和 C++。
Java 誕生的時候, C 和 C++ 橫行,想要立足,必須要有調(diào)用 C 和 C++的程序。
它在內(nèi)存區(qū)域中專門開辟了一塊標(biāo)記區(qū)域: Native Method Stack ,登記 native 方法,
在最終執(zhí)行的時候,加載本地方法庫中的方法通過 JNI。
比如: Java程序驅(qū)動打印機(jī),管理系統(tǒng)。這部分掌握即可,在企業(yè)級應(yīng)用中較為少見。
現(xiàn)在調(diào)用第三方語言接口的方式很多,比如:Socket、WebService、HTTP等。
6、PC 寄存器
程序計(jì)數(shù)器:Program Counter Register
每個線程都有一個程序計(jì)數(shù)器,是線程私有的,就是一個指針,指向方法區(qū)中的方法字節(jié)碼(用來存儲指向一條指令的地址,也即將要執(zhí)行的指令代碼),在執(zhí)行引擎下讀取下一條指令,是一個非常小的內(nèi)存空間,幾乎可以忽略不計(jì)。
7、方法區(qū)
Method Area 方法區(qū)
方法區(qū)是被所有線程共享的,所有字段和方法字節(jié)碼,以及一些特殊方法,如構(gòu)造函數(shù),接口代碼也在此定義,簡單說,所有定義的方法的信息都保存在該區(qū)域,此區(qū)域?qū)儆诠蚕韰^(qū)間。
**==靜態(tài)變量、常量、類信息(構(gòu)造方法、接口定義)、運(yùn)行時的常量池存在方法區(qū)中,但是實(shí)例變量存在堆內(nèi)存中,和方法區(qū)無關(guān)== ** 。
static、final、Class、常量池。
8、棧
棧是一種數(shù)據(jù)結(jié)構(gòu),可以形象地理解為一個水桶或水杯,其特點(diǎn)是先進(jìn)后出。 比如,依次將乒乓球放入杯子中,先放進(jìn)去的球,最后才能拿出來。
棧中存放 8 大基本數(shù)據(jù)類型 + 對象引用 + 實(shí)例的方法。
棧內(nèi)存主管程序的運(yùn)行,生命周期和線程同步。Java 中執(zhí)行方法的過程就是調(diào)用棧的過程。為什么 main() 方法,最先執(zhí)行,最后結(jié)束呢?因?yàn)閙ain() 方法是程序的入口,執(zhí)行時 main() 會最先被壓到棧底, 在 main() 方法中調(diào)用其他方法時,依次將其他方法壓入棧中。
線程結(jié)束,棧內(nèi)存就釋放了,對于棧來說,不存在垃圾回收問題。
棧的運(yùn)行原理:

棧 + 堆 + 方法區(qū)的交互關(guān)系

畫一個對象實(shí)例化的過程在內(nèi)存中:百度、看視頻。
JVM的內(nèi)存區(qū)域劃分,對象實(shí)例化分析
9、三種 JVM
- Sun 公司
HotSpot:Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode) - BEA:
JRockit - IBM:
J9VM
我們學(xué)習(xí)的都是HotSpot 。
10、堆
Heap,一個 JVM 只有一個堆內(nèi)存,堆內(nèi)存的大小是可以調(diào)節(jié)的。
類加載器讀取類文件后,一般會把什么東西放入堆中?類、方法、常量、變量,保存所有引用類型的真實(shí)對象。
堆內(nèi)存中還要細(xì)分為三個區(qū)域:
- 新生區(qū)(伊甸園區(qū) Eden):Young/New
- 養(yǎng)老區(qū):old
- 永久區(qū):Perm

新生區(qū)中沒有被垃圾收集器干掉的對象會進(jìn)入幸存區(qū)0區(qū),幸存區(qū)0區(qū)中沒有被干掉的對象會進(jìn)入幸存區(qū)1區(qū)。幸存區(qū)0區(qū)和幸存區(qū)1區(qū)會不停的交換位置。經(jīng)過一定次數(shù)后的垃圾回收后還沒有被干掉的對象會進(jìn)入養(yǎng)老區(qū),這個區(qū)域的對象一般不會被干掉,但不是絕對的。假設(shè)養(yǎng)老區(qū)滿了,對象會進(jìn)入永久存儲區(qū)。
針對新生區(qū)的垃圾回收稱為輕量級的垃圾收集,也稱輕 GC。針對養(yǎng)老區(qū)的垃圾回收稱為重量級的垃圾收集,也稱重 GC。
GC 垃圾回收,主要是在伊甸園區(qū)和養(yǎng)老區(qū)。
假設(shè)內(nèi)存滿了,會報錯 OOM,OutOfMemeroy,對內(nèi)存不夠!
public static void main(String[] args) {
String str = "hello world!";
while (true) {
str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999);
}
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at java.util.Arrays.copyOf(Arrays.java:3332)
// at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
// at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
// at java.lang.StringBuilder.append(StringBuilder.java:208)
// at Hello.main(Hello.java:8)
}
在 JDK8 以后,永久存儲區(qū)改名為元空間。
11、新生區(qū)、老年區(qū)
新生區(qū):
- 類:誕生和成長的地方,甚至死亡;
- 伊甸園,所有對象都是在伊甸園區(qū) new 出來的;
- 幸存者區(qū):0 和 1 區(qū)。
- 經(jīng)過研究,99%的對象都是臨時對象,所以進(jìn)入老年區(qū)的對象很少。
12、永久區(qū)
這個區(qū)域常駐內(nèi)存的。用來存放 JDK 自身攜帶的 Class 對象。Interface 元數(shù)據(jù),存儲的是 Java 運(yùn)行時的一些環(huán)境或類信息,這個區(qū)域不存在垃圾回收!關(guān)閉 VM 虛擬機(jī)就會釋放這個區(qū)域的內(nèi)存。
一個啟動類,加載了大量的第三方 jar 包。Tomcat 部署了太多的應(yīng)用,大量動態(tài)生成的反射類。不斷的被加載,知道內(nèi)存滿,就會出現(xiàn) OOM。
- JDK1.6:永久代,常量池存放在方法區(qū);
- JDK1.7:永久代,但是慢慢的退化了,常量池在堆中;
- JDK1.8:無永久代,常量池在元空間。

13、堆內(nèi)存調(diào)優(yōu)
public static void main(String[] args) {
// 返回 JVM 試圖使用的最大內(nèi)存
long max = Runtime.getRuntime().maxMemory(); // 字節(jié)
// 返回 JVM 的初始化總內(nèi)存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max=" + max + "字節(jié)\t" + (max / (double)1024/1024) + "MB");
// max=2831679488字節(jié) 2700.5MB
System.out.println("total=" + max + "字節(jié)\t" + (total / (double)1024/1024) + "MB");
// total=2831679488字節(jié) 182.5MB
// 默認(rèn)情況下:分配的從內(nèi)存是電腦內(nèi)存的 1/4, 而初始化內(nèi)存是電腦內(nèi)存的 1/64。
}
元空間在邏輯上存在,物理上不存在。
OOM 解決方案:
- 嘗試擴(kuò)大堆內(nèi)存看結(jié)果;
- 分析內(nèi)存,看一下哪個地方出現(xiàn)了問題(專業(yè)工具)。


14、使用 JProfier 工具分析 OOM 原因
在一個項(xiàng)目中,突然出現(xiàn)了 OOM 故障,那么該如何排除,研究為什么出錯?
- 能夠看到代碼第幾行出錯:內(nèi)存快照分析工具,MAT(Eclipse)、JProfiler
- Debugger,一行行分析代碼。
MAT 、JProfiler的作用:
- 分析 Dump 內(nèi)存文件,快讀定位內(nèi)存泄漏;
- 獲得堆中的數(shù)據(jù);
- 獲得大的對象;
- 。。。。。。
JProfiler 插件和Windows客戶端安裝百度。

編寫一個 OOM 的程序測試
public class Demo03 {
byte[] array = new byte[1 * 1024 * 1024]; // 1MB
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new Demo03());
count ++;
}
} catch (Exception e) { // 錯誤寫法
e.printStackTrace();
}
}
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at Demo03.<init>(Demo03.java:4)
// at Demo03.main(Demo03.java:12)
}
這個程序出現(xiàn)了 OOM,但是從報錯信息無法看出哪里的問題。
此時需要添加一些配置,打印一些信息。


通過 JProfiler 工具打開文件:


總結(jié):
// -Xms 設(shè)置初始化內(nèi)存分配的大小 默認(rèn)1/64
// -Xmx 設(shè)置最大分配內(nèi)存 默認(rèn) 1/4
// -XX:+PrintDCDetails // 打印GC垃圾回收信息
// -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError OOM Dump(轉(zhuǎn)儲文件)
15、GC : 常用算法
GC 的作用區(qū)域:

JVM 在進(jìn)行 GC 時,并不是對這三個區(qū)域統(tǒng)一回收,大部分時候,回收都是新生區(qū)。
- 新生區(qū)
- 幸存區(qū):from to
- 老年區(qū)
GC 的分類:
輕 GC(普通的 GC):主要針對新生區(qū),偶爾對幸存區(qū)進(jìn)行 GC。
重 GC(全局 GC):把上面所有的區(qū)域都進(jìn)行 GC,也就是釋放內(nèi)存。
堆內(nèi)存中的幸存區(qū)是可以交換位置的。

GC 的題目:
- JVM 的內(nèi)存模型和分區(qū),詳細(xì)到每個區(qū)放什么;
- 堆里面的分區(qū)有哪些? Eden、from、to、Old,說說他們的特點(diǎn);
- GC 的算法有哪些?標(biāo)記清除法、標(biāo)記壓縮、復(fù)制算法、引用計(jì)數(shù)器,怎么用的?
- 輕 GC 和重 GC 分別在什么時候發(fā)生?
引用計(jì)數(shù)器:(用得少)

復(fù)制算法:幸存區(qū)的復(fù)制


GC 復(fù)制算法:
好處:沒有內(nèi)存碎片;
壞處:浪費(fèi)了空間內(nèi)存,多了一半空間(to)永遠(yuǎn)都是空的。極端情況下,比如對象 100% 存活,這個缺點(diǎn)就很明顯。
復(fù)制算法最佳使用場景:對象存活度較低,比如新生區(qū)。
標(biāo)記清除算法
優(yōu)點(diǎn):不需要額外的空間!
缺點(diǎn):兩次掃描,嚴(yán)重浪費(fèi)時間,會產(chǎn)生內(nèi)存碎片。

標(biāo)記壓縮
對標(biāo)清除進(jìn)行再優(yōu)化。

標(biāo)記清除壓縮
再次優(yōu)化上述算法,可以多次進(jìn)行標(biāo)記清除,進(jìn)行一次標(biāo)記壓縮。
16、總結(jié):
- 內(nèi)存效率:復(fù)制算法 > 標(biāo)記清除算法 > 標(biāo)記壓縮算法(時間復(fù)雜度)
- 內(nèi)存整齊度:復(fù)制算法 == 標(biāo)記壓縮算法 > 標(biāo)記清除算法
- 內(nèi)存利用率:標(biāo)記壓縮算法 == 標(biāo)記清除算法 > 復(fù)制算法
思考一個問題:難道沒有一個最優(yōu)的算法嗎?
答案:沒有,沒有最好的算法,只有最合適的算法---> GC:分代收集算法
年輕代:
- 存活率低
- 復(fù)制算法
老年代:
- 區(qū)域大,存活率低
- 標(biāo)記清除(內(nèi)存碎片不是太多)+ 標(biāo)記壓縮混合實(shí)現(xiàn)。
參考書籍:《深入理解 JVM》
17、JMM(高頻) 和 快速學(xué)習(xí)方法
- 什么是 JMM?
JMM:Java Memory Model 的縮寫。
- 它是干嘛的?
作用:緩存一致性協(xié)議,用于定義數(shù)據(jù)讀寫的規(guī)則。
JMM 定義了線程工作內(nèi)存和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory)。
解決共享對象可見性這個問題: volilate
- 它該如何學(xué)習(xí)?
官方、其他人的博客、對應(yīng)的視頻。。。
關(guān)于內(nèi)存圖:可以去思維導(dǎo)圖 : processon 網(wǎng)站搜索 JVM 可以看到別人畫的相關(guān)思維導(dǎo)圖。