寫在前面
上周老大給安排幾個面試的任務(wù),我一般問兩方面:
- 項目經(jīng)驗中解決過的比較有意思的問題又哪些?
- HashMap使用的時候需要注意些什么?
大部分情況下都是希望從第二個問題中挖出來一些東西,和其他同事問的一些JVM相關(guān)的問題比還是太弱了,所以一定要學(xué)習(xí)一下。這篇文章主要想搞明白:
- 內(nèi)存管理
- 代碼執(zhí)行
Java虛擬的內(nèi)容太多了,不是隨便看看書就能學(xué)會的,這里將要總結(jié)的都只是皮毛而已,應(yīng)付一般面試是夠了。
內(nèi)存管理
Java虛擬機在執(zhí)行的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域,大致如下:
各部分的功能如下:
區(qū)域 | 功能 |
---|---|
程序計數(shù)器 | 可以看做當(dāng)前線程執(zhí)行字節(jié)碼的行號 |
虛擬機棧 | 存放局部變量、操作棧等 |
本地方法棧 | 與虛擬機棧類似,不過是服務(wù)于本地方法 |
堆 | 存放對象 |
方法區(qū) | 存放類信息、常量、靜態(tài)變量、JIT編譯后的代碼等 |
運行時常量池 | 編譯時生成的各種字面量和符號使用 |
直接內(nèi)存 | 通過NIO分配的對外內(nèi)存 |
在內(nèi)存管理部分比較大的一塊內(nèi)容是GC(垃圾回收),所謂垃圾回收就是將垃圾占用的內(nèi)存回收掉。那么第一個問題:什么是垃圾?
- 引用計數(shù)算法:被引用次數(shù)為0的對象。
- 根搜索算法:從GC Roots沿著引用找不到的對象。
這里都提到了引用,在JDK 1.2之后Java就已經(jīng)對引用的概念進行了擴充,那么第二個問題:有哪些類型的引用?
- 強引用:Object o = new Object()這種都是強引用。
- 弱引用:還有用但非必須的,在OOM之前被回收。
- 軟引用:更弱的引用,在下次GC的時候被回收。
- 虛引用:最弱的,唯一的作用是在對象被回收的時候可以收到通知。
這里只有強引用才能對對象的生命周期造成影響。在虛擬機發(fā)展的過程中進化出不少垃圾回收算法,比如:
- 標(biāo)記-清除算法
- 復(fù)制算法
- 標(biāo)記-整理算法
- 分代收集算法
在實際中用到的回收器都是這幾種算法的組合,比如從VisualVM中看到的內(nèi)存是這樣的(需要明白各部分都是怎樣互相配合的):
整體上來看是分代收集算法,而S0、S1這兩部分可以看做是標(biāo)記-整理算法。那么第三個問題:常見的CMS垃圾回收器的執(zhí)行流程是怎樣的?
- 初始標(biāo)記:GC Roots直接關(guān)聯(lián)的對象。
- 并發(fā)標(biāo)記:Root Tracing。
- 重新標(biāo)記:修復(fù)由于程序運行導(dǎo)致標(biāo)記產(chǎn)生變動。
- 并發(fā)清除
具體如下圖所示:
可以看到只有在初始標(biāo)記和重新標(biāo)記的時候才需要Stop The World,其他都是和用戶線程一起執(zhí)行,并行執(zhí)行的過程會消耗掉一些CPU資源。
代碼執(zhí)行
把Java源碼丟給JVM肯定是不能執(zhí)行的,需要先用javac編譯成class文件才行,那么第一個問題:class文件的結(jié)構(gòu)是怎樣的?
- 常量池
- 訪問標(biāo)志
- 類索引、父類索引和接口索引
- 字段表
- 方法表
- 屬性表
虛擬機規(guī)范并沒有規(guī)定在什么時候要加載類,但是規(guī)定了在遇到new、反射、父類、Main的時候需要初始化完成。整個類的生命周期如下:
在虛擬機中通過ClassLoader來進行類的加載,這地方需要明白:
- 兩個類是否相同,除了類名外還需要判斷ClassLoader是否相同。
- 雙親委派模式并不是一個強制約束。
在類加載完成之后就可以開始執(zhí)行了,和線程運轉(zhuǎn)相關(guān)的東西都放在棧幀中,其結(jié)構(gòu)如下:
屬性 | 作用/含義 |
---|---|
局部變量表 | 方法參數(shù)及方法內(nèi)部定義的局部變量 |
操作數(shù)棧 | 用來被指令操作 |
動態(tài)連接 | 指向運行時常量池中該棧幀所屬方法的引用 |
方法返回地址 | 上層方法調(diào)用本方法的位置 |
附加信息 | 調(diào)試信息等 |
執(zhí)行中具體調(diào)用哪個方法是個頭疼的問題,需要處理:
- 靜態(tài)分派:相同名稱、不同參數(shù)類型的方法。
- 動態(tài)分派:繼承中復(fù)寫的方法。
字節(jié)碼中的指令都是基于棧的操作,比如要完成1+1這樣的計算,對應(yīng)的指令如下:
iconst_1 // 將常量1壓入棧
iconst_1
iadd // 把棧頂?shù)膬蓚€值相加并出棧,然后把結(jié)果放回棧
istore_0 // 將棧頂?shù)闹捣诺骄植孔兞勘淼?個Solt
解釋執(zhí)行的好處是下載后啟動速度快,但是確定也非常明顯:運行速度慢。JIT正是用來解決這個問題的,能夠?qū)?strong>多次調(diào)用的方法、多次執(zhí)行的循環(huán)體編譯成本地代碼。
優(yōu)化是個很好玩的題目,記得在參加一次變成比賽的時候用gcc -O3編譯之后的代碼把printf()都沒輸出了。。在JIT中比較常見的優(yōu)化手段有:
手段 | 描述 |
---|---|
公共子表達式消除 | 如果一個表達式已經(jīng)計算過了,那么后面不需要重復(fù)計算 |
數(shù)組范圍檢查消除 | 并不是必須一次不漏地檢查 |
方法內(nèi)聯(lián) | 把代碼復(fù)制到調(diào)用方法中 |
逃逸分析 | 判斷對象是否可能被方法外引用到 |
程序執(zhí)行一定會涉及到內(nèi)存操作,在Java中定義了八種操作來完成:
操作 | 含義 |
---|---|
lock | 把一個變量標(biāo)識為線程獨占狀態(tài) |
unlock | 釋放變量 |
read | 將變量從主存讀取到工作內(nèi)存 |
load | 將read到的變量值放入工作內(nèi)存中的副本 |
use | 將工作內(nèi)存中的變量傳遞給執(zhí)行引擎 |
assign | 引擎返回的值傳遞給工作內(nèi)存中的副本 |
store | 將工作內(nèi)存中的變量傳遞給主存 |
write | 把從工作內(nèi)存得到的變量寫入主存對應(yīng)的變量中 |
這里有必要講一下volatile的作用,在使用到的時候能明白下面兩條即可:
- 保證變量對所有線程是可見的。
- 禁止指令重排優(yōu)化。
如果Java中所有的操作都需要程序員來控制的話,會有大量的重復(fù)代碼,而且寫起來很累,那么我們可以通過先行發(fā)生原則來判斷并行的兩個操作是否存在沖突:
- 程序次序規(guī)則:單線程內(nèi)按照程序書寫順序。
- 管程鎖定規(guī)則:unlock必須在lock之前。
- volatile變量規(guī)則:寫操作先行發(fā)生于讀操作。
- 線程啟動規(guī)則:Thread.start()先于線程的其他任意方法。
- 線程終止規(guī)則:線程中所有的操作都先于對此線程的終止檢測。
- 線程中斷規(guī)則:interrupt()先于中斷檢測。
- 對象終結(jié)規(guī)則:對象的初始化完成先于它的finalize()方法。
- 傳遞規(guī)則:如果A先于B、B先于C,那么A先于C。
Thread的底層實現(xiàn)還是比較麻煩的,但是最起碼應(yīng)該知道Thread的狀態(tài)是如何進行轉(zhuǎn)換:
最后,常見的同步方式是synchronized或者aqs的各種實現(xiàn),這里就不講了,因為每個都足夠?qū)懸淮笃?/p>