面試官:說說JVM內(nèi)存整體結(jié)構(gòu)?線程私有還是共享的?
JVM 整體架構(gòu),中間部分就是 Java 虛擬機(jī)定義的各種運(yùn)行時(shí)數(shù)據(jù)區(qū)域。
圖片
Java 虛擬機(jī)定義了若干種程序運(yùn)行期間會(huì)使用到的運(yùn)行時(shí)數(shù)據(jù)區(qū),其中有一些會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出而銷毀。另外一些則是與線程一一對(duì)應(yīng)的,這些與線程一一對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開始和結(jié)束而創(chuàng)建和銷毀。
線程私有:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū)
線程共享:堆、方法區(qū), 堆外內(nèi)存(Java7的永久代或JDK8的元空間、代碼緩存)
什么是程序計(jì)數(shù)器(線程私有)?
PC 寄存器用來存儲(chǔ)指向下一條指令的地址,即將要執(zhí)行的指令代碼。由執(zhí)行引擎讀取下一條指令。
PC寄存器為什么會(huì)被設(shè)定為線程私有的?
多線程在一個(gè)特定的時(shí)間段內(nèi)只會(huì)執(zhí)行其中某一個(gè)線程方法,CPU會(huì)不停的做任務(wù)切換,這樣必然會(huì)導(dǎo)致經(jīng)常中斷或恢復(fù)。為了能夠準(zhǔn)確的記錄各個(gè)線程正在執(zhí)行的當(dāng)前字節(jié)碼指令地址,所以為每個(gè)線程都分配了一個(gè)PC寄存器,每個(gè)線程都獨(dú)立計(jì)算,不會(huì)互相影響。
什么是虛擬機(jī)棧(線程私有)?
主管 Java 程序的運(yùn)行,它保存方法的局部變量、部分結(jié)果,并參與方法的調(diào)用和返回。每個(gè)線程在創(chuàng)建的時(shí)候都會(huì)創(chuàng)建一個(gè)虛擬機(jī)棧,其內(nèi)部保存一個(gè)個(gè)的棧幀(Stack Frame),對(duì)應(yīng)著一次次 Java 方法調(diào)用,是線程私有的,生命周期和線程一致。
特點(diǎn)?
- 棧是一種快速有效的分配存儲(chǔ)方式,訪問速度僅次于程序計(jì)數(shù)器
- JVM 直接對(duì)虛擬機(jī)棧的操作只有兩個(gè):每個(gè)方法執(zhí)行,伴隨著入棧(進(jìn)棧/壓棧),方法執(zhí)行結(jié)束出棧
- 棧不存在垃圾回收問題
- 可以通過參數(shù)-Xss來設(shè)置線程的最大??臻g,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度
該區(qū)域有哪些異常?
- 如果采用固定大小的 Java 虛擬機(jī)棧,那每個(gè)線程的 Java 虛擬機(jī)棧容量可以在線程創(chuàng)建的時(shí)候獨(dú)立選定。如果線程請(qǐng)求分配的棧容量超過 Java 虛擬機(jī)棧允許的最大容量,Java 虛擬機(jī)將會(huì)拋出一個(gè) StackOverflowError 異常
- 如果 Java 虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,并且在嘗試擴(kuò)展的時(shí)候無法申請(qǐng)到足夠的內(nèi)存,或者在創(chuàng)建新的線程時(shí)沒有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧,那 Java 虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常
棧幀的內(nèi)部結(jié)構(gòu)?
- 局部變量表(Local Variables)
- 操作數(shù)棧(Operand Stack)(或稱為表達(dá)式棧)
- 動(dòng)態(tài)鏈接(Dynamic Linking):指向運(yùn)行時(shí)常量池的方法引用
- 方法返回地址(Return Address):方法正常退出或異常退出的地址
- 一些附加信息
圖片
Java虛擬機(jī)棧如何進(jìn)行方法計(jì)算的?
以如下代碼為例:
private static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}
可以通過jsclass 等工具查看bytecode
圖片
壓棧的步驟如下:
0: iconst_0 // 0壓棧
1: istore_2 // 彈出int,存放于局部變量2
2: iload_0 // 把局部變量0壓棧
3: iload_1 // 局部變量1壓棧
4: iadd //彈出2個(gè)變量,求和,結(jié)果壓棧
5: istore_2 //彈出結(jié)果,放于局部變量2
6: iload_2 //局部變量2壓棧
7: ireturn //返回
如果計(jì)算100+98的值,那么操作數(shù)棧的變化如下圖:
圖片
什么是本地方法棧(線程私有)?
- 本地方法接口
一個(gè) Native Method 就是一個(gè) Java 調(diào)用非 Java 代碼的接口。我們知道的 Unsafe 類就有很多本地方法。
- 本地方法棧(Native Method Stack)
Java 虛擬機(jī)棧用于管理 Java 方法的調(diào)用,而本地方法棧用于管理本地方法的調(diào)用
什么是方法區(qū)(線程共享)?
方法區(qū)(method area)只是 JVM 規(guī)范中定義的一個(gè)概念,用于存儲(chǔ)類信息、常量池、靜態(tài)變量、JIT編譯后的代碼等數(shù)據(jù),并沒有規(guī)定如何去實(shí)現(xiàn)它,不同的廠商有不同的實(shí)現(xiàn)。而永久代(PermGen)**是 **Hotspot** 虛擬機(jī)特有的概念, Java8 的時(shí)候又被**元空間取代了,永久代和元空間都可以理解為方法區(qū)的落地實(shí)現(xiàn)。
JDK1.8之前調(diào)節(jié)方法區(qū)大?。?/p>
-XX:PermSize=N //方法區(qū)(永久代)初始大小
-XX:MaxPermSize=N //方法區(qū)(永久代)最大大小,超出這個(gè)值將會(huì)拋出OutOfMemoryError
JDK1.8開始方法區(qū)(HotSpot的永久代)被徹底刪除了,取而代之的是元空間,元空間直接使用的是本機(jī)內(nèi)存。參數(shù)設(shè)置:
-XX:MetaspaceSize=N //設(shè)置Metaspace的初始(和最小大?。?-XX:MaxMetaspaceSize=N //設(shè)置Metaspace的最大大小
棧、堆、方法區(qū)的交互關(guān)系
圖片
永久代和元空間內(nèi)存使用上的差異?
Java虛擬機(jī)規(guī)范中只定義了方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量和即時(shí)編譯后的代碼等數(shù)據(jù)
- jdk1.7開始符號(hào)引用存儲(chǔ)在native heap中,字符串常量和靜態(tài)類型變量存儲(chǔ)在普通的堆區(qū)中,但分離的并不徹底,此時(shí)永久代中還保存另一些與類的元數(shù)據(jù)無關(guān)的雜項(xiàng)
- jdk8后HotSpot 原永久代中存儲(chǔ)的類的元數(shù)據(jù)將存儲(chǔ)在metaspace中,而類的靜態(tài)變量和字符串常量將放在Java堆中,metaspace是方法區(qū)的一種實(shí)現(xiàn),只不過它使用的不是虛擬機(jī)內(nèi)的內(nèi)存,而是本地內(nèi)存。在元空間中保存的數(shù)據(jù)比永久代中純粹很多,就只是類的元數(shù)據(jù),這些信息只對(duì)編譯期或JVM的運(yùn)行時(shí)有用。
- 永久代有一個(gè)JVM本身設(shè)置固定大小上線,無法進(jìn)行調(diào)整,而元空間使用的是直接內(nèi)存,受本機(jī)可用內(nèi)存的限制,并且永遠(yuǎn)不會(huì)得到j(luò)ava.lang.OutOfMemoryError。
- 符號(hào)引用沒有存在元空間中,而是存在native heap中,這是兩個(gè)方式和位置,不過都可以算作是本地內(nèi)存,在虛擬機(jī)之外進(jìn)行劃分,沒有設(shè)置限制參數(shù)時(shí)只受物理內(nèi)存大小限制,即只有占滿了操作系統(tǒng)可用內(nèi)存后才OOM。
堆區(qū)內(nèi)存是怎么細(xì)分的?
對(duì)于大多數(shù)應(yīng)用,Java 堆是 Java 虛擬機(jī)管理的內(nèi)存中最大的一塊,被所有線程共享。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)據(jù)都在這里分配內(nèi)存。
為了進(jìn)行高效的垃圾回收,虛擬機(jī)把堆內(nèi)存邏輯上劃分成三塊區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):
- 新生帶(年輕代):新對(duì)象和沒達(dá)到一定年齡的對(duì)象都在新生代
- 老年代(養(yǎng)老區(qū)):被長時(shí)間使用的對(duì)象,老年代的內(nèi)存空間應(yīng)該要比年輕代更大
Java 虛擬機(jī)規(guī)范規(guī)定,Java 堆可以是處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,像磁盤空間一樣。實(shí)現(xiàn)時(shí),既可以是固定大小,也可以是可擴(kuò)展的,主流虛擬機(jī)都是可擴(kuò)展的(通過 -Xmx 和 -Xms 控制),如果堆中沒有完成實(shí)例分配,并且堆無法再擴(kuò)展時(shí),就會(huì)拋出 OutOfMemoryError 異常。
- 年輕代 (Young Generation)
年輕代是所有新對(duì)象創(chuàng)建的地方。當(dāng)填充年輕代時(shí),執(zhí)行垃圾收集。這種垃圾收集稱為 Minor GC。年輕一代被分為三個(gè)部分——伊甸園(Eden Memory)和兩個(gè)幸存區(qū)(Survivor Memory,被稱為from/to或s0/s1),默認(rèn)比例是8:1:1
- 大多數(shù)新創(chuàng)建的對(duì)象都位于 Eden 內(nèi)存空間中
- 當(dāng) Eden 空間被對(duì)象填充時(shí),執(zhí)行Minor GC,并將所有幸存者對(duì)象移動(dòng)到一個(gè)幸存者空間中
- Minor GC 檢查幸存者對(duì)象,并將它們移動(dòng)到另一個(gè)幸存者空間。所以每次,一個(gè)幸存者空間總是空的
- 經(jīng)過多次 GC 循環(huán)后存活下來的對(duì)象被移動(dòng)到老年代。通常,這是通過設(shè)置年輕一代對(duì)象的年齡閾值來實(shí)現(xiàn)的,然后他們才有資格提升到老一代
- 老年代(Old Generation)
舊的一代內(nèi)存包含那些經(jīng)過許多輪小型 GC 后仍然存活的對(duì)象。通常,垃圾收集是在老年代內(nèi)存滿時(shí)執(zhí)行的。老年代垃圾收集稱為 主GC(Major GC),通常需要更長的時(shí)間。
大對(duì)象直接進(jìn)入老年代(大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象)。這樣做的目的是避免在 Eden 區(qū)和兩個(gè)Survivor 區(qū)之間發(fā)生大量的內(nèi)存拷貝
圖片
JVM中對(duì)象在堆中的生命周期?
- 在 JVM 內(nèi)存模型的堆中,堆被劃分為新生代和老年代
新生代又被進(jìn)一步劃分為 Eden區(qū) 和 Survivor區(qū),Survivor 區(qū)由 From Survivor 和 To Survivor 組成
- 當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),對(duì)象會(huì)被優(yōu)先分配到新生代的 Eden 區(qū)
此時(shí) JVM 會(huì)給對(duì)象定義一個(gè)對(duì)象年輕計(jì)數(shù)器(-XX:MaxTenuringThreshold)
- 當(dāng) Eden 空間不足時(shí),JVM 將執(zhí)行新生代的垃圾回收(Minor GC)
JVM 會(huì)把存活的對(duì)象轉(zhuǎn)移到 Survivor 中,并且對(duì)象年齡 +1
對(duì)象在 Survivor 中同樣也會(huì)經(jīng)歷 Minor GC,每經(jīng)歷一次 Minor GC,對(duì)象年齡都會(huì)+1
- 如果分配的對(duì)象超過了-XX:PetenureSizeThreshold,對(duì)象會(huì)直接被分配到老年代
JVM中對(duì)象的分配過程?
為對(duì)象分配內(nèi)存是一件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù),JVM 的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問題,并且由于內(nèi)存分配算法和內(nèi)存回收算法密切相關(guān),所以還需要考慮 GC 執(zhí)行完內(nèi)存回收后是否會(huì)在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。
- new 的對(duì)象先放在伊甸園區(qū),此區(qū)有大小限制
- 當(dāng)伊甸園的空間填滿時(shí),程序又需要?jiǎng)?chuàng)建對(duì)象,JVM 的垃圾回收器將對(duì)伊甸園區(qū)進(jìn)行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷毀。再加載新的對(duì)象放到伊甸園區(qū)
- 然后將伊甸園中的剩余對(duì)象移動(dòng)到幸存者 0 區(qū)
- 如果再次觸發(fā)垃圾回收,此時(shí)上次幸存下來的放到幸存者 0 區(qū),如果沒有回收,就會(huì)放到幸存者 1 區(qū)
- 如果再次經(jīng)歷垃圾回收,此時(shí)會(huì)重新放回幸存者 0 區(qū),接著再去幸存者 1 區(qū)
- 什么時(shí)候才會(huì)去養(yǎng)老區(qū)呢?默認(rèn)是 15 次回收標(biāo)記
- 在養(yǎng)老區(qū),相對(duì)悠閑。當(dāng)養(yǎng)老區(qū)內(nèi)存不足時(shí),再次觸發(fā) Major GC,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理
- 若養(yǎng)老區(qū)執(zhí)行了 Major GC 之后發(fā)現(xiàn)依然無法進(jìn)行對(duì)象的保存,就會(huì)產(chǎn)生 OOM 異常
什么是 TLAB (Thread Local Allocation Buffer)?
- 從內(nèi)存模型而不是垃圾回收的角度,對(duì) Eden 區(qū)域繼續(xù)進(jìn)行劃分,JVM 為每個(gè)線程分配了一個(gè)私有緩存區(qū)域,它包含在 Eden 空間內(nèi)
- 多線程同時(shí)分配內(nèi)存時(shí),使用 TLAB 可以避免一系列的非線程安全問題,同時(shí)還能提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方式稱為快速分配策略
- OpenJDK 衍生出來的 JVM 大都提供了 TLAB 設(shè)計(jì)
為什么要有 TLAB ?
- 堆區(qū)是線程共享的,任何線程都可以訪問到堆區(qū)中的共享數(shù)據(jù)
- 由于對(duì)象實(shí)例的創(chuàng)建在 JVM 中非常頻繁,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
- 為避免多個(gè)線程操作同一地址,需要使用加鎖等機(jī)制,進(jìn)而影響分配速度
盡管不是所有的對(duì)象實(shí)例都能夠在 TLAB 中成功分配內(nèi)存,但 JVM 確實(shí)是將 TLAB 作為內(nèi)存分配的首選。
在程序中,可以通過 -XX:UseTLAB 設(shè)置是否開啟 TLAB 空間。
默認(rèn)情況下,TLAB 空間的內(nèi)存非常小,僅占有整個(gè) Eden 空間的 1%,我們可以通過 -XX:TLABWasteTargetPercent 設(shè)置 TLAB 空間所占用 Eden 空間的百分比大小。
一旦對(duì)象在 TLAB 空間分配內(nèi)存失敗時(shí),JVM 就會(huì)嘗試著通過使用加鎖機(jī)制確保數(shù)據(jù)操作的原子性,從而直接在 Eden 空間中分配內(nèi)存。