1、什么是 JVM?
JVM 就是 Java 虛擬機,它是 Java 實現跨平臺的基石。程序運行之前,需要先通過編譯器將 Java 源代碼文件編譯成 Java 字節碼文件;程序運行時,JVM 會對字節碼文件進行逐行解釋,翻譯成機器碼指令,交給對應的操作系統執行。這樣就實現了 Java 一次編譯,處處運行的特性。
1.1 說說 JVM 的其他特性?
- JVM 可以自動管理內存,通過垃圾回收器回收不再使用的對象并釋放內存空間。
- JVM 包含一個即時編譯器 JIT,它可以在運行時將熱點代碼緩存到 codeCache 中,下次執行的時候不用再一行一行的解釋,而是直接執行緩存后的機器碼,執行效率會大幅提高。
- 任何可以通過 Java 編譯的語言,比如說 Groovy、Kotlin、Scala 等,都可以在 JVM 上運行。
1.2 為什么要學習 JVM ?
學習 JVM 可以幫助我們開發者更好地優化程序性能、避免內存問題。比如:了解 JVM 的內存模型和垃圾回收機制,可以幫助我們更合理地配置內存、減少 GC 停頓。掌握 JVM 的類加載機制可以幫助我們排查類加載沖突或異常。JVM 還提供了很多調試和監控工具,可以幫助我們分析內存和線程的使用情況,從而解決內存溢出內存泄露等問題。
2、說說 JVM 的組織架構
JVM 大致可以劃分為三個部分:類加載器、運行時數據區和執行引擎。
- 類加載器:負責從文件系統、網絡或其他來源加載 Class 文件,將 Class 文件中的二進制數據讀入到內存當中。
- 運行時數據區:JVM 在執行 Java 程序時,需要在內存中分配空間來處理各種數據,這些內存區域按照 Java 虛擬機規范可以劃分為方法區、堆、虛擬機棧、本地方法棧和程序計數器。
- 執行引擎:JVM 的心臟,負責執行字節碼。它包括解釋器、JIT 編譯器和垃圾回收器。
3、能說一下 JVM 的內存區域嗎?
按照 Java 虛擬機規范,JVM 的內存區域可以細分為方法區、堆、虛擬機棧、本地方法棧和程序計數器。其中方法區和堆是線程共享的,虛擬機棧、本地方法棧和程序計數器是線程私有的。
3.1 介紹一下方法區?
方法區并不真實存在,是 Java 虛擬機規范中的一個邏輯概念,用于存儲已被 JVM 加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等。在 HotSpot 虛擬機中,方法區的實現稱為永久代 PermGen,但在 Java 8 及之后的版本中,已經被元空間 Metaspace 所替代。
3.2 介紹一下 Java 堆?
堆是 JVM 中最大的一塊內存區域,被所有線程共享,在 JVM 啟動時創建,主要用來存儲 new 出來的對象。Java 中幾乎所有的對象都會在堆中分配,堆也是垃圾收集器管理的目標區域。
從內存回收的角度來看,由于垃圾收集器大部分都是基于分代收集理論設計的,所以堆又被細分為新生代、老年代、Eden空間、From Survivor空間、To Survivor空間等。
從 JDK 7 開始,JVM 默認開啟了逃逸分析,意味著如果某些方法中的對象引用沒有被返回或者沒有在方法體外使用,也就是未逃逸出去,那么對象可以直接在棧上分配內存。
3.3 介紹一下 Java 虛擬機棧?
Java 虛擬機棧的生命周期與線程相同。當線程執行一個方法時,會創建一個對應的棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,然后棧幀會被壓入虛擬機棧中。當方法執行完畢后,棧幀會從虛擬機棧中移除。
3.4 介紹一下本地方法棧?
本地方法棧與虛擬機棧相似,區別在于虛擬機棧是為 JVM 執行 Java 編寫的方法服務的,而本地方法棧是為 Java 調用本地 native 方法服務的,通常由 C/C++ 編寫。
3.5 介紹一下本地方法棧的運行場景?
當 Java 應用需要與操作系統底層或硬件交互時,通常會用到本地方法棧。比如調用操作系統的特定功能,如內存管理、文件操作、系統時間、系統調用等。
3.6 native 方法解釋一下?
native 方法是在 Java 中通過 native 關鍵字聲明的,用于調用非 Java 語言,如 C/C++ 編寫的代碼。Java 可以通過 JNI,也就是 Java Native Interface 與底層系統、硬件設備、或者本地庫進行交互。
3.7 介紹一下程序計數器?
程序計數器也被稱為 PC 寄存器,是一塊較小的內存空間。它可以看作是當前線程所執行的字節碼行號指示器。
3.8 變量存在堆棧的什么位置?
對于局部變量,它存儲在當前方法棧幀中的局部變量表中。當方法執行完畢,棧幀被回收,局部變量也會被釋放。對于靜態變量來說,它存儲在 Java 虛擬機規范中的方法區中,在 Java 7 中是永久代,在 Java 8 及以后是元空間。
4、說一下 JDK 1.6、1.7、1.8 內存區域的變化?
- JDK 1.6 使用永久代來實現方法區。
- JDK 1.7 依然是永久帶,但是將字符串常量池、靜態變量存放到了堆上。
- JDK 1.8 直接在內存中劃出了一塊區域,叫元空間,來取代之前放在 JVM 內存中的永久代,并將運行時常量池、類常量池都移動到了元空間。
5、為什么使用元空間替代永久代?
因為永久代受到 JVM 內存大小的限制,會導致 Java 應用程序出現內存溢出的問題。
6、對象創建的過程了解嗎?
當我們使用 new 關鍵字創建一個對象時,JVM 首先會檢查 new 指令的參數是否能在常量池中定位到類的符號引用,然后檢查這個符號引用代表的類是否已被加載、解析和初始化。如果沒有,就先執行類加載。如果已經加載,JVM 會為對象分配內存完成初始化,比如數值類型的成員變量初始值是 0,布爾類型是 false,對象類型是 null。接下來,會設置對象頭,里面包含了對象是哪個類的實例、對象的哈希碼、對象的 GC 分代年齡等信息。最后,JVM 會執行構造方法 完成賦值操作,將成員變量賦值為預期的值,比如 int age = 18,這樣一個對象就創建完成了。
6.1 對象的銷毀過程了解嗎?
當對象不再被任何引用指向時,就會變成垃圾。垃圾收集器會通過可達性分析算法判斷對象是否存活,如果對象不可達,就會被回收。垃圾收集器通過標記清除、標記復制、標記整理等算法來回收內存,釋放對象占用的內存空間。
7、堆內存是如何分配的?
在堆中為對象分配內存時,主要使用兩種策略:指針碰撞和空閑列表。指針碰撞適用于管理簡單、碎片化較少的內存區域,如年輕代;而空閑列表適用于內存碎片化較嚴重或對象大小差異較大的場景如老年代。
- 指針碰撞:假設堆內存是一個連續的空間,被分為兩個部分,一部分是已經被使用的內存,另一部分是未被使用的內存。在分配內存時,Java 虛擬機會維護一個指針,指向下一個可用的內存地址,每次分配內存時,只需要將指針向后移動一段距離,如果沒有發生碰撞,就將這段內存分配給對象實例。
- 空閑列表:JVM 會維護一個列表,記錄堆中所有未占用的內存塊,每個內存塊都記錄有大小和地址信息。當有新的對象請求內存時,JVM 會遍歷空閑列表,尋找足夠大的空間來存放新對象。分配后,如果選中的內存塊未被完全利用,剩余的部分會作為一個新的內存塊加入到空閑列表中。
8、new 對象時,堆會發生搶占嗎?
會發生搶占。new 對象時,指針會向右移動一個對象大小的距離,假如一個線程 A 正在給 String 對象 分配內存,另一個線程 B 同時為 ArrayList 對象分配內存,兩個線程就發生了搶占。
8.1 JVM 怎么解決堆內存分配的競爭問題?
為了解決堆內存分配的競爭問題,JVM 為每個線程保留了一小塊內存空間,被稱為 TLAB,也就是線程本地分配緩沖區,用于存放該線程分配的對象。當線程需要分配對象時,直接從 TLAB 中分配。只有當 TLAB 用盡或對象太大需要直接在堆中分配時,才會使用全局分配指針。
9、能說一下對象的內存布局嗎?
對象的內存布局是由 Java 虛擬機規范定義的,拿我們常用的 HotSpot 來說吧。對象在內存中包括三部分:對象頭、實例數據和對齊填充。
9.1 說說對象頭的作用?
對象頭是對象存儲在內存中的元信息,包含了:Mark Word、類型指針等信息。Mark Word 存儲了對象的運行時狀態信息,包括鎖、哈希值、GC 標記等。在 64 位操作系統下占 8 個字節。類型指針指向對象所屬類的元數據,也就是 Class 對象,用來支持多態、方法調用等功能。在開啟壓縮指針的情況下占 4 個字節,否則占 8 個字節。除此之外,如果對象是數組類型,還會有一個額外的數組長度字段。占 4 個字節。
9.2 實例數據了解嗎?
實例數據是對象實際的字段值,也就是成員變量的值,按照字段在類中聲明的順序存儲。JVM 會對這些數據進行對齊 / 重排,以提高內存訪問速度。
9.3 對齊填充了解嗎?
由于 JVM 的內存模型要求對象的起始地址是 8 字節對齊(64 位 JVM 中),因此對象的總大小必須是 8 字節的倍數。如果對象頭和實例數據的總長度不是 8 的倍數,JVM 會通過填充額外的字節來對齊。比如說,如果對象頭 + 實例數據 = 14 字節,則需要填充 2 個字節,使總長度變為 16 字節。
9.4 為什么非要進行 8 字節對齊呢?
因為 CPU 進行內存訪問時,一次尋址的指針大小是 8 字節,正好是 L1 緩存行的大小。如果不進行內存對齊,則可能出現跨緩存行訪問,導致額外的緩存行加載,CPU 的訪問效率就會降低。8 字節對齊,是一種以空間換時間的方案。
9.5 new Object() 對象的內存大小是多少?
一般來說,目前的操作系統都是 64 位的,并且 JDK 8 中的壓縮指針是默認開啟的,因此在 64 位的 JVM 上,new Object()的大小是 16 字節(12 字節的對象頭 + 4 字節的對齊填充)。
假如 MyObject 對象有三個成員變量,分別是 int、long 和 byte 類型,那么它們占用的內存大小分別是 4 字節、8 字節和 1 字節。考慮到對齊填充,MyObject 對象的總大小為 12 + 4 + 8 + 1 + 7(填充)= 32 字節。
9.6 對象的引用(類型指針)大小了解嗎?
在 64 位 JVM 上,未開啟壓縮指針時,對象引用占用 8 字節;開啟壓縮指針時,對象引用會被壓縮到 4 字節。HotSpot 虛擬機默認是開啟壓縮指針的。
10、JVM 怎么訪問對象的?
主流的方式有兩種:句柄和直接指針。句柄是通過中間的句柄表來定位對象的,優點是對象被移動時只需要修改句柄表中的指針,而不需要修改對象引用本身。直接指針是通過引用直接存儲對象的內存地址的,因為對象的實例數據和類型信息都存儲在堆中固定的內存區域。直接指針訪問的優點是訪問速度更快,因為少了一次句柄的尋址操作。缺點是如果對象在內存中移動,引用需要更新為新的地址。HotSpot 虛擬機主要使用直接指針來進行對象訪問。