Java內(nèi)存區(qū)域全解析:一次被面試官“逼瘋”的回憶錄
哈嘍大家好,我是你們的老朋友小米,今年31歲,Java搬磚第10年,Bug修復(fù)無(wú)數(shù),面試經(jīng)驗(yàn)滿滿。今天咱不講框架、不講中間件,我們來(lái)聊聊——JVM 內(nèi)存區(qū)域這個(gè)老生常談的面試題。
題目是這樣問(wèn)的:“請(qǐng)你說(shuō)一下 JVM 的主要組成部分以及各自的作用。”
看到這個(gè)問(wèn)題,我眼前一黑,腦子開(kāi)始高速回憶各種堆啊、棧啊、方法區(qū)啊……還真別說(shuō),這問(wèn)題看似基礎(chǔ),答起來(lái)真有點(diǎn)講究。
所以,今天我就來(lái)和大家聊聊,當(dāng)年我是怎么一步一步搞懂這道題,并順利在面試中脫穎而出的——順便,也讓你少走點(diǎn)彎路。
一個(gè)關(guān)于“卡殼”的故事
先說(shuō)說(shuō)我第一次被問(wèn)到這個(gè)問(wèn)題的場(chǎng)景。
那是一家特別講究“基礎(chǔ)扎實(shí)”的互聯(lián)網(wǎng)公司,面試官大叔不茍言笑,一開(kāi)口就問(wèn):
“JVM的主要內(nèi)存區(qū)域有哪些?能說(shuō)下它們各自的作用嗎?”
我當(dāng)時(shí)一愣,“呃……堆、棧、方法區(qū)、程序計(jì)數(shù)器,還有本地方法棧?”
大叔點(diǎn)點(diǎn)頭:“繼續(xù)?!?/p>
我繼續(xù):“嗯……堆是用來(lái)存放對(duì)象實(shí)例的,棧用來(lái)存放局部變量……方法區(qū)用來(lái)存放類信息……”
說(shuō)到這,我就卡殼了。
那個(gè)瞬間,空氣凝固了三秒鐘,我的腦門(mén)開(kāi)始冒汗。面試官嘆了口氣,說(shuō):
“你還是回去好好復(fù)習(xí)一下《深入理解JVM》吧?!?/p>
那天我落荒而逃,回到家就開(kāi)始痛定思痛,打開(kāi)《深入理解Java虛擬機(jī)》,開(kāi)啟了長(zhǎng)達(dá)三晚的“Java內(nèi)存苦修”模式。
JVM 內(nèi)存結(jié)構(gòu)大揭秘
要搞懂 JVM 的內(nèi)存區(qū)域,先來(lái)認(rèn)識(shí)一下 JVM 的整體結(jié)構(gòu)(放心,不用背圖,我用故事講清楚):
JVM 就像一個(gè)智能大管家,掌管著內(nèi)存的調(diào)配,分成幾個(gè)“分區(qū)”來(lái)處理不同的事兒。它的主要組成包括:
接下來(lái)我用一個(gè)比喻幫你理解。
用“咖啡師工作室”類比 JVM 內(nèi)存區(qū)域
想象 JVM 是一個(gè)咖啡店的工作室,里面有一位超級(jí)忙碌的咖啡師,他每天需要處理各種訂單、材料、配方和機(jī)器指令。我們把這些比喻一下:
1、程序計(jì)數(shù)器:你的“備忘小便簽”
每個(gè)咖啡師(線程)都會(huì)拿著一張便簽紙,寫(xiě)著他現(xiàn)在做到哪一步,比如“正在煮咖啡”,“正在加奶油”。這就是程序計(jì)數(shù)器——它記錄當(dāng)前線程正在執(zhí)行哪一行字節(jié)碼指令。
它是線程私有的,因?yàn)槊總€(gè)咖啡師做的事不一樣,不能共用便簽。
2、Java虛擬機(jī)棧:你的“工具箱”
每個(gè)咖啡師有一個(gè)工具箱,里面有當(dāng)前正在處理訂單的方法調(diào)用棧幀,裝著局部變量(杯子、糖、勺子)、返回地址等。
方法一調(diào)用,就入棧;方法一執(zhí)行完,就出棧。棧深不夠,就棧溢出了(StackOverflowError)。
3、本地方法棧:備用老設(shè)備
有時(shí)候需要用上古機(jī)器來(lái)完成任務(wù),比如一個(gè)只能通過(guò)老式電話撥號(hào)器完成的任務(wù),這些“非Java”的方法叫做本地方法(Native)。本地方法棧專門(mén)為它們服務(wù)。
4、堆:所有原材料的大倉(cāng)庫(kù)
你要制作一杯拿鐵,需要咖啡豆、牛奶、杯子等等,這些對(duì)象都統(tǒng)一放在堆里。
Java中所有對(duì)象實(shí)例和數(shù)組都放在堆里。JVM的垃圾回收器(GC)主要盯著這塊區(qū)域,及時(shí)回收用不到的原材料。
5、方法區(qū)(元空間):配方手冊(cè)與規(guī)則存檔
這里是存儲(chǔ)類的結(jié)構(gòu)信息、方法、常量池、靜態(tài)變量的地方。以前叫方法區(qū),JDK8之后移到了本地內(nèi)存中,叫元空間(Metaspace)。
每一塊區(qū)域的深度八卦
來(lái),我們一個(gè)個(gè)深入聊聊它們到底“藏”了多少秘密。
1、程序計(jì)數(shù)器
- 非常小,但很重要。
- 如果線程切換,就靠它記錄上一次執(zhí)行的位置。
- 它是唯一一個(gè)不會(huì)內(nèi)存溢出的區(qū)域!
2、虛擬機(jī)棧
- 方法每調(diào)用一次,就生成一個(gè)棧幀。
- 局部變量表、操作數(shù)棧、方法返回地址等都在里面。
- 如果方法遞歸太深,可能會(huì)導(dǎo)致StackOverflowError。
- 如果棧擴(kuò)展失敗,則可能是OutOfMemoryError。
3、本地方法棧
- 用得少,但別小看。
- JNI調(diào)用C函數(shù)時(shí)就要靠它。
- 異常一般不容易發(fā)生,除非調(diào)用了大量 native 方法。
4、堆(Heap)
- Java內(nèi)存最大的一塊。
- 所有的 new 出來(lái)的對(duì)象都在這里。
- GC經(jīng)常掃它,有“新生代”“老年代”的分代概念。
- 如果對(duì)象太多來(lái)不及回收,就會(huì)出現(xiàn)OutOfMemoryError: Java heap space。
5、方法區(qū)(元空間)
- 存儲(chǔ)類元數(shù)據(jù)、常量池、靜態(tài)變量。
- JDK 8 之前叫永久代(PermGen),容易報(bào)OutOfMemoryError: PermGen space。
- JDK 8 之后改為元空間,直接用本地內(nèi)存,更靈活,但也可能爆OutOfMemoryError: Metaspace。
如何在面試中回答這個(gè)問(wèn)題?
回到那個(gè)面試官的問(wèn)題——“說(shuō)一下 JVM 的主要組成部分及其作用?”
我在第二次面試時(shí),這樣回答的:
“JVM主要內(nèi)存結(jié)構(gòu)可以分為五大部分:
- 程序計(jì)數(shù)器(線程私有):記錄字節(jié)碼指令執(zhí)行位置;
- Java虛擬機(jī)棧(線程私有):存儲(chǔ)方法調(diào)用相關(guān)的棧幀、局部變量;
- 本地方法棧(線程私有):調(diào)用native方法時(shí)使用;
- Java堆(線程共享):存儲(chǔ)所有對(duì)象實(shí)例,是GC關(guān)注的重點(diǎn);
- 方法區(qū)/元空間(線程共享):保存類結(jié)構(gòu)、靜態(tài)變量、常量池等元信息。
其中,堆和方法區(qū)是線程共享的,其余是線程私有的。GC主要關(guān)注堆區(qū),JDK8后永久代被元空間替代。”
大叔聽(tīng)完,點(diǎn)了點(diǎn)頭,說(shuō):“這個(gè)問(wèn)題你答得挺清楚的。”
我心里那塊大石頭總算落地。
小結(jié):一圖勝千言
最后我分享給大家一個(gè)總結(jié)圖:
圖片
記住關(guān)鍵點(diǎn):線程共享 vs 線程私有、GC關(guān)注堆、JDK8 改了方法區(qū)……
END
那場(chǎng)失敗的面試,曾讓我對(duì) JVM 的印象變得“又怕又恨”。但現(xiàn)在回頭看,它成了我技術(shù)成長(zhǎng)的分水嶺。
如果你也曾在 JVM 面前崩潰過(guò),別氣餒,它確實(shí)難,但也值得!