上海攜程java高階面試題(一)

程序员木子啊a發表於2024-04-14

一、JVM載入Class檔案的原理機制?

在面試java工程師的時候,這道題經常被問到,故需特別注意。

Java中的所有類,都需要由類載入器裝載到JVM中才能執行。類載入器本身也是一個類,而它的工作就是把class檔案從硬碟讀取到記憶體中。在寫程式的時候,我們幾乎不需要關心類的載入,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的載入所需要的類。

Java類的載入是動態的,它並不會一次性將所有類全部載入後再執行,而是保證程式執行的基礎類(像是基類)完全載入到jvm中,至於其他類,則在需要的時候才載入。這當然就是為了節省記憶體開銷。

Java的類載入器有三個,對應Java的三種類:

類載入器是一個用來載入類檔案的類。Java 原始碼透過 javac 編譯器編譯成類 檔案。然後 JVM 來執行類檔案中的位元組碼來執行程式。類載入器負責載入檔案系統、網路或其他來源的類檔案。

有三種預設使用的類載入器:Bootstrap 類載入器、Extension 類載入器和Application 類載入器。每種類載入器都有設定好從哪裡載入類。

Bootstrap 類載入器負責載入 rt.jar 中的 JDK 類檔案,它是所有類載入器的父載入 器 。Bootstrap 類 加 載 器 沒 有 任 何 父 類 加 載 器 , 如 果 你 調 用String.class.getClassLoader() , 會 返 回 null , 任 何 基 於 此 的 代 碼 會 拋 出NullPointerException 異常。Bootstrap 載入器被稱為初始類載入器。

而 Extension 將載入類的請求先委託給它的父載入器,也就是 Bootstrap,如果沒有成功載入的話,再從 jre/lib/ext 目錄下或者 java.ext.dirs 系統屬性定義的目錄下載入類。Extension 載入器由 sun.misc.Launcher$ExtClassLoader 實現。

第三種預設的載入器就是 Application 類載入器了。它負責從 classpath 環境變數中載入某些應用相關的類,classpath 環境變數通常由-classpath 或-cp 命令列選項來定義,或者是 JAR 中的 Manifest 的 classpath 屬性。Application 類加 載 器 是 Extension 類 加 載 器 的 子 加 載 器 。通 過sun.misc.Launcher$AppClassLoader 實現。

三個載入器各自完成自己的工作,但它們是如何協調工作呢?哪一個類該由哪個類載入器完成呢?為了解決這個問題,Java採用了委託模型機制。

委託模型機制的工作原理很簡單:當類載入器需要載入類的時候,先請示其Parent(即上一層載入器)在其搜尋路徑載入,如果找不到,才在自己的搜尋路徑搜尋該類。這樣的順序其實就是載入器層次上自頂而下的搜尋,因為載入器必須保證基礎類的載入。之所以是這種機制,還有一個安全上的考慮:如果某人將一個惡意的基礎類載入到jvm,委託模型機制會搜尋其父類載入器,顯然是不可能找到的,自然就不會將該類載入進來。

我們可以透過這樣的程式碼來獲取類載入器:

注意一個很重要的問題,就是 Java 在邏輯上並不存在 BootstrapKLoader 的實

體!因為它是用 C++編寫的,所以列印其內容將會得到 null。

前面是對類載入器的簡單介紹,它的原理機制非常簡單,就是下面幾個步驟:

1.裝載:查詢和匯入 class 檔案;

2.連線:

3. 初始化:初始化靜態變數,靜態程式碼塊

二、什麼是Java 垃圾回收機制?

什麼是垃圾回收機制:在系統執行過程中,會產生一些無用的物件, 這些物件佔據著一定的記憶體, 如果不對這些物件清理回收無用物件的記憶體, 可能會導致記憶體的耗盡, 所以垃圾回收機制回收的是記憶體。同時 GC 回收的是堆區和方法區的記憶體。

JVM 回收特點:(stop-the-world)當要進行垃圾回收時候, 不管何種 GC 演算法, 除了垃圾回收的執行緒之外其他任何執行緒都將停止執行。被中斷的任務將會在垃圾回收完成後恢復進行。GC 不同演算法或是 GC 調優就是減少 stop-the-world 的時間。à(為何非要 stop-the-world),就像是一個同學的聚會,地上有很多垃圾, 你去打掃, 邊打掃邊丟垃圾怎麼都不可能打掃乾淨的哈。當在垃圾回收時候不暫停所有的程式, 在垃圾回收時候有 new 一個新的物件 B,此時物件 A 是可達 B 的,但是沒有來及標記就把 B 當成無用的物件給清理掉了,這就會導致程式的執行會出現錯誤。

如何判斷哪些物件需要回收呢:

引用計數演算法(java 中不是使用此方法):每個物件中新增一個引用計數器, 當有別人引用它的時候, 計數器就會加 1, 當別人不引用它的時候, 計數器就會減 1, 當計數器為 0的時候物件就可以當成垃圾。演算法簡單, 但是最大問題就是在迴圈引用的時候不能夠正確把物件當成垃圾。

根搜尋方法( 這是後面垃圾蒐集演算法的基礎) :這是 JVM 一般使用的演算法, 設立若干了根物件, 當上述若干個跟物件對某一個物件都不可達的時候, 這個物件就是無用的物件。物件所佔的記憶體可以回收。

根搜尋演算法的基礎上, 現代虛擬機器的實現當中, 垃圾蒐集的演算法主要有三種, 分別是標記-清除演算法、 複製演算法、 標記-整理演算法。

標記-消除演算法:當堆中的有效記憶體被耗盡的時候, 就會停止整個系統, 就會呼叫標記-消除演算法, 主要做兩件事, 1 就是標記, 2 就是清除。然後讓程式恢復。

標記:遍歷所有 GCroots 把可達的物件標記為存活的物件。

清除:把未標記為存活的物件清楚掉。

缺點:

就是效率相對比較低。會導致 stop-the-world 時間過長。

因為無用的物件記憶體不是連續的因此清理後的記憶體也不是連續的, (會產生記憶體碎片)因此JVM 還要維持一個空閒列表, 增加一筆開銷, 同時在以後記憶體使用時候, 去查詢可用的記憶體這個效率也是很低的。

複製演算法:(這個演算法一般適合在新生代 GC), 將原有的記憶體分為兩塊, 每次只適用其中的一塊, 在垃圾回收的時候, 將一塊正在使用的記憶體中存活(上述根搜尋的演算法)的物件複製到另一塊沒有使用的記憶體中, 原來的那一塊全部清除。 與上述的標記-清除演算法相比效率更高,但是不太適合使用在物件存活較多的情況下(如老年代)。

缺點:

每次對整個半區記憶體回收, 因此效率比上面的要高點, 同時在分配記憶體的時候不需要考慮記憶體的碎片。

按照順序分配記憶體。

簡單高效。

但是最大的問題在於此演算法在物件存活率非常低的時候使用, 將可用記憶體分為兩份, 每次只使用一份這樣極大浪費了記憶體。

注意(重要) :

現在的虛擬機器使用複製演算法來進行新生代的記憶體回收。

因為在新生代中絕大多數的物件都是“朝生夕亡”, 所以不需要將整個記憶體分為兩個部分, 而是分為三個部分, 一塊為 Eden 和兩塊較小的 Survivor 空間(比例->8:1:1)。

每次使用 Eden 和其中的一塊 Survivor,垃圾回收時候將上述兩塊中存活的物件複製到另外一塊 Survivor 上, 同時清理上述 Eden 和Survivor。

所以每次新生代就可以使用 90%的記憶體。

只有 10%的記憶體是浪費的。

(不能保證每次新生代都少於 10%的物件存活, 當在垃圾回收複製時候如果一塊 Survivor 不夠時候, 需要老年代來分擔, 大物件直接進入老年代)

標記-整理演算法:

(老年代 GC)在存活率較高的情況下, 複製的演算法效率相對比較低, 同時還要考慮存活率可能為 100%的極端情況, 因此又不能把記憶體分為兩部分的複製演算法。

在上面標記-複製演算法的基礎之上, 演變出了一個新的演算法就是標記-整理演算法。

首先從GCroots 開始標記所有可達的物件, 標記為存活的物件。

然後將存活的物件壓縮到記憶體一端按照記憶體地址的次序依次排列, 然後末端記憶體地址之後的所有記憶體都清除。

總結:

將標記存活的物件按照記憶體地址順序排列到記憶體另一端, 末端記憶體地址之後的記憶體都會被清除。

比較:

相比較於標記-清楚演算法 (傳統的), 該演算法可以解決記憶體碎片問題同時還可以解決複製演算法部分記憶體不能利用的問題。

但是標記-整理演算法的效率也不是很高。

上述演算法都是根據根節點搜尋演算法來判斷一個物件是不是需要回收, 而支撐根節點搜尋演算法能夠正常工作理論依據就是語法中變數作用域的相關內容。

三種演算法比較:

效率:

複製演算法>標記-整理演算法>標記-清除演算法;

記憶體整齊度:

複製演算法=標記-整理演算法>標記-清除演算法

記憶體利用率:

標記-整理演算法=標記-清除演算法>複製演算法

分代收集演算法:

現在使用的 Java 虛擬機器並不是只是使用一種記憶體回收機制, 而是分代收集的演算法。就是將記憶體根據物件存活的週期劃分為幾塊。一般是把堆分為新生代、 和老年代。短命物件存放在新生代中, 長命物件放在老年代中。

對於不同的代, 採用不同的收集演算法:

新生代:

由於存活的物件相對比較少, 因此可以採用複製演算法該演算法效率比較快。

老年代:

由於存活的物件比較多哈, 可以採用標記-清除演算法或是標記-整理演算法

( 注意) 新生態由於根據統計可能有 98%物件存活時間很短因此將記憶體分為一塊比較大的Eden 空間和兩塊較小的 Survivor 空間, 每次使用 Eden 和其中一塊 Survivor。

當回收時, 將Eden 和 Survivor 中還存活著的物件一次性地複製到另外一塊 Survivor 空間上, 最後清理掉Eden 和剛才用過的 Survivor 空間。

上述是垃圾回收機制的演算法, 但是垃圾回收器才是垃圾回收的具體實現:

常見有五個垃圾回收器:

一:序列收集器:(Serial 收集器)

該收集器最古老、 穩定簡單是一個單執行緒的收集器, (stop-the-world) 可能會產生長時間的停頓. serial 收集器一定不能用於伺服器端。

這個收集器型別僅應用於單核 CPU 桌面電腦。

新生代和老年代都會使用 serial 收集器。

新生代使用複製演算法(記憶體分三塊的那個複製演算法)。

老年代使用標記-整理演算法。

二:並行收集器:(Parallel 收集器)

parallel 收集器使用多執行緒並行處理 GC, 因此更快。

當有足夠大的記憶體和大量芯數時, parallel收集器是有用的。

它也被稱為“吞吐量優先垃圾收集器。”

三:並行收集器:(Parallel Old 垃圾收集器)

相比於 parallel 收集器, 他們的唯一區別就是在老年代所執行的 GC 演算法的不同。

它執行三個步驟:

標記-彙總-壓縮( mark – summary – compaction) 。

彙總步驟與清理的不同之處在於, 其將依然倖存的物件分發到 GC 預先處理好的不同區域, 演算法相對清理來說略微複雜一點。

四:並行收集器:(CMS 收集器)

(ConcurrentMark Sweep:併發標記清除) 是一種以獲取最短回收停頓時間為目標的收集器。

適合應用在網際網路站或者 B/S 系統的伺服器上, 這類應用尤其重視伺服器的響應速度, 希望系統停頓時間最短。

五:G1 收集器

這個型別的垃圾收集演算法是為了替代 CMS 收集器而被建立的, 因為 CMS 收集器在長時間持續執行時會產生很多問題。

相關文章