(Java實習生)每日10道面試題打卡——JVM篇
臨近秋招,備戰暑期實習,祝大家每天進步億點點!==打卡 Day06==!
有粉絲大佬要求更新有難度的,所以本篇總結的是 JVM 相關的面試題,后續會每日更新~
注:JVM 比較枯燥,直接刷題前,最好先去串一遍 JVM 課程,這里推薦傳智播客的 JVM 教程:黑馬程序員JVM教程筆記完整目錄

1、請你簡述一下 Java 內存結構(運行時數據區)
如圖所示:
① 程序計數器
程序計數器:==線程私有==。一塊較小的內存空間,程序計數器用于保存 JVM 中下一條所要執行的字節碼指令的地址!如果正在執行的是 Native 方法,則這個計數器值則為空。程序計數器在硬件層面是通過 寄存器 實現的!
Java指令執行流程:
.java代碼源文件經過編譯為.class 二進制字節碼文件。
.class 文件中的每一條二進制字節碼指令(JVM指令) 通過 解釋器 轉換成 機器碼 然后就可以被 CPU 執行了!
當 解釋器 將一條 jvm 指令轉換成 機器碼 后,同時會向程序計數器 遞交下一條 jvm 指令的執行地址!
如圖所示:
② 虛擬機棧
虛擬機棧:==線程私有==,它的生命周期與線程相同。虛擬機棧是Java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
每個棧由多個棧幀(Frame) 組成,對應著每個方法運行時所占用的內存。
每個線程只能有一個活動棧幀,對應著當前正在執行的方法,當方法執行時壓入棧,方法執行完畢后彈出棧。
方法體中的引用變量和基本類型的變量都在棧上,其他都在堆上。
實例代碼:
/** * @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; } }
流程分析:
我們打斷點來Debug 一下看一下方法執行的流程:
接這往下走,使方法B執行完畢:
然后方法A 執行完畢,其對應的棧幀出棧,main 方法對應的棧幀為活動棧幀;最后main執行完畢,棧幀出棧,虛擬機棧為空,代碼運行結束!
③ 本地方法棧
本地方法棧:==線程私有==。本地方法棧與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。
一些帶有native 關鍵字修飾的方法就是需要JAVA去調用本地的C或者C++方法,因為JAVA有時候沒法直接和操作系統底層交互,所以需要用到本地方法!
④ 堆
堆:==線程共享==。Java堆是Java虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。Java堆的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。
通過new關鍵字創建的對象都會被放在堆內存。
方法體中的引用變量和基本類型的變量都在棧上,其他都在堆上。
Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”(Garbage)。
-Xmx -Xms:JVM初始分配的堆內存由-Xms指定,默認是物理內存的1/64。
⑤ 方法區
方法區:==線程共享==。方法區用于存儲已被虛擬機加載的 *類信息(構造方法、接口定義)、常量、靜態變量、即時編譯器編譯后的代碼(字節碼)*等數據。
方法區在 JVM 啟動的時候被創建,并且它的實際的物理內存空間和 Java堆一樣都可以是不連續的, 關閉 Jvm 就會釋放這個區域的內存。
方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出錯誤:(java.lang.OutOfMemoryError:PermGen space、java.lang.OutOfMemoryError:Metaspace)。
注意:方法區時一種規范,而==永久代和元空間==是它的2種實現方式。
方法區的演進:
1.6 版本方法區是由 永久代 實現(使用堆內存的一部分作為方法區),且由JVM 管理。由Class、ClassLoader、常量池(包括StringTable) 組成。
Jdk 1.7 版本仍有永久代,但已經逐步 " 去永久代 ",StringTable、靜態變量從永久代移除,保存在堆中。
1.8 版本后,方法區交給本地內存管理,而脫離了JVM,由元空間實現(元空間不再使用堆的內存,而是使用本地內存,即操作系統的內存),由Class、ClassLoader、常量池(StringTable 被移到了堆中管理) 組成。
⑥ 運行時常量池
常量池:可以看做是一張表,虛擬機指令根據這張常量表找到要執行的 類名,方法名,參數類型、字面量 等信息。
常量池是*.class文件中的,當該類被加載以后,它的常量池信息就會放入運行時常量池,并把里面的符號地址變為真實內存地址。
運行時常量池:是方法區的一部分。
String str = new String("hello");
上面的語句中變量 str 放在棧上,用 new 創建出來的字符串對象放在堆上,而hello這個字面量是放在堆中。
2、請問jvm垃圾回收是否涉及棧內存?
不需要。因為虛擬機棧中是由一個個棧幀組成的,在方法執行完畢后,對應的棧幀就會被彈出棧。所以無需通過垃圾回收機制去回收內存。
3、虛擬機棧內存的分配越大越好嗎?
不是。因為物理內存是一定的,棧內存越大,可以支持更多的遞歸調用,但是可執行的線程數就會越少。
我們來看一張圖:
舉例:如果物理內存是500M(假設),如果一個線程所能分配的棧內存為2M的話,那么可以有250個線程。而如果一個線程分配棧內存占5M的話,那么最多只能有100 個線程同時執行!
4、從JVM的角度分析,方法內的局部變量是否是線程安全的?
我們通過兩張圖去分析一下:
情況一:
情況二:
從圖中得出:局部變量如果是靜態的可以被多個線程共享,那么就存在線程安全問題。如果是非靜態的只存在于某個方法作用范圍內,被線程私有,那么就是線程安全的!
再來看一個案例:
/** * 局部變量的線程安全問題 */ public class Demo02 { public static void main(String[] args) {// main 函數主線程 StringBuilder sb = new StringBuilder(); sb.append(4); sb.append(5); sb.append(6); new Thread(() -> {// Thread新創建的線程 m2(sb); }).start(); } public static void m1() { // sb 作為方法m1()內部的局部變量,是線程私有的 ---> 線程安全 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()外部的傳遞來的參數,sb 不在方法m2()的作用范圍內 // 不是線程私有的 ---> 非線程安全 sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { // sb 作為方法m3()內部的局部變量,是線程私有的 StringBuilder sb = new StringBuilder();// sb 為引用類型的變量 sb.append(1); sb.append(2); sb.append(3); return sb;// 然而方法m3()將sb返回,sb逃離了方法m3()的作用范圍,且sb是引用類型的變量 // 其他線程也可以拿到該變量的 ---> 非線程安全 // 如果sb是非引用類型,即基本類型(int/char/float...)變量的話,逃離m3()作用范圍后,則不會存在線程安全 } }
所以,該面試題答案是:
如果方法內局部變量沒有逃離方法的作用范圍,則是線程安全的。
如果局部變量引用了對象,并逃離了方法的作用范圍,則需要考慮線程安全問題。
5、虛擬機棧內存溢出的情況有哪些?
1.虛擬機棧中,棧幀過多(方法無限遞歸)導致棧內存溢出,這種情況比較常見!
2.每個棧幀所占用內存過大(某個/某幾個棧幀內存直接超過虛擬機棧最大內存),這種情況比較少見!
如圖所示,就是棧中棧幀過多的情況:
6、請你說一下JVM運行時數據區方法區的演進?
1.6 版本方法區是由永久代實現(使用堆內存的一部分作為方法區),且由JVM 管理。由Class、ClassLoader、常量池(包括StringTable) 組成。
靜態變量就存放在永久代(方法區)上。
Jdk 1.7 版本仍有永久代,但已經逐步 " 去永久代 ",StringTable、靜態變量從永久代移除,保存在堆中。
1.8 版本后,方法區交給本地內存管理,而脫離了JVM,由元空間實現(元空間不再使用堆的內存,而是使用本地內存,即操作系統的內存),由Class、ClassLoader、常量池(StringTable 被移到了堆中管理) 組成。
靜態變量、StringTable 存放在堆中!
為什么要用元空間取代永久代?
因為永久代有以下幾個弊端:
① 字符串常量池存在于永久代中,在大量使用字符串的情況下,非常容易出現OOM的異常。
② JVM加載的class的總數,方法的大小等都很難確定,因此對永久代大小的指定難以確定。太小的永久代容易導致永久代內存溢出,太大的永久代則容易導致虛擬機內存緊張,空間浪費。
③ 永久代進行調優很困難:方法區的垃圾收集主要回收兩部分,常量池中廢棄的常量和不再使用的類。而不再使用的類或類的加載器回收比較復雜,FULL GC 的時間長。
7、請問Java虛擬機中有哪些類加載器?
以 JDK 8 為例:
類加載器的優先級(由高到低):啟動類加載器 -> 擴展類加載器 -> 應用程序類加載器 -> 自定義類加載器。
**啟動類加載器(Bootstrap ClassLoader):**這個類加載器負責將存放在 JAVA_HOME/jre/lib 目錄中的,或者被-Xbootclasspath 參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
**擴展類加載器(Extension ClassLoader):**這個加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載JAVA_HOME/jre/lib/ext目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
**應用程序類加載器(Application ClassLoader):**這個類加載器由 sun.misc.Launcher$AppClassLoader 實現。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
**自定義類加載器:**用戶自定義的類加載器。
8、請你說一下類的加載的過程?
類加載的過程包括:加載、驗證、準備、解析、初始化。其中驗證、準備、解析統稱為連接。
加載:通過一個類的全限定名來獲取定義此類的二進制字節流,在內存中生成一個代表這個類的java.lang.Class對象。
驗證:確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
準備:為靜態變量分配內存并設置靜態變量初始值,這里所說的初始值“通常情況”下是數據類型的零值。
解析:將常量池內的符號引用替換為直接引用。
初始化:到了初始化階段,才真正開始執行類中定義的 Java 初始化程序代碼。主要是靜態變量賦值動作和靜態語句塊(static{})中的語句。
9、請你說一下什么是雙親委派模型?
如圖所示:
什么是雙親委派模型?
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
為什么要使用雙親委派模型呢?(好處)
避免重復加載 + 避免核心類篡改
采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重復加載,當父加載器已經加載了該類時,就沒有必要子加載器再加載一次。
其次是考慮到安全因素,java 核心 api 中定義類型不會被隨意替換,假設通過網絡傳遞一個名為 java.lang.Integer 的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,并不會重新加載網絡傳遞的過來的 java.lang.Integer,而直接返回已加載過的 Integer.class,這樣便可以防止核心API庫被隨意篡改。
10、說一下虛擬機棧和堆的區別?
① 物理地址方面的區別:
堆 的物理地址分配對對象是不連續的。因此性能慢些。
虛擬機棧 使用的是數據結構中的棧,先進后出的原則,物理地址分配是連續的。所以性能快。
② 內存分配方面的區別:
堆 因為是不連續的,所以分配的內存是在運行期確認的,因此大小不固定。一般堆大小遠遠大于虛擬機棧。
虛擬機棧 是連續的,所以分配的內存大小要在編譯期就確認,大小是固定的。
③ 存放的內容方面的區別:
堆 存放的是對象的實例和數組。因此該區更關注的是數據的存儲。
虛擬機棧 存放的是局部變量,操作數棧,返回結果。該區更關注的是程序方法的執行。
注:靜態變量放在方法區,而靜態的對象還是放在堆。
④ 線程共享方面的區別:
堆 對于整個應用程序都是共享、可見的。
虛擬機棧 只對于線程是可見的。所以也是線程私有。他的生命周期和線程相同。
參考文章:
JVM_01 內存結構(程序計數器、虛擬機棧、本地方法棧)
JVM_02-03 內存結構(堆、方法區)
JVM_11 類加載與字節碼技術 (類加載與類的加載器)
總結的面試題也挺費時間的,文章會不定時更新,有時候一天多更新幾篇,如果幫助您復習鞏固了知識點,還請三連支持一下,后續會億點點的更新!
Java JVM 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。