1. 程式人生 > 其它 >JAVA面試題--Java虛擬機器

JAVA面試題--Java虛擬機器

Java虛擬機器

1.說一下JVM的記憶體結構?

詳見:https://blog.csdn.net/rongtaoup/article/details/89142396

2.棧幀裡面包含哪些東西?

區域性變量表、運算元棧、動態連線、返回地址等

3.程式計數器有什麼作用?

程式計數器是一塊較小的記憶體空間,它的作用可以看作是當前執行緒所執行的位元組碼的行號指示器。這裡面存的,就是當前執行緒執行的進度。
程式計數器還儲存了當前正在執行的流程,包括正在執行的指令、跳轉、分支、迴圈、異常處理等。

4.字串常量存放在哪個區域?

  1. 字串常量池,已經移動到堆上(jdk8之前是perm區),也就是執行intern方法後存的地方。
  2. 類檔案常量池,constant_pool,是每個類每個介面所擁有的,這部分資料在方法區,也就是元資料區。而執行時常量池是在類載入後的一個記憶體區域,它們都在元空間。

5.你熟悉哪些垃圾收集演算法?

標記清除(缺點是碎片化)
複製演算法(缺點是浪費空間)
標記整理演算法(效率比前兩者差)
分代收集演算法(老年代一般使用“標記-清除”、“標記-整理”演算法,年輕代一般用複製演算法)

6.Java裡有哪些引用型別?

強引用
這種引用屬於最普通最強硬的一種存在,只有在和 GC Roots 斷絕關係時,才會被消滅掉。

軟引用
軟引用用於維護一些可有可無的物件。在記憶體足夠的時候,軟引用物件不會被回收,只有在記憶體不足時,系統則會回收軟引用物件,如果回收了軟引用物件之後仍然沒有足夠的記憶體,才會丟擲記憶體溢位異常。
可以看到,這種特性非常適合用在快取技術上。比如網頁快取、圖片快取等。
軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收,Java 虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

弱引用
弱引用物件相比較軟引用,要更加無用一些,它擁有更短的生命週期。當JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件。弱引用擁有更短的生命週期,在 Java 中,用 java.lang.ref.WeakReference 類來表示。它的應用場景和軟引用類似,可以在一些對記憶體更加敏感的系統裡採用。

虛引用
這是一種形同虛設的引用,在現實場景中用的不是很多。虛引用必須和引用佇列(ReferenceQueue)聯合使用。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。實際上,虛引用的 get,總是返回 null。

7.JVM怎麼判斷一個物件是不是要回收?

引用計數法(缺點是對於相互引用的物件,無法進行清除)
可達性分析

8.GC Roots 有哪些?

GC Roots 是一組必須活躍的引用。用通俗的話來說,就是程式接下來通過直接引用或者間接引用,能夠訪問到的潛在被使用的物件。

GC Roots 包括:
Java 執行緒中,當前所有正在被呼叫的方法的引用型別引數、區域性變數、臨時值等。也就是與我們棧幀相關的各種引用。
所有當前被載入的 Java 類。
Java 類的引用型別靜態變數。
執行時常量池裡的引用型別常量(String 或 Class 型別)。
JVM 內部資料結構的一些引用,比如 sun.jvm.hotspot.memory.Universe 類。
用於同步的監控物件,比如呼叫了物件的 wait() 方法。
JNI handles,包括 global handles 和 local handles。

這些 GC Roots 大體可以分為三大類,下面這種說法更加好記一些:
活動執行緒相關的各種引用。
類的靜態變數的引用。
JNI 引用。

有兩個注意點:
我們這裡說的是活躍的引用,而不是物件,物件是不能作為 GC Roots 的。
GC 過程是找出所有活物件,並把其餘空間認定為“無用”;而不是找出所有死掉的物件,並回收它們佔用的空間。所以,哪怕 JVM 的堆非常的大,基於 tracing 的 GC 方式,回收速度也會非常快。

9.你知道哪些GC型別?

Minor GC:發生在年輕代的 GC。
Major GC:發生在老年代的 GC。
Full GC:全堆垃圾回收。比如 Metaspace 區引起年輕代和老年代的回收。

10.物件都是優先分配在年輕代上的嗎?

不是。當新生代記憶體不夠時,老年代分配擔保。而大物件則是直接在老年代分配。

11.你瞭解過哪些垃圾收集器?

年輕代
Serial 垃圾收集器(單執行緒,通常用在客戶端應用上。因為客戶端應用不會頻繁建立很多物件,使用者也不會感覺出明顯的卡頓。相反,它使用的資源更少,也更輕量級。)
ParNew 垃圾收集器(多執行緒,追求降低使用者停頓時間,適合互動式應用。)
Parallel Scavenge 垃圾收集器(追求 CPU 吞吐量,能夠在較短時間內完成指定任務,適合沒有互動的後臺計算。)

老年代
Serial Old 垃圾收集器
Parallel Old垃圾收集器
CMS 垃圾收集器(以獲取最短 GC 停頓時間為目標的收集器,它在垃圾收集時使得使用者執行緒和 GC 執行緒能夠併發執行,因此在垃圾收集過程中使用者也不會感到明顯的卡頓。)

12.說說CMS垃圾收集器的工作原理

Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間, 和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法。
最短的垃圾收集停頓時間可以為互動比較高的程式提高使用者體驗。
CMS 工作機制相比其他的垃圾收集器來說更復雜,整個過程分為以下 4 個階段:
1)初始標記
只是標記一下 GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒。
2)併發標記
進行 GC Roots 跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒。
3)重新標記
為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,仍然需要暫停所有的工作執行緒。
4)併發清除
清除 GC Roots 不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。由於耗時最長的併發標記和併發清除過程中,垃圾收集執行緒可以和使用者執行緒一起併發工作, 所以總體上來看CMS 收集器的記憶體回收和使用者執行緒是一起併發地執行。

13.說說G1垃圾收集器的工作原理

優點:指定最大停頓時間、分Region的記憶體佈局、按收益動態確定回收集

G1開創的基於Region的堆記憶體佈局是它能夠實現這個目標的關鍵。雖然G1也仍是遵循分代收集理論設計的,但其堆記憶體的佈局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新建立的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果。

雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區域(不需要連續)的動態集合。G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的記憶體空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region裡面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先順序列表,每次根據使用者設定允許的收集停頓時間(使用引數-XX:MaxGCPauseMillis指定,預設值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。這種使用Region劃分記憶體空間,以及具有優先順序的區域回收方式,保證了G1收集器在有限的時間內獲取儘可能高的收集效率。

G1收集器的運作過程大致可劃分為以下四個步驟:
·初始標記 (Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS指標的值,讓下一階段使用者執行緒併發執行時,能正確地在可用的Region中分配新物件。這個階段需要停頓執行緒,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際並沒有額外的停頓。
·併發標記 (Concurrent Marking):從GC Root開始對堆中物件進行可達性分析,遞迴掃描整個堆裡的物件圖,找出要回收的物件,這階段耗時較長,但可與使用者程式併發執行。當物件圖掃描完成以後,還要重新處理SATB記錄下的在併發時有引用變動的物件。
·最終標記 (Final Marking):對使用者執行緒做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。
·篩選回收 (Live Data Counting and Evacuation):負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據使用者所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活物件複製到空的Region中,再清理掉整個舊Region的全部空間。這裡的操作涉及存活物件的移動,是必須暫停使用者執行緒,由多條收集器執行緒並行完成的。
從上述階段的描述可以看出,G1收集器除了併發標記外,其餘階段也是要完全暫停使用者執行緒的 。

14.說說ZGC垃圾收集器的工作原理

1)記憶體佈局
·小型Region(Small Region):容量固定為2MB,用於放置小於256KB的小物件。
·中型Region(Medium Region):容量固定為32MB,用於放置大於等於256KB但小於4MB的物件。
·大型Region(Large Region):容量不固定,可以動態變化,但必須為2MB的整數倍,用於放置4MB或以上的大物件。每個大型Region中只會存放一個大物件,這也預示著雖然名字叫作“大型Region”,但它的實際容量完全有可能小於中型Region,最小容量可低至4MB。大型Region在ZGC的實現中是不會被重分配(重分配是ZGC的一種處理動作,用於複製物件的收集器階段,稍後會介紹到)的,因為複製一個大物件的代價非常高昂。

2)染色指標
染色指標是一種直接將少量額外的資訊儲存在指標上的技術,可是為什麼指標本身也可以儲存額外資訊呢?在64位系統中,理論可以訪問的記憶體高達16EB(2的64次冪)位元組 [3] 。實際上,基於需求(用不到那麼多記憶體)、效能(地址越寬在做地址轉換時需要的頁表級數越多)和成本(消耗更多電晶體)的考慮,在AMD64架構 [4] 中只支援到52位(4PB)的地址匯流排和48位(256TB)的虛擬地址空間,所以目前64位的硬體實際能夠支援的最大記憶體只有256TB。此外,作業系統一側也還會施加自己的約束,64位的Linux則分別支援47位(128TB)的程序虛擬地址空間和46位(64TB)的實體地址空間,64位的Windows系統甚至只支援44位(16TB)的實體地址空間。
儘管Linux下64位指標的高18位不能用來定址,但剩餘的46位指標所能支援的64TB記憶體在今天仍然能夠充分滿足大型伺服器的需要。鑑於此,ZGC的染色指標技術繼續盯上了這剩下的46位指標寬度,將其高4位提取出來儲存四個標誌資訊。通過這些標誌位,虛擬機器可以直接從指標中看到其引用物件的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到。當然,由於這些標誌位進一步壓縮了原本就只有46位的地址空間,也直接導致ZGC能夠管理的記憶體不可以超過4TB(2的42次冪) 。

3)收集過程
·併發標記 (Concurrent Mark):與G1、Shenandoah一樣,併發標記是遍歷物件圖做可達性分析的階段,前後也要經過類似於G1、Shenandoah的初始標記、最終標記(儘管ZGC中的名字不叫這些)的短暫停頓,而且這些停頓階段所做的事情在目標上也是相類似的。與G1、Shenandoah不同的是,ZGC的標記是在指標上而不是在物件上進行的,標記階段會更新染色指標中的Marked 0、Marked 1標誌位。
·併發預備重分配 (Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。重分配集與G1收集器的回收集(Collection Set)還是有區別的,ZGC劃分Region的目的並非為了像G1那樣做收益優先的增量回收。相反,ZGC每次回收都會掃描所有的Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。因此,ZGC的重分配集只是決定了裡面的存活物件會被重新複製到其他的Region中,裡面的Region會被釋放,而並不能說回收行為就只是針對這個集合裡面的Region進行,因為標記過程是針對全堆的。此外,在JDK 12的ZGC中開始支援的類解除安裝以及弱引用的處理,也是在這個階段中完成的。
·併發重分配 (Concurrent Relocate):重分配是ZGC執行過程中的核心階段,這個過程要把重分配集中的存活物件複製到新的Region上,併為重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊物件到新物件的轉向關係。得益於染色指標的支援,ZGC收集器能僅從引用上就明確得知一個物件是否處於重分配集之中,如果使用者執行緒此時併發訪問了位於重分配集中的物件,這次訪問將會被預置的記憶體屏障所截獲,然後立即根據Region上的轉發表記錄將訪問轉發到新複製的物件上,並同時修正更新該引用的值,使其直接指向新物件,ZGC將這種行為稱為指標的“自愈”(Self-Healing)能力。這樣做的好處是隻有第一次訪問舊物件會陷入轉發,也就是隻慢一次,對比Shenandoah的Brooks轉發指標,那是每次物件訪問都必須付出的固定開銷,簡單地說就是每次都慢,因此ZGC對使用者程式的執行時負載要比Shenandoah來得更低一些。還有另外一個直接的好處是由於染色指標的存在,一旦重分配集中某個Region的存活物件都複製完畢後,這個Region就可以立即釋放用於新物件的分配(但是轉發表還得留著不能釋放掉),哪怕堆中還有很多指向這個物件的未更新指標也沒有關係,這些舊指標一旦被使用,它們都是可以自愈的。
·併發重對映 (Concurrent Remap):重對映所做的就是修正整個堆中指向重分配集中舊物件的所有引用,這一點從目標角度看是與Shenandoah併發引用更新階段一樣的,但是ZGC的併發重對映並不是一個必須要“迫切”去完成的任務,因為前面說過,即使是舊引用,它也是可以自愈的,最多隻是第一次
使用時多一次轉發和修正操作。重對映清理這些舊引用的主要目的是為了不變慢(還有清理結束後可以釋放轉發表這樣的附帶收益),所以說這並不是很“迫切”。因此,ZGC很巧妙地把併發重對映階段要做的工作,合併到了下一次垃圾收集迴圈中的併發標記階段裡去完成,反正它們都是要遍歷所有物件的,這樣合併就節省了一次遍歷物件圖 [9] 的開銷。一旦所有指標都被修正之後,原來記錄新舊物件關係的轉發表就可以釋放掉了。

15.ZGC收集器中的染色指標有什麼用?

染色指標是一種直接將少量額外的資訊儲存在指標上的技術,可是為什麼指標本身也可以儲存額外資訊呢?在64位系統中,理論可以訪問的記憶體高達16EB(2的64次冪)位元組 [3] 。實際上,基於需求(用不到那麼多記憶體)、效能(地址越寬在做地址轉換時需要的頁表級數越多)和成本(消耗更多電晶體)的考慮,在AMD64架構 [4] 中只支援到52位(4PB)的地址匯流排和48位(256TB)的虛擬地址空間,所以目前64位的硬體實際能夠支援的最大記憶體只有256TB。此外,作業系統一側也還會施加自己的約束,64位的Linux則分別支援47位(128TB)的程序虛擬地址空間和46位(64TB)的實體地址空間,64位的Windows系統甚至只支援44位(16TB)的實體地址空間。
儘管Linux下64位指標的高18位不能用來定址,但剩餘的46位指標所能支援的64TB記憶體在今天仍然能夠充分滿足大型伺服器的需要。鑑於此,ZGC的染色指標技術繼續盯上了這剩下的46位指標寬度,將其高4位提取出來儲存四個標誌資訊。通過這些標誌位,虛擬機器可以直接從指標中看到其引用物件的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到。當然,由於這些標誌位進一步壓縮了原本就只有46位的地址空間,也直接導致ZGC能夠管理的記憶體不可以超過4TB(2的42次冪) 。

16.說說類載入的過程

載入
驗證
準備(為一些類變數分配記憶體,並將其初始化為預設值)
解析(將符號引用替換為直接引用。類和介面、類方法、介面方法、欄位等解析)
初始化

17.說下有哪些類載入器?

Bootstrap ClassLoader(啟動類載入器)
Extention ClassLoader(擴充套件類載入器)
App ClassLoader(應用類載入器)

18.什麼是雙親委派機制?

雙親委派機制的意思是除了頂層的啟動類載入器以外,其餘的類載入器,在載入之前,都會委派給它的父載入器進行載入。這樣一層層向上傳遞,直到祖先們都無法勝任,它才會真正的載入。

19.雙親委派機制可以被違背嗎?請舉例說明。

可以被違背。
打破雙親委派的例子:Tomcat

對於一些需要載入的非基礎類,會由一個叫作WebAppClassLoader的類載入器優先載入。等它載入不到的時候,再交給上層的ClassLoader進行載入。
這個載入器用來隔絕不同應用的 .class 檔案,比如你的兩個應用,可能會依賴同一個第三方的不同版本,它們是相互沒有影響的。

20.Tomcat是怎麼打破雙親委派機制的呢?

是通過重寫ClassLoader#loadClass和ClassLoader#findClass 實現的。可以看圖中的WebAppClassLoader,它載入自己目錄下的.class檔案,並不會傳遞給父類的載入器。但是,它卻可以使用 SharedClassLoader 所載入的類,實現了共享和分離的功能。

21.Java物件的佈局瞭解過嗎?

物件頭區域此處儲存的資訊包括兩部分:
1、物件自身的執行時資料( MarkWord ),佔8位元組
儲存 hashCode、GC 分代年齡、鎖型別標記、偏向鎖執行緒 ID 、 CAS 鎖指向執行緒 LockRecord 的指標等, synconized 鎖的機制與這個部分( markwork )密切相關,用 markword 中最低的三位代表鎖的狀態,其中一位是偏向鎖位,另外兩位是普通鎖位。
2、物件型別指標( Class Pointer ),佔4位元組
物件指向它的類元資料的指標、 JVM 就是通過它來確定是哪個 Class 的例項。

例項資料區域
此處儲存的是物件真正有效的資訊,比如物件中所有欄位的內容

對齊填充區域
JVM 的實現 HostSpot 規定物件的起始地址必須是 8 位元組的整數倍,換句話來說,現在 64 位的 OS 往外讀取資料的時候一次性讀取 64bit 整數倍的資料,也就是 8 個位元組,所以 HotSpot 為了高效讀取物件,就做了"對齊",如果一個物件實際佔的記憶體大小不是 8byte 的整數倍時,就"補位"到 8byte 的整數倍。所以對齊填充區域的大小不是固定的。

22.什麼情況下會發生棧記憶體溢位?

棧是執行緒私有的,他的生命週期與執行緒相同,每個方法在執行的時候都會建立一個棧幀,用來儲存區域性變量表,運算元棧,動態連結,方法出口等資訊。區域性變量表又包含基本資料型別,物件引用型別。
如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverflowError異常,方法遞迴呼叫產生這種結果。
如果Java虛擬機器棧可以動態擴充套件,並且擴充套件的動作已經嘗試過,但是無法申請到足夠的記憶體去完成擴充套件,或者在新建立執行緒的時候沒有足夠的記憶體去建立對應的虛擬機器棧,那麼Java虛擬機器將丟擲一個OutOfMemory 異常。(執行緒啟動過多)。

23.JVM新生代中為什麼要分為Eden和Survivor?

如果沒有Survivor,Eden區每進行一次Minor GC,存活的物件就會被送到老年代。老年代很快被填滿,觸發Major GC.老年代的記憶體空間遠大於新生代,進行一次Full GC消耗的時間比Minor GC長得多,所以需要分為Eden和Survivor。
Survivor的存在意義,就是減少被送到老年代的物件,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的物件,才會被送到老年代。
設定兩個Survivor區最大的好處就是解決了碎片化,剛剛新建的物件在Eden中,經歷一次Minor GC,Eden中的存活物件就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活物件又會被複制送入第二塊survivor space S1(這個過程非常重要,因為這種複製演算法保證了S1中來自S0和Eden兩部分的存活物件佔用連續的記憶體空間,避免了碎片化的發生)

24.JVM中一次完整的GC流程是怎樣的,物件如何晉升到老年代?

當 Eden 區的空間滿了, Java虛擬機器會觸發一次 Minor GC,以收集新生代的垃圾,存活下來的物件,則會轉移到 Survivor區。
大物件(需要大量連續記憶體空間的Java物件,如那種很長的字串)直接進入老年態;
如果物件在Eden出生,並經過第一次Minor GC後仍然存活,並且被Survivor容納的話,年齡設為1,每熬過一次Minor GC,年齡+1,若年齡超過一定限制(15),則被晉升到老年態。即長期存活的物件進入老年態。
老年代滿了而無法容納更多的物件,Minor GC 之後通常就會進行Full GC,Full GC 清理整個記憶體堆 – 包括年輕代和年老代。
Major GC 發生在老年代的GC,清理老年區,經常會伴隨至少一次Minor GC,比Minor GC慢10倍以上。

25.什麼是指令重排序?

在實際執行時,程式碼指令可能並不是嚴格按照程式碼語句順序執行的。大多數現代微處理器都會採用將指令亂序執行(out-of-order execution,簡稱OoOE或OOE)的方法,在條件允許的情況下,直接運行當前有能力立即執行的後續指令,避開獲取下一條指令所需資料時造成的等待。通過亂序執行的技術,處理器可以大大提高執行效率。而這就是指令重排。

26.什麼是記憶體屏障?

記憶體屏障,也叫記憶體柵欄,是一種CPU指令,用於控制特定條件下的重排序和記憶體可見性問題。
LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。
StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。 在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能。

27.什麼是happen-before原則?

單執行緒happen-before原則:在同一個執行緒中,書寫在前面的操作happen-before後面的操作。 鎖的happen-before原則:同一個鎖的unlock操作happen-before此鎖的lock操作。
volatile的happen-before原則:對一個volatile變數的寫操作happen-before對此變數的任意操作(當然也包括寫操作了)。
happen-before的傳遞性原則:如果A操作 happen-before B操作,B操作happen-before C操作,那麼A操作happen-before C操作。
執行緒啟動的happen-before原則:同一個執行緒的start方法happen-before此執行緒的其它方法。
執行緒中斷的happen-before原則 :對執行緒interrupt方法的呼叫happen-before被中斷執行緒的檢測到中斷髮送的程式碼。
執行緒終結的happen-before原則: 執行緒中的所有操作都happen-before執行緒的終止檢測。
物件建立的happen-before原則: 一個物件的初始化完成先於他的finalize方法呼叫。

28.說說你知道的幾種主要的JVM引數

1)堆疊配置相關
-Xmx3550m: 最大堆大小為3550m。
-Xms3550m: 設定初始堆大小為3550m。
-Xmn2g: 設定年輕代大小為2g。
-Xss128k: 每個執行緒的堆疊大小為128k。
-XX:MaxPermSize: 設定持久代大小為16m
-XX:NewRatio=4: 設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區佔整個年輕代的1/6
-XX:MaxTenuringThreshold=0: 設定垃圾最大年齡。如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代。

2)垃圾收集器相關
-XX:+UseParallelGC: 選擇垃圾收集器為並行收集器。
-XX:ParallelGCThreads=20: 配置並行收集器的執行緒數
-XX:+UseConcMarkSweepGC: 設定年老代為併發收集。
-XX:CMSFullGCsBeforeCompaction:由於併發收集器不對記憶體空間進行壓縮、整理,所以執行一段時間以後會產生“碎片”,使得執行效率降低。此值設定執行多少次GC以後對記憶體空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection: 開啟對年老代的壓縮。可能會影響效能,但是可以消除碎片

3)輔助資訊相關
-XX:+PrintGC 輸出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails 輸出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs

29.怎麼打出執行緒棧資訊?

輸入jps,獲得程序號。
top -Hp pid 獲取本程序中所有執行緒的CPU耗時效能
jstack pid命令檢視當前java程序的堆疊狀態
或者 jstack -l > /tmp/output.txt 把堆疊資訊打到一個txt檔案。
可以使用fastthread 堆疊定位(fastthread.io)

30.為什麼需要雙親委派模式?

在這裡,先想一下,如果沒有雙親委派,那麼使用者是不是可以自己定義一個java.lang.Object的同名類,java.lang.String的同名類,並把它放到ClassPath中,那麼類之間的比較結果及類的唯一性將無法保證,因此,為什麼需要雙親委派模型?防止記憶體中出現多份同樣的位元組碼。

31.怎麼打破雙親委派模型?

打破雙親委派機制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法。

32.說一下堆和棧的區別

1)實體地址
堆的實體地址分配對物件是不連續的。因此效能慢些。在GC的時候也要考慮到不連續的分配,所以有各種演算法。比如,標記-消除,複製,標記-壓縮,分代(即新生代使用複製演算法,老年代使用標記——壓縮)
棧使用的是資料結構中的棧,先進後出的原則,實體地址分配是連續的。所以效能快。

2)記憶體分別
堆因為是不連續的,所以分配的記憶體是在執行期確認的,因此大小不固定。一般堆大小遠遠大於棧。
棧是連續的,所以分配的記憶體大小要在編譯期就確認,大小是固定的。

3)存放的內容
堆存放的是物件的例項和陣列。因此該區更關注的是資料的儲存
棧存放:區域性變數,運算元棧,返回結果。該區更關注的是程式方法的執行。

4)程式的可見度
堆對於整個應用程式都是共享、可見的。
棧只對於執行緒是可見的。所以也是執行緒私有。他的生命週期和執行緒相同。

33.Java 8 為什麼要將永久代(PermGen)替換為元空間(MetaSpace)呢?

整個永久代有一個 JVM 本身設定固定大小上線,無法進行調整,而元空間使用的是直接記憶體,受本機可用記憶體的限制,並且永遠不會出現java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize 標誌設定最大元空間大小,預設值為 unlimited,這意味著它只受系統記憶體的限制。-XX:MetaspaceSize 調整標誌定義元空間的初始大小如果未指定此標誌,則 Metaspace 將根據執行時的應用程式需求動態地重新調整大小。

34.說一下Java物件的建立過程

1)類載入檢查: 虛擬機器遇到一條 new 指令時,首先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被載入過、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。
2)分配記憶體: 在類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需的記憶體大小在類載入完成後便可確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從 Java 堆中劃分出來。分配方式有 “指標碰撞” 和 “空閒列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
選擇以上2種方式中的哪一種,取決於 Java 堆記憶體是否規整。而 Java 堆記憶體是否規整,取決於 GC 收集器的演算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製演算法記憶體也是規整的。

在建立物件的時候有一個很重要的問題,就是執行緒安全,因為在實際開發過程中,建立物件是很頻繁的事情,作為虛擬機器來說,必須要保證執行緒是安全的,通常來講,虛擬機器採用兩種方式來保證執行緒安全:
CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。虛擬機器採用 CAS 配上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個執行緒預先在Eden區分配一塊兒記憶體,JVM在給執行緒中的物件分配記憶體時,首先在TLAB分配,當物件大於TLAB中的剩餘記憶體或TLAB的記憶體已用盡時,再採用上述的CAS進行記憶體分配

3)初始化零值: 記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。

4)設定物件頭: 初始化零值完成之後,虛擬機器要對物件進行必要的設定,例如這個物件是那個類的例項、如何才能找到類的元資料資訊、物件的雜湊嗎、物件的 GC 分代年齡等資訊。 這些資訊存放在物件頭中。 另外,根據虛擬機器當前執行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式。

5)執行 init 方法: 在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了,但從 Java 程式的視角來看,物件建立才剛開始,init 方法還沒有執行,所有的欄位都還為零。所以一般來說,執行 new 指令之後會接著執行 init 方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。

35.物件的訪問定位有哪幾種方式?

建立物件就是為了使用物件,我們的Java程式通過棧上的 reference 資料來操作堆上的具體物件。物件的訪問方式有虛擬機器實現而定,目前主流的訪問方式有使用控制代碼和直接指標2種:

控制代碼: 如果使用控制代碼的話,那麼Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference 中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。

直接指標: 如果使用直接指標訪問,那麼 Java 堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference 中儲存的直接就是物件的地址。

這兩種物件訪問方式各有優勢。使用控制代碼來訪問的最大好處是 reference 中儲存的是穩定的控制代碼地址,在物件被移動時只會改變控制代碼中的例項資料指標,而 reference 本身不需要修改。使用直接指標訪問方式最大的好處就是速度快,它節省了一次指標定位的時間開銷。

36.說一下堆記憶體中物件的分配的基本策略

eden區、s0區、s1區都屬於新生代,tentired 區屬於老年代。大部分情況,物件都會首先在 Eden 區域分配,在一次新生代垃圾回收後,如果物件還存活,則會進入 s0 或者 s1,並且物件的年齡還會加 1(Eden區->Survivor 區後物件的初始年齡變為1),當它的年齡增加到一定程度(預設為15歲),就會被晉升到老年代中。物件晉升到老年代的年齡閾值,可以通過引數 -XX:MaxTenuringThreshold 來設定。
另外,大物件和長期存活的物件會直接進入老年代。

37.Minor Gc和Full GC 有什麼不同呢?

大多數情況下,物件在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC。
新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。

38.Java會存在記憶體洩漏嗎?請簡單描述。

記憶體洩漏是指不再被使用的物件或者變數一直被佔據在記憶體中。理論上來說,Java是有GC垃圾回收機制的,也就是說,不再被使用的物件,會被GC自動回收掉,自動從記憶體中清除

但是,即使這樣,Java也還是存在著記憶體洩漏的情況,java導致記憶體洩露的原因很明確:長生命週期的物件持有短生命週期物件的引用就很可能發生記憶體洩露,儘管短生命週期物件已經不再需要,但是因為長生命週期物件持有它的引用而導致不能被回收,這就是java中記憶體洩露的發生場景。

39.如何判斷一個類是無用的類?

方法區主要回收的是無用的類,判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類” :
該類所有的例項都已經被回收,也就是 Java 堆中不存在該類的任何例項。
載入該類的 ClassLoader 已經被回收。
該類對應的 java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機器可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而並不是和物件一樣不使用了就會必然被回收。

40.介紹一下類檔案結構吧!

魔數: 確定這個檔案是否為一個能被虛擬機器接收的 Class 檔案。
Class 檔案版本 :Class 檔案的版本號,保證編譯正常執行。
常量池 :常量池主要存放兩大常量:字面量和符號引用。
訪問標誌 :標誌用於識別一些類或者介面層次的訪問資訊,包括:這個 Class 是類還是介面,是否為 public 或者 abstract 型別,如果是類的話是否宣告為 final 等等。
當前類索引,父類索引 :類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名,由於 Java 語言的單繼承,所以父類索引只有一個,除了 java.lang.Object 之外,所有的 java 類都有父類,因此除了 java.lang.Object 外,所有 Java 類的父類索引都不為 0。
介面索引集合 :介面索引集合用來描述這個類實現了那些介面,這些被實現的介面將按implents(如果這個類本身是介面的話則是extends) 後的介面順序從左到右排列在介面索引集合中。
欄位表集合 :描述介面或類中宣告的變數。欄位包括類級變數以及例項變數,但不包括在方法內部宣告的區域性變數。
方法表集合 :類中的方法。
屬性表集合 : 在 Class 檔案,欄位表,方法表中都可以攜帶自己的屬性表集合。

41.說一下 JVM 調優的工具?

常用調優工具分為兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory AnalyzerTool)、GChisto。

jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制檯,用於對JVM中記憶體, 執行緒和類等的監控。
jvisualvm,jdk自帶全能工具,可以分析記憶體快照、執行緒快照;監控記憶體變化、GC變化等。
MAT,Memory Analyzer Tool,一個基於Eclipse的記憶體分析工具,是一個快速、功能豐富的Javaheap分析工具,它可以幫助我們查詢記憶體洩漏和減少記憶體消耗。
GChisto,一款專業分析gc日誌的工具。

42.JVM調優命令有哪些?

jps,JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機器程序。
jstat,JVM statistics Monitoring是用於監視虛擬機器執行時狀態資訊的命令,它可以顯示出虛擬機器程序中的類裝載、記憶體、垃圾收集、JIT編譯等執行資料。
jmap,JVM Memory Map命令用於生成heap dump檔案
jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內建了一個微型的HTTP/HTML伺服器,生成dump的分析結果後,可以在瀏覽器中檢視
jstack,用於生成java虛擬機器當前時刻的執行緒快照。
jinfo,JVM Configuration info 這個命令作用是實時檢視和調整虛擬機器執行引數。

43.JRE、JDK、JVM 及 JIT 之間有什麼不同?

JRE 代表 Java 執行時(Java run-time),是執行 Java 引用所必須的。JDK 代表 Java 開發工具(Java development kit),是 Java 程式的開發工具,如 Java編譯器,它也包含 JRE。JVM 代表 Java 虛擬機器(Java virtual machine),它的責任是執行 Java 應用。JIT 代表即時編譯(Just In Time compilation),當代碼執行的次數超過一定的閾值時,會將 Java 位元組碼轉換為原生代碼,如,主要的熱點程式碼會被準換為原生代碼,這樣有利大幅度提高 Java 應用的效能。

44.程式計數器為什麼是私有的?

程式計數器主要有下面兩個作用:

位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。
在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了。
需要注意的是,如果執行的是 native 方法,那麼程式計數器記錄的是 undefined 地址,只有執行的是 Java 程式碼時程式計數器記錄的才是下一條指令的地址。

所以,程式計數器私有主要是為了執行緒切換後能恢復到正確的執行位置。

45.如何判斷一個常量是廢棄常量 ?

執行時常量池主要回收的是廢棄的常量。假如在常量池中存在字串 "abc",如果當前沒有任何 String 物件引用該字串常量的話,就說明常量 "abc" 就是廢棄常量,如果這時發生記憶體回收的話而且有必要的話,"abc" 就會被系統清理出常量池。

刷題小程式

參考資料

https://blog.csdn.net/qq_41701956/article/details/100074023
https://blog.csdn.net/cunily/article/details/106915944
https://www.cnblogs.com/chengxuyuanxiaoyang/p/13692997.html
https://github.com/JavaInterviewHub/JavaInterview/blob/main/JavaIO.md