打卡每日10道面試題——JVM篇
1、請(qǐng)你簡(jiǎn)述一下 Java 內(nèi)存結(jié)構(gòu)(運(yùn)行時(shí)數(shù)據(jù)區(qū))
如圖所示:
① 程序計(jì)數(shù)器
- 程序計(jì)數(shù)器:線(xiàn)程私有。一塊較小的內(nèi)存空間,程序計(jì)數(shù)器用于保存 JVM 中下一條所要執(zhí)行的字節(jié)碼指令的地址!如果正在執(zhí)行的是 Native 方法,則這個(gè)計(jì)數(shù)器值則為空。程序計(jì)數(shù)器在硬件層面是通過(guò) 寄存器 實(shí)現(xiàn)的!
Java指令執(zhí)行流程:
- java代碼源文件經(jīng)過(guò)編譯為.class 二進(jìn)制字節(jié)碼文件。
- class 文件中的每一條二進(jìn)制字節(jié)碼指令(JVM指令) 通過(guò) 解釋器 轉(zhuǎn)換成 機(jī)器碼 然后就可以被 CPU 執(zhí)行了!
- 當(dāng) 解釋器 將一條 jvm 指令轉(zhuǎn)換成 機(jī)器碼 后,同時(shí)會(huì)向程序計(jì)數(shù)器 遞交下一條 jvm 指令的執(zhí)行地址!
如圖所示:
② 虛擬機(jī)棧
- 虛擬機(jī)棧:線(xiàn)程私有,它的生命周期與線(xiàn)程相同。虛擬機(jī)棧是Java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從被調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。
- 每個(gè)棧由多個(gè)棧幀(Frame) 組成,對(duì)應(yīng)著每個(gè)方法運(yùn)行時(shí)所占用的內(nèi)存。
- 個(gè)線(xiàn)程只能有一個(gè)活動(dòng)棧幀,對(duì)應(yīng)著當(dāng)前正在執(zhí)行的方法,當(dāng)方法執(zhí)行時(shí)壓入棧,方法執(zhí)行完畢后彈出棧。
- 方法體中的引用變量和基本類(lèi)型的變量都在棧上,其他都在堆上。
實(shí)例代碼:
/** * @Auther: csp1999 * @Date: 2020/11/10/11:36 * @Description: 演示棧幀 */ public class Demo01 { public static void main(String[] args) { methodA(); } private static void methodA() { methodB(1, 2); } private static int methodB(int a, int b) { int c = a + b; return c; } }
流程分析:
我們打斷點(diǎn)來(lái)Debug 一下看一下方法執(zhí)行的流程:
接這往下走,使方法B執(zhí)行完畢:
然后方法A 執(zhí)行完畢,其對(duì)應(yīng)的棧幀出棧,main 方法對(duì)應(yīng)的棧幀為活動(dòng)棧幀;最后main執(zhí)行完畢,棧幀出棧,虛擬機(jī)棧為空,代碼運(yùn)行結(jié)束!
③ 本地方法棧
本地方法棧:線(xiàn)程私有。本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過(guò)是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。
一些帶有native 關(guān)鍵字修飾的方法就是需要JAVA去調(diào)用本地的C或者C++方法,因?yàn)镴AVA有時(shí)候沒(méi)法直接和操作系統(tǒng)底層交互,所以需要用到本地方法!
④ 堆
堆:線(xiàn)程共享。Java堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊,是被所有線(xiàn)程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。Java堆的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。 通過(guò)new關(guān)鍵字創(chuàng)建的對(duì)象都會(huì)被放在堆內(nèi)存。方法體中的引用變量和基本類(lèi)型的變量都在棧上,其他都在堆上。Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱(chēng)做“GC 堆”(Garbage)。-Xmx -Xms:JVM初始分配的堆內(nèi)存由-Xms指定,默認(rèn)是物理內(nèi)存的1/64。
⑤ 方法區(qū)
方法區(qū):線(xiàn)程共享。方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的 *類(lèi)信息(構(gòu)造方法、接口定義)、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼(字節(jié)碼)*等數(shù)據(jù)。 方法區(qū)在 JVM 啟動(dòng)的時(shí)候被創(chuàng)建,并且它的實(shí)際的物理內(nèi)存空間和 Java堆一樣都可以是不連續(xù)的, 關(guān)閉 Jvm 就會(huì)釋放這個(gè)區(qū)域的內(nèi)存。方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類(lèi),如果系統(tǒng)定義了太多的類(lèi),導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤:(java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace)。 注意:方法區(qū)時(shí)一種規(guī)范,而永久代和元空間是它的2種實(shí)現(xiàn)方式。
方法區(qū)的演進(jìn):
1.6 版本方法區(qū)是由 永久代 實(shí)現(xiàn)(使用堆內(nèi)存的一部分作為方法區(qū)),且由JVM 管理。由Class、ClassLoader、常量池(包括StringTable) 組成。
Jdk 1.7 版本仍有永久代,但已經(jīng)逐步 " 去永久代 ",StringTable、靜態(tài)變量從永久代移除,保存在堆中。
1.8 版本后,方法區(qū)交給本地內(nèi)存管理,而脫離了JVM,由元空間實(shí)現(xiàn)(元空間不再使用堆的內(nèi)存,而是使用本地內(nèi)存,即操作系統(tǒng)的內(nèi)存),由Class、ClassLoader、常量池(StringTable 被移到了堆中管理) 組成。
⑥ 運(yùn)行時(shí)常量池
常量池:可以看做是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的 類(lèi)名,方法名,參數(shù)類(lèi)型、字面量 等信息。
常量池是*.class文件中的,當(dāng)該類(lèi)被加載以后,它的常量池信息就會(huì)放入運(yùn)行時(shí)常量池,并把里面的符號(hào)地址變?yōu)檎鎸?shí)內(nèi)存地址。
運(yùn)行時(shí)常量池:是方法區(qū)的一部分。
String str = new String("hello");
上面的語(yǔ)句中變量 str 放在棧上,用 new 創(chuàng)建出來(lái)的字符串對(duì)象放在堆上,而hello這個(gè)字面量是放在堆中。
2、請(qǐng)問(wèn)jvm垃圾回收是否涉及棧內(nèi)存?
不需要。因?yàn)樘摂M機(jī)棧中是由一個(gè)個(gè)棧幀組成的,在方法執(zhí)行完畢后,對(duì)應(yīng)的棧幀就會(huì)被彈出棧。所以無(wú)需通過(guò)垃圾回收機(jī)制去回收內(nèi)存。
3、虛擬機(jī)棧內(nèi)存的分配越大越好嗎?
- 不是。因?yàn)槲锢韮?nèi)存是一定的,棧內(nèi)存越大,可以支持更多的遞歸調(diào)用,但是可執(zhí)行的線(xiàn)程數(shù)就會(huì)越少。
我們來(lái)看一張圖:
舉例:如果物理內(nèi)存是500M(假設(shè)),如果一個(gè)線(xiàn)程所能分配的棧內(nèi)存為2M的話(huà),那么可以有250個(gè)線(xiàn)程。而如果一個(gè)線(xiàn)程分配棧內(nèi)存占5M的話(huà),那么最多只能有100 個(gè)線(xiàn)程同時(shí)執(zhí)行!
4、從JVM的角度分析,方法內(nèi)的局部變量是否是線(xiàn)程安全的?
我們通過(guò)兩張圖去分析一下:
情況一:
情況二:
從圖中得出:局部變量如果是靜態(tài)的可以被多個(gè)線(xiàn)程共享,那么就存在線(xiàn)程安全問(wèn)題。如果是非靜態(tài)的只存在于某個(gè)方法作用范圍內(nèi),被線(xiàn)程私有,那么就是線(xiàn)程安全的!
再來(lái)看一個(gè)案例:
/** * 局部變量的線(xiàn)程安全問(wèn)題 */ public class Demo02 { public static void main(String[] args) {// main 函數(shù)主線(xiàn)程 StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); new Thread(() -> {// Thread新創(chuàng)建的線(xiàn)程 m2(sb); }).start(); } public static void m1() { // sb 作為方法m1()內(nèi)部的局部變量,是線(xiàn)程私有的 ---> 線(xiàn)程安全 StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { // sb 作為方法m2()外部的傳遞來(lái)的參數(shù),sb 不在方法m2()的作用范圍內(nèi) // 不是線(xiàn)程私有的 ---> 非線(xiàn)程安全 sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { // sb 作為方法m3()內(nèi)部的局部變量,是線(xiàn)程私有的 StringBuilder sb = new StringBuilder();// sb 為引用類(lèi)型的變量 sb.append(1); sb.append(2); sb.append(3); return sb;// 然而方法m3()將sb返回,sb逃離了方法m3()的作用范圍,且sb是引用類(lèi)型的變量 // 其他線(xiàn)程也可以拿到該變量的 ---> 非線(xiàn)程安全 // 如果sb是非引用類(lèi)型,即基本類(lèi)型(int/char/float...)變量的話(huà),逃離m3()作用范圍后,則不會(huì)存在線(xiàn)程安全 } }
所以,該面試題答案是:
如果方法內(nèi)局部變量沒(méi)有逃離方法的作用范圍,則是線(xiàn)程安全的。如果局部變量引用了對(duì)象,并逃離了方法的作用范圍,則需要考慮線(xiàn)程安全問(wèn)題。
5、虛擬機(jī)棧內(nèi)存溢出的情況有哪些?
- 1.虛擬機(jī)棧中,棧幀過(guò)多(方法無(wú)限遞歸)導(dǎo)致棧內(nèi)存溢出,這種情況比較常見(jiàn)!
- 2.每個(gè)棧幀所占用內(nèi)存過(guò)大(某個(gè)/某幾個(gè)棧幀內(nèi)存直接超過(guò)虛擬機(jī)棧最大內(nèi)存),這種情況比較少見(jiàn)!
如圖所示,就是棧中棧幀過(guò)多的情況:
6、請(qǐng)你說(shuō)一下JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)方法區(qū)的演進(jìn)?
- 1.6 版本方法區(qū)是由永久代實(shí)現(xiàn)(使用堆內(nèi)存的一部分作為方法區(qū)),且由JVM 管理。由Class、ClassLoader、常量池(包括StringTable) 組成。
- 靜態(tài)變量就存放在永久代(方法區(qū))上。
- Jdk 1.7 版本仍有永久代,但已經(jīng)逐步 " 去永久代 ",StringTable、靜態(tài)變量從永久代移除,保存在堆中。
- 1.8 版本后,方法區(qū)交給本地內(nèi)存管理,而脫離了JVM,由元空間實(shí)現(xiàn)(元空間不再使用堆的內(nèi)存,而是使用本地內(nèi)存,即操作系統(tǒng)的內(nèi)存),由Class、ClassLoader、常量池(StringTable 被移到了堆中管理) 組成。
- 靜態(tài)變量、StringTable 存放在堆中!
為什么要用元空間取代永久代?
因?yàn)橛谰么幸韵聨讉€(gè)弊端:
① 字符串常量池存在于永久代中,在大量使用字符串的情況下,非常容易出現(xiàn)OOM的異常。
② JVM加載的class的總數(shù),方法的大小等都很難確定,因此對(duì)永久代大小的指定難以確定。太小的永久代容易導(dǎo)致永久代內(nèi)存溢出,太大的永久代則容易導(dǎo)致虛擬機(jī)內(nèi)存緊張,空間浪費(fèi)。
③ 永久代進(jìn)行調(diào)優(yōu)很困難:方法區(qū)的垃圾收集主要回收兩部分,常量池中廢棄的常量和不再使用的類(lèi)。而不再使用的類(lèi)或類(lèi)的加載器回收比較復(fù)雜,F(xiàn)ULL GC 的時(shí)間長(zhǎng)。
7、請(qǐng)問(wèn)Java虛擬機(jī)中有哪些類(lèi)加載器?
以 JDK 8 為例:
名稱(chēng) | 加載哪的類(lèi) | 說(shuō)明 |
---|---|---|
Bootstrap ClassLoader(啟動(dòng)類(lèi)加載器) | JAVA_HOME/jre/lib | 無(wú)法直接訪(fǎng)問(wèn) |
Extension ClassLoader(擴(kuò)展類(lèi)加載器) | JAVA_HOME/jre/lib/ext | 上級(jí)為 Bootstrap,顯示為 null |
Application ClassLoader(應(yīng)用程序類(lèi)加載器) | classpath | 上級(jí)為 Extension |
自定義類(lèi)加載器 | 自定義 | 上級(jí)為 Application |
類(lèi)加載器的優(yōu)先級(jí)(由高到低):啟動(dòng)類(lèi)加載器 -> 擴(kuò)展類(lèi)加載器 -> 應(yīng)用程序類(lèi)加載器 -> 自定義類(lèi)加載器。
- **啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):**這個(gè)類(lèi)加載器負(fù)責(zé)將存放在 JAVA_HOME/jre/lib 目錄中的,或者被-Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如rt.jar,名字不符合的類(lèi)庫(kù)即使放在lib目錄中也不會(huì)被加載)類(lèi)庫(kù)加載到虛擬機(jī)內(nèi)存中。
- **擴(kuò)展類(lèi)加載器(Extension ClassLoader):**這個(gè)加載器由 sun.misc.Launcher$ExtClassLoader 實(shí)現(xiàn),它負(fù)責(zé)加載JAVA_HOME/jre/lib/ext目錄中的,或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑中的所有類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。
- **應(yīng)用程序類(lèi)加載器(Application ClassLoader):**這個(gè)類(lèi)加載器由 sun.misc.Launcher$AppClassLoader 實(shí)現(xiàn)。由于這個(gè)類(lèi)加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱(chēng)它為系統(tǒng)類(lèi)加載器。它負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)上所指定的類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用這個(gè)類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。
- **自定義類(lèi)加載器:**用戶(hù)自定義的類(lèi)加載器。
8、請(qǐng)你說(shuō)一下類(lèi)的加載的過(guò)程?
類(lèi)加載的過(guò)程包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化。其中驗(yàn)證、準(zhǔn)備、解析統(tǒng)稱(chēng)為連接。
- 加載:通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流,在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象。
- 驗(yàn)證:確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
- 準(zhǔn)備:為靜態(tài)變量分配內(nèi)存并設(shè)置靜態(tài)變量初始值,這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類(lèi)型的零值。
- 解析:將常量池內(nèi)的符號(hào)引用替換為直接引用。
- 初始化:到了初始化階段,才真正開(kāi)始執(zhí)行類(lèi)中定義的 Java 初始化程序代碼。主要是靜態(tài)變量賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{})中的語(yǔ)句。
9、請(qǐng)你說(shuō)一下什么是雙親委派模型?
如圖所示:
什么是雙親委派模型?
如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己去加載。
為什么要使用雙親委派模型呢?(好處)
避免重復(fù)加載 + 避免核心類(lèi)篡改
- 采用雙親委派模式的是好處是Java類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,通過(guò)這種層級(jí)關(guān)可以避免類(lèi)的重復(fù)加載,當(dāng)父加載器已經(jīng)加載了該類(lèi)時(shí),就沒(méi)有必要子加載器再加載一次。
- 其次是考慮到安全因素,java 核心 api 中定義類(lèi)型不會(huì)被隨意替換,假設(shè)通過(guò)網(wǎng)絡(luò)傳遞一個(gè)名為 java.lang.Integer 的類(lèi),通過(guò)雙親委托模式傳遞到啟動(dòng)類(lèi)加載器,而啟動(dòng)類(lèi)加載器在核心Java API發(fā)現(xiàn)這個(gè)名字的類(lèi),發(fā)現(xiàn)該類(lèi)已被加載,并不會(huì)重新加載網(wǎng)絡(luò)傳遞的過(guò)來(lái)的 java.lang.Integer,而直接返回已加載過(guò)的 Integer.class,這樣便可以防止核心API庫(kù)被隨意篡改。
10、說(shuō)一下虛擬機(jī)棧和堆的區(qū)別?
① 物理地址方面的區(qū)別:
- 堆 的物理地址分配對(duì)對(duì)象是不連續(xù)的。因此性能慢些。
- 虛擬機(jī)棧 使用的是數(shù)據(jù)結(jié)構(gòu)中的棧,先進(jìn)后出的原則,物理地址分配是連續(xù)的。所以性能快。
② 內(nèi)存分配方面的區(qū)別:
- 堆 因?yàn)槭遣贿B續(xù)的,所以分配的內(nèi)存是在運(yùn)行期確認(rèn)的,因此大小不固定。一般堆大小遠(yuǎn)遠(yuǎn)大于虛擬機(jī)棧。
- 虛擬機(jī)棧 是連續(xù)的,所以分配的內(nèi)存大小要在編譯期就確認(rèn),大小是固定的。
③ 存放的內(nèi)容方面的區(qū)別:
- 堆 存放的是對(duì)象的實(shí)例和數(shù)組。因此該區(qū)更關(guān)注的是數(shù)據(jù)的存儲(chǔ)。
- 虛擬機(jī)棧 存放的是局部變量,操作數(shù)棧,返回結(jié)果。該區(qū)更關(guān)注的是程序方法的執(zhí)行。
注:靜態(tài)變量放在方法區(qū),而靜態(tài)的對(duì)象還是放在堆。
④ 線(xiàn)程共享方面的區(qū)別:
- 堆 對(duì)于整個(gè)應(yīng)用程序都是共享、可見(jiàn)的。
- 虛擬機(jī)棧 只對(duì)于線(xiàn)程是可見(jiàn)的。所以也是線(xiàn)程私有。他的生命周期和線(xiàn)程相同。
總結(jié)
文章會(huì)不定時(shí)更新,有時(shí)候一天多更新幾篇,如果幫助您復(fù)習(xí)鞏固了知識(shí)點(diǎn),后續(xù)會(huì)億點(diǎn)點(diǎn)的更新!也希望大家關(guān)注腳本之家其他文章!
相關(guān)文章
Spring IOC創(chuàng)建對(duì)象的兩種方式
這篇文章主要給大家介紹了關(guān)于Spring IOC創(chuàng)建對(duì)象的兩種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03SpringBoot詳細(xì)講解視圖整合引擎thymeleaf
這篇文章主要分享了Spring Boot整合使用Thymeleaf,Thymeleaf是新一代的Java模板引擎,類(lèi)似于Velocity、FreeMarker等傳統(tǒng)引擎,關(guān)于其更多相關(guān)內(nèi)容,需要的小伙伴可以參考一下2022-06-06Java引用傳遞和值傳遞棧內(nèi)存與堆內(nèi)存的指向操作
這篇文章主要介紹了Java引用傳遞和值傳遞棧內(nèi)存與堆內(nèi)存的指向操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09Springboot配置security basic path無(wú)效解決方案
這篇文章主要介紹了Springboot配置security basic path無(wú)效解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09IDEA 自定義方法注解模板的實(shí)現(xiàn)方法
這篇文章主要介紹了IDEA 自定義方法注解模板的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09Java圖形界面之JFrame,JLabel,JButton詳解
這篇文章主要介紹了Java圖形界面之JFrame、JLabel、JButton詳解,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04