很多人說JVM是不是真的要學?面試官都會問JVM的問題嗎?很重要嗎?
的確很重要。
隨着互聯網的發展,高併發高可用、快速響應成爲軟件的必須,而JVM與這些有着密切關聯。
我們在盡情享受Java虛擬機帶來好處的同時,還應該去了解和思考“這些技術特性是如何實現的”,去了解最底層的原理。只有熟悉JVM,你才能在遇到OutOfMemory等異常時,不會束手無策,不會一臉懵逼的上網找解決辦法,最後就算改了幾個啓動參數解決了問題,也還是雲裏霧裏。
其實,“爲什麼學Java虛擬機”這個問題,就和“爲什麼要學習數據結構和算法”是一個道理:工欲善其事,必先利其器。可以說,Java虛擬機就是每一位Java工程師進階加薪的利器,你想往上升,你想深入技術,不想一直停留在簡單開發,或者你在做Java性能分析、調優工作時,那麼,Java虛擬機絕對是一把助力的利劍。
1.內存模型以及分區,需要詳細到每個區放什麼。
JVM 分爲堆區和棧區,還有方法區,初始化的對象放在堆裏面,引用放在棧裏面,class類信息常量池(static常量和static變量)等放在方法區
new:
方法區:主要是存儲類信息,常量池(static常量和static變量),編譯後的代碼(字節碼)等數據
堆:初始化的對象,成員變量 (那種非static的變量),所有的對象實例和數組都要在堆上分配
棧:棧的結構是棧幀組成的,調用一個方法就壓入一幀,幀上面存儲局部變量表,操作數棧,方法出口等信息,局部變量表存放的是8大基礎類型加上一個應用類型,所以還是一個指向地址的指針
本地方法棧:主要爲Native方法服務
程序計數器:記錄當前線程執行的行號
2.堆裏面的分區:Eden,survival (from+ to),老年代,各自的特點。
堆裏面分爲新生代和老生代(java8取消了永久代,採用了Metaspace),新生代包含Eden+Survivor區,survivor區裏面分爲from和to區,內存回收時,如果用的是複製算法,從from複製到to,當經過一次或者多次GC之後,存活下來的對象會被移動到老年區,當JVM內存不夠用的時候,會觸發Full GC,清理JVM老年區
當新生區滿了之後會觸發YGC,先把存活的對象放到其中一個Survice 區,然後進行垃圾清理。因爲如果僅僅清理需要刪除的對象,這樣會導致內存碎 片,因此一般會把Eden 進行完全的清理,然後整理內存。那麼下次GC 的時候, 就會使用下一個Survive,這樣循環使用。如果有特別大的對象,新生代放不下, 就會使用老年代的擔保,直接放到老年代裏面。因爲JVM 認爲,一般大對象的存 活時間一般比較久遠。
3.對象創建方法,對象的內存分配,對象的訪問定位。
new 一個對象
4.GC的兩種判定方法:
引用計數法:指的是如果某個地方引用了這個對象就+1,如果失效了就-1,當爲0就會回收但是JVM沒有用這種方式,因爲無法判定相互循環引用(A引用B,B引用A)的情況
引用鏈法: 通過一種GC ROOT的對象(方法區中靜態變量引用的對象等-static變量)來判斷,如果有一條鏈能夠到達GC ROOT就說明,不能到達GC ROOT就說明可以回收
5.SafePoint是什麼
比如GC的時候必須要等到Java線程都進入到safepoint的時候VMThread才能開始執行GC,
循環的末尾 (防止大循環的時候一直不進入safepoint,而其他線程在等待它進入safepoint)
方法返回前
調用方法的call之後
拋出異常的位置
6.GC的三種收集方法:標記清除、標記整理、複製算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?
先標記,標記完畢之後再清除,效率不高,會產生碎片
複製算法:分爲8:1的Eden區和survivor區,就是上面談到的YGC
標記整理:標記完畢之後,讓所有存活的對象向一端移動
7.GC收集器有哪些?CMS收集器與G1收集器的特點。
並行收集器:串行收集器使用一個單獨的線程進行收集,GC時服務有停頓時間
串行收集器:次要回收中使用多線程來執行
CMS收集器是基於“標記—清除”算法實現的,經過多次標記纔會被清除
G1從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的
8.Minor GC與Full GC分別在什麼時候發生?
新生代內存不夠用時候發生MGC也叫YGC,JVM內存不夠的時候發生FGC
9.幾種常用的內存調試工具:jmap、jstack、jconsole、jhat
jstack 可以看當前棧的情況,jmap 查看內存,jhat 進行 dump 堆的信息
mat(eclipse 的也要了解一下)
10.類加載的幾個過程
加載、驗證、準備、解析、初始化。然後是使用和卸載了
通過全限定名來加載生成 class 對象到內存中,然後進行驗證這個 class 文件,包括文
件格式校驗、元數據驗證,字節碼校驗等。準備是對這個對象分配內存。解析是將符
號引用轉化爲直接引用(指針引用),初始化就是開始執行構造器的代碼
11.JVM內存分哪幾個區,每個區的作用是什麼?
java 虛擬機主要分爲以下一個區:
方法區:
- 有時候也成爲永久代,在該區內很少發生垃圾回收,但是並不代表不發生 GC,在這裏
進行的 GC 主要是對方法區裏的常量池和對類型的卸載
- 方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯後
的代碼等數據。
-
該區域是被線程共享的。
- 方法區裏有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池
具有動態性,也就是說常量並不一定是編譯時確定,運行時生成的常量也會存在這個常量
池中。
虛擬機棧:
- 虛擬機棧也就是我們平常所稱的棧內存,它爲 java 方法服務,每個方法在執行的時候都
會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接和方法出口等信息。
-
虛擬機棧是線程私有的,它的生命週期與線程相同。
- 局部變量表裏存儲的是基本數據類型、returnAddress 類型(指向一條字節碼指令的地
址)和對象引用,這個對象引用有可能是指向對象起始地址的一個指針,也有可能是代表
對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間確定
4.操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同於局部變量表通過索
引來訪問,而是壓棧和出棧的方式
5.每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了
支持方法調用過程中的動態連接.動態鏈接就是將常量池中的符號引用在運行期轉化爲直接
引用。
本地方法棧
本地方法棧和虛擬機棧類似,只不過本地方法棧爲 Native 方法服務。
堆
java 堆是所有線程所共享的一塊內存,在虛擬機啓動時創建,幾乎所有的對象實例都在這
裏創建,因此該區域經常發生垃圾回收操作。
程序計數器
內存空間小,字節碼解釋器工作時通過改變這個計數值可以選取下一條需要執行的字節碼
指令,分支、循環、跳轉、異常處理和線程恢復等功能都需要依賴這個計數器完成。該內
存區域是唯一一個 java 虛擬機規範沒有規定任何 OOM 情況的區域。
12.如和判斷一個對象是否存活?(或者GC對象的判定方法)
判斷一個對象是否存活有兩種方法:
- 引用計數法
所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象
時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說
明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.
引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象 A 引用對象 B,對象
B 又引用者對象 A,那麼此時 A,B 對象的引用計數器都不爲零,也就造成無法完成垃圾回
收,所以主流的虛擬機都沒有采用這種算法。
2.可達性算法(引用鏈法)
該算法的思想是:從一個被稱爲 GC Roots 的對象開始向下搜索,如果一個對象到 GC
Roots 沒有任何引用鏈相連時,則說明此對象不可用。
在 java 中可以作爲 GC Roots 的對象有以下幾種:
虛擬機棧中引用的對象
方法區類靜態屬性引用的對象
方法區常量池引用的對象
本地方法棧 JNI 引用的對象
雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一
定會被回收。當一個對象不可達 GC Root 時,這個對象並
不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記
如果對象在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就會被第一次標記並且進行
一次篩選,篩選的條件是是否有必要執行 finalize()方法。當對象沒有覆蓋 finalize()方法
或者已被虛擬機調用過,那麼就認爲是沒必要的。
如果該對象有必要執行 finalize()方法,那麼這個對象將會放在一個稱爲 F-Queue 的對隊
列中,虛擬機會觸發一個 Finalize()線程去執行,此線程是低優先級的,並且虛擬機不會承
諾一直等待它運行完,這是因爲如果 finalize()執行緩慢或者發生了死鎖,那麼就會造成 F-
Queue 隊列一直等待,造成了內存回收系統的崩潰。GC 對處於 F-Queue 中的對象進行
第二次被標記,這時,該對象將被移除”即將回收”集合,等待回收。
13.簡述java垃圾回收機制?
在 java 中,程序員是不需要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在
JVM 中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,只有在虛
擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將
它們添加到要回收的集合中,進行回收。
14.java中垃圾收集的方法有哪些?
- 標記-清除:
這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被
回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不
高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以後程序在
分配較大的對象時,由於沒有充足的連續內存而提前觸發一次 GC 動作。
- 複製算法:
爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,然後每次只
使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,然
後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。但是這種方式,
內存的代價太高,每次基本上都要浪費一般的內存。
於是將該算法進行了改進,內存區域不再是按照 1:1 去劃分,而是將內存劃分爲
8:1:1 三部分,較大那份內存交 Eden 區,其餘是兩塊較小的內存區叫 Survior 區。
每次都會優先使用 Eden 區,若 Eden 區滿,就將對象複製到第二塊內存區上,然
後清除 Eden 區,如果此時存活的對象太多,以至於 Survivor 不夠時,會將這些對
象通過分配擔保機制複製到老年代中。(java 堆又分爲新生代和老年代)
- 標記-整理
該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高
時,也解決了複製算法的效率問題。它的不同之處就是在清除對象的時候現將可回
收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
- 分代收集
現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生
代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那
麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔
保,所以可以使用標記-整理 或者 標記-清除。
15.java內存模型
java 內存模型(JMM)是線程間通信的控制機制.JMM 定義了主內存和線程之間抽象關係。
線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地
內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是
JMM 的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬
件和編譯器優化。Java 內存模型的抽象示意圖如下:
從上圖來看,線程 A 與線程 B 之間如要通信的話,必須要經歷下面 2 個步驟:
-
首先,線程 A 把本地內存 A 中更新過的共享變量刷新到主內存中去。
- 然後,線程 B 到主內存中去讀取線程 A 之前已更新過的共享變量。
16.java類加載過程?
java 類加載需要經歷一下 7 個過程:
加載
加載時類加載的第一個過程,在這個階段,將完成一下三件事情:
-
通過一個類的全限定名獲取該類的二進制流。
-
將該二進制流中的靜態存儲結構轉化爲方法去運行時數據結構。
- 在內存中生成該類的 Class 對象,作爲該類的數據訪問入口。
驗證
驗證的目的是爲了確保 Class 文件的字節流中的信息不回危害到虛擬機.在該階段主要完成
以下四鍾驗證:
- 文件格式驗證:驗證字節流是否符合 Class 文件的規範,如主次版本號是否在當前虛擬
機範圍內,常量池中的常量是否有不被支持的類型.
- 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不
被繼承的類等。
- 字節碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,
確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉
指令是否正確等。
- 符號引用驗證:這個動作在後面的解析過程中發生,主要是爲了確保解析動作能正確執
行。
準備
準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進
行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象
一起分配在 Java 堆中。
public static int value=123; //在準備階段 value 初始值爲 0 。在初始化階段纔會變
爲 123 。
解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之
前,也有可能在初始化之後。
初始化
初始化時類加載的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過
自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正
開始執行類中定義的 Java 程序代碼。
- 簡述java類加載機制?
虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗,解析和初始化,最
終形成可以被虛擬機直接使用的 java 類型。
- 類加載器雙親委派模型機制?
當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類
去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。
19.什麼是類加載器,類加載器有哪些?
實現通過類的權限定名獲取該類的二進制字節流的代碼塊叫做類加載器。
主要有一下四種類加載器:
- 啓動類加載器(Bootstrap ClassLoader)用來加載 java 核心類庫,無法被 java 程序直接
引用。
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的
實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)
來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過
ClassLoader.getSystemClassLoader()來獲取它。
- 用戶自定義類加載器,通過繼承 java.lang.ClassLoader 類的方式實現。
20.簡述java內存分配與回收策率以及Minor GC和Major GC
-
對象優先在堆的 Eden 區分配。
-
大對象直接進入老年代.
- 長期存活的對象將直接進入老年代.
當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次 Minor GC.Minor Gc 通
常發生在新生代的 Eden 區,在這個區的對象生存期短,往往發生 Gc 的頻率較高,
回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代 GC
的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之前進行一次 Minor
GC 這樣可以加快老年代的回收速度。