別逛了,送你一份2023年Java核心篇JVM(虛擬機器)面試題整理

一眼萬年的星空發表於2023-04-01

Java記憶體區域
說一下 JVM 的主要組成部分及其作用?

image.png

JVM包含兩個子系統和兩個元件,兩個子系統為Class loader(類裝載)、Execution engine(執行引擎);兩個元件為Runtime data area(執行時資料區)、Native Interface(本地介面)。

●Class loader(類裝載):根據給定的全限定名類名(如:java.lang.Object)來裝載class檔案到Runtime data area中的method area。
●Execution engine(執行引擎):執行classes中的指令。
●Native Interface(本地介面):與native libraries互動,是其它程式語言互動的介面。
●Runtime data area(執行時資料區域):這就是我們常說的JVM的記憶體。

作用 :首先透過編譯器把 Java 程式碼轉換成位元組碼,類載入器(ClassLoader)再把位元組碼載入到記憶體中,將其放在執行時資料區(Runtime data area)的方法區內,而位元組碼檔案只是 JVM 的一套指令集規範,並不能直接交給底層作業系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將位元組碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要呼叫其他語言的本地庫介面(Native Interface)來實現整個程式的功能。

下面是Java程式執行機制詳細說明

Java程式執行機制步驟
●首先利用IDE整合開發工具編寫Java原始碼,原始檔的字尾為.java;
●再利用編譯器(javac命令)將原始碼編譯成位元組碼檔案,位元組碼檔案的字尾名為.class;
●執行位元組碼的工作是由直譯器(java命令)來完成的。

image.png

從上圖可以看,java檔案透過編譯器變成了.class檔案,接下來類載入器又將這些.class檔案載入到JVM中。
其實可以一句話來解釋:類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個 java.lang.Class物件,用來封裝類在方法區內的資料結構。

說一下 JVM 執行時資料區
Java 虛擬機器在執行 Java 程式的過程中會把它所管理的記憶體區域劃分為若干個不同的資料區域。這些區域都有各自的用途,以及建立和銷燬的時間,有些區域隨著虛擬機器程式的啟動而存在,有些區域則是依賴執行緒的啟動和結束而建立和銷燬。Java 虛擬機器所管理的記憶體被劃分為如下幾個區域:

image.png

不同虛擬機器的執行時資料區可能略微有所不同,但都會遵從 Java 虛擬機器規範, Java 虛擬機器規範規定的區域分為以下 5 個部分:

●程式計數器(Program Counter Register):當前執行緒所執行的位元組碼的行號指示器,位元組碼解析器的工作是透過改變這個計數器的值,來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能,都需要依賴這個計數器來完成;
●Java 虛擬機器棧(Java Virtual Machine Stacks):用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊;
●本地方法棧(Native Method Stack):與虛擬機器棧的作用是一樣的,只不過虛擬機器棧是服務 Java 方法的,而本地方法棧是為虛擬機器呼叫 Native 方法服務的;
●Java 堆(Java Heap):Java 虛擬機器中記憶體最大的一塊,是被所有執行緒共享的,幾乎所有的物件例項都在這裡分配記憶體;
●方法區(Methed Area):用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料。

深複製和淺複製

淺複製(shallowCopy)只是增加了一個指標指向已存在的記憶體地址,
深複製(deepCopy)是增加了一個指標並且申請了一個新的記憶體,使這個增加的指標指向這個新的記憶體,
使用深複製的情況下,釋放記憶體的時候不會因為出現淺複製時釋放同一個記憶體的錯誤。
淺複製:僅僅是指向被複制的記憶體地址,如果原地址發生改變,那麼淺複製出來的物件也會相應的改變。
深複製:在計算機中開闢一塊新的記憶體地址用於存放複製的物件。

說一下堆疊的區別?

實體地址
堆的實體地址分配對物件是不連續的。因此效能慢些。在GC的時候也要考慮到不連續的分配,所以有各種演算法。比如,標記-消除,複製,標記-壓縮,分代(即新生代使用複製演算法,老年代使用標記——壓縮)
棧使用的是資料結構中的棧,先進後出的原則,實體地址分配是連續的。所以效能快。
記憶體分別
堆因為是不連續的,所以分配的記憶體是在執行期確認的,因此大小不固定。一般堆大小遠遠大於棧。
棧是連續的,所以分配的記憶體大小要在編譯期就確認,大小是固定的。
存放的內容
堆存放的是物件的例項和陣列。因此該區更關注的是資料的儲存
棧存放:區域性變數,運算元棧,返回結果。該區更關注的是程式方法的執行。

PS:
1靜態變數放在方法區
2靜態的物件還是放在堆。
程式的可見度
堆對於整個應用程式都是共享、可見的。
棧只對於執行緒是可見的。所以也是執行緒私有。他的生命週期和執行緒相同。

佇列和棧是什麼?有什麼區別?

佇列和棧都是被用來預儲存資料的。
●操作的名稱不同。佇列的插入稱為入隊,佇列的刪除稱為出隊。棧的插入稱為進棧,棧的刪除稱為出棧。
●可操作的方式不同。佇列是在隊尾入隊,隊頭出隊,即兩邊都可操作。而棧的進棧和出棧都是在棧頂進行的,無法對棧底直接進行操作。
●操作的方法不同。佇列是先進先出(FIFO),即佇列的修改是依先進先出的原則進行的。新來的成員總是加入隊尾(不能從中間插入),每次離開的成員總是佇列頭上(不允許中途離隊)。而棧為後進先出(LIFO),即每次刪除(出棧)的總是當前棧中最新的元素,即最後插入(進棧)的元素,而最先插入的被放在棧的底部,要到最後才能刪除。

HotSpot虛擬機器物件探秘

物件的建立
說到物件的建立,首先讓我們看看 Java 中提供的幾種物件建立方式:

image.png

下面是物件建立的主要流程:

image.png

虛擬機器遇到一條new指令時,先檢查常量池是否已經載入相應的類,如果沒有,必須先執行相應的類載入。類載入透過後,接下來分配記憶體。若Java堆中記憶體是絕對規整的,使用“指標碰撞“方式分配記憶體;如果不是規整的,就從空閒列表中分配,叫做”空閒列表“方式。劃分記憶體時還需要考慮一個問題-併發,也有兩種方式: CAS同步處理,或者本地執行緒分配緩衝(Thread Local Allocation Buffer, TLAB)。然後記憶體空間初始化操作,接著是做一些必要的物件設定(元資訊、雜湊碼…),最後執行方法。

為物件分配記憶體

類載入完成後,接著會在Java堆中劃分一塊記憶體分配給物件。記憶體分配根據Java堆是否規整,有兩種方式:
●指標碰撞:如果Java堆的記憶體是規整,即所有用過的記憶體放在一邊,而空閒的的放在另一邊。分配記憶體時將位於中間的指標指示器向空閒的記憶體移動一段與物件大小相等的距離,這樣便完成分配記憶體工作。
●空閒列表:如果Java堆的記憶體不是規整的,則需要由虛擬機器維護一個列表來記錄那些記憶體是可用的,這樣在分配的時候可以從列表中查詢到足夠大的記憶體分配給物件,並在分配後更新列表記錄。
選擇哪種分配方式是由 Java 堆是否規整來決定的,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

image.png

處理併發安全問題

物件的建立在虛擬機器中是一個非常頻繁的行為,哪怕只是修改一個指標所指向的位置,在併發情況下也是不安全的,可能出現正在給物件 A 分配記憶體,指標還沒來得及修改,物件 B 又同時使用了原來的指標來分配記憶體的情況。解決這個問題有兩種方案:

●對分配記憶體空間的動作進行同步處理(採用 CAS + 失敗重試來保障更新操作的原子性);
●把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在 Java 堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer, TLAB)。哪個執行緒要分配記憶體,就在哪個執行緒的 TLAB 上分配。只有 TLAB 用完並分配新的 TLAB 時,才需要同步鎖。透過-XX:+/-UserTLAB引數來設定虛擬機器是否使用TLAB。

image.png

物件的訪問定位

Java程式需要透過 JVM 棧上的引用訪問堆中的具體物件。物件的訪問方式取決於 JVM 虛擬機器的實現。目前主流的訪問方式有 控制程式碼 和 直接指標 兩種方式。

指標: 指向物件,代表一個物件在記憶體中的起始地址。
控制程式碼: 可以理解為指向指標的指標,維護著物件的指標。控制程式碼不直接指向物件,而是指向物件的指標(控制程式碼不發生變化,指向固定記憶體地址),再由物件的指標指向物件的真實記憶體地址。

控制程式碼訪問

Java堆中劃分出一塊記憶體來作為控制程式碼池,引用中儲存物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與物件型別資料各自的具體地址資訊,具體構造如下圖所示:

image.png

優勢:引用中儲存的是穩定的控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而引用本身不需要修改。

直接指標

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

image.png

優勢:速度更快,節省了一次指標定位的時間開銷。由於物件的訪問在Java中非常頻繁,因此這類開銷積少成多後也是非常可觀的執行成本。HotSpot 中採用的就是這種方式。

記憶體溢位異常

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

記憶體洩漏是指不再被使用的物件或者變數一直被佔據在記憶體中。理論上來說,Java是有GC垃圾回收機制的,也就是說,不再被使用的物件,會被GC自動回收掉,自動從記憶體中清除。
但是,即使這樣,Java也還是存在著記憶體洩漏的情況,java導致記憶體洩露的原因很明確:長生命週期的物件持有短生命週期物件的引用就很可能發生記憶體洩露,儘管短生命週期物件已經不再需要,但是因為長生命週期物件持有它的引用而導致不能被回收,這就是java中記憶體洩露的發生場景。

垃圾收集器

簡述Java垃圾回收機制

在java中,程式設計師是不需要顯示的去釋放一個物件的記憶體的,而是由虛擬機器自行執行。在JVM中,有一個垃圾回收執行緒,它是低優先順序的,在正常情況下是不會執行的,只有在虛擬機器空閒或者當前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何引用的物件,並將它們新增到要回收的集合中,進行回收。

GC是什麼?為什麼要GC

GC 是垃圾收集的意思(Gabage Collection),記憶體處理是程式設計人員容易出現問題的地方,忘記或者錯誤的記憶體
回收會導致程式或系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測物件是否超過作用域從而達到自動
回收記憶體的目的,Java 語言沒有提供釋放已分配記憶體的顯示操作方法。

垃圾回收的優點和原理。並考慮2種回收機制

java語言最顯著的特點就是引入了垃圾回收機制,它使java程式設計師在編寫程式時不再考慮記憶體管理的問題。
由於有這個垃圾回收機制,java中的物件不再有“作用域”的概念,只有引用的物件才有“作用域”。
垃圾回收機制有效的防止了記憶體洩露,可以有效的使用可使用的記憶體。
垃圾回收器通常作為一個單獨的低階別的執行緒執行,在不可預知的情況下對記憶體堆中已經死亡的或很長時間沒有用過的物件進行清除和回收。
程式設計師不能實時的對某個物件或所有物件呼叫垃圾回收器進行垃圾回收。
垃圾回收有分代複製垃圾回收、標記垃圾回收、增量垃圾回收。

垃圾回收器的基本原理是什麼?垃圾回收器可以馬上回收記憶體嗎?有什麼辦法主動通知虛擬機器進行垃圾回收?

對於GC來說,當程式設計師建立物件時,GC就開始監控這個物件的地址、大小以及使用情況。
通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有物件。透過這種方式確定哪些物件是"可達的",哪些物件是"不可達的"。當GC確定一些物件為"不可達"時,GC就有責任回收這些記憶體空間。
可以。程式設計師可以手動執行System.gc(),通知GC執行,但是Java語言規範並不保證GC一定會執行。

Java 中都有哪些引用型別?

●強引用:發生 gc 的時候不會被回收。
●軟引用:有用但不是必須的物件,在發生記憶體溢位之前會被回收。
●弱引用:有用但不是必須的物件,在下一次GC時會被回收。
●虛引用(幽靈引用/幻影引用):無法透過虛引用獲得物件,用 PhantomReference 實現虛引用,虛引用的用途是在 gc 時返回一個通知。

怎麼判斷物件是否可以被回收?

垃圾收集器在做垃圾回收的時候,首先需要判定的就是哪些記憶體是需要被回收的,哪些物件是「存活」的,是不可以被回收的;哪些物件已經「死掉」了,需要被回收。
一般有兩種方法來判斷:

●引用計數器法:為每個物件建立一個引用計數,有物件引用時計數器 +1,引用被釋放時計數 -1,當計數器為 0 時就可以被回收。它有一個缺點不能解決迴圈引用的問題;
●可達性分析演算法:從 GC Roots 開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是可以被回收的。

在Java中,物件什麼時候可以被垃圾回收

當物件對當前使用這個物件的應用程式變得不可觸及的時候,這個物件就可以被回收了。
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。這就是為什麼正確的永久代大小對避免Full GC是非常重要的原因。

JVM中的永久代中會發生垃圾回收嗎

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。這就是為什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到後設資料區
(譯者注:Java8中已經移除了永久代,新加了一個叫做後設資料區的native記憶體區)

說一下 JVM 有哪些垃圾回收演算法?

●標記-清除演算法:標記無用物件,然後進行清除回收。缺點:效率不高,無法清除垃圾碎片。
●複製演算法:按照容量劃分二個大小相等的記憶體區域,當一塊用完的時候將活著的物件複製到另一塊上,然後再把已使用的記憶體空間一次清理掉。缺點:記憶體使用率不高,只有原來的一半。
●標記-整理演算法:標記無用物件,讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。
●分代演算法:根據物件存活週期的不同將記憶體劃分為幾塊,一般是新生代和老年代,新生代基本採用複製演算法,老年代採用標記整理演算法。

標記-清除演算法

標記無用物件,然後進行清除回收。
標記-清除演算法(Mark-Sweep)是一種常見的基礎垃圾收集演算法,它將垃圾收集分為兩個階段:
●標記階段:標記出可以回收的物件。
●清除階段:回收被標記的物件所佔用的空間。
標記-清除演算法之所以是基礎的,是因為後面講到的垃圾收集演算法都是在此演算法的基礎上進行改進的。

優點:實現簡單,不需要物件進行移動。
缺點:標記、清除過程效率低,產生大量不連續的記憶體碎片,提高了垃圾回收的頻率。
標記-清除演算法的執行的過程如下圖所示

image.png

複製演算法

為了解決標記-清除演算法的效率不高的問題,產生了複製演算法。它把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾收集時,遍歷當前使用的區域,把存活物件複製到另外一個區域中,最後將當前使用的區域的可回收的物件進行回收。
優點:按順序分配記憶體即可,實現簡單、執行高效,不用考慮記憶體碎片。
缺點:可用的記憶體大小縮小為原來的一半,物件存活率高時會頻繁進行復制。
複製演算法的執行過程如下圖所示

image.png

標記-整理演算法

在新生代中可以使用複製演算法,但是在老年代就不能選擇複製演算法了,因為老年代的物件存活率會較高,這樣會有較多的複製操作,導致效率變低。標記-清除演算法可以應用在老年代中,但是它效率不高,在記憶體回收後容易產生大量記憶體碎片。因此就出現了一種標記-整理演算法(Mark-Compact)演算法,與標記-整理演算法不同的是,在標記可回收的物件後將所有存活的物件壓縮到記憶體的一端,使他們緊湊的排列在一起,然後對端邊界以外的記憶體進行回收。回收後,已用和未用的記憶體都各自一邊。
優點:解決了標記-清理演算法存在的記憶體碎片問題。
缺點:仍需要進行區域性物件移動,一定程度上降低了效率。
標記-整理演算法的執行過程如下圖所示

image.png

分代收集演算法

當前商業虛擬機器都採用分代收集的垃圾收集演算法。分代收集演算法,顧名思義是根據物件的存活週期將記憶體劃分為幾塊。一般包括年輕代、老年代 和 永久代,如圖所示:

image.png

說一下 JVM 有哪些垃圾回收器?

如果說垃圾收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。下圖展示了7種作用於不同分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不同收集器之間的連線表示它們可以搭配使用。

image.png

●Serial收集器(複製演算法): 新生代單執行緒收集器,標記和清理都是單執行緒,優點是簡單高效;
●ParNew收集器 (複製演算法): 新生代收並行集器,實際上是Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現;
●Parallel Scavenge收集器 (複製演算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間),高吞吐量可以高效率的利用CPU時間,儘快完成程式的運算任務,適合後臺應用等對互動相應要求不高的場景;
●Serial Old收集器 (標記-整理演算法): 老年代單執行緒收集器,Serial收集器的老年代版本;
●Parallel Old收集器 (標記-整理演算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
●CMS(Concurrent Mark Sweep)收集器(標記-清除演算法): 老年代並行收集器,以獲取最短回收停頓時間為目標的收集器,具有高併發、低停頓的特點,追求最短GC回收停頓時間。
●G1(Garbage First)收集器 (標記-整理演算法): Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於“標記-整理”演算法實現,也就是說不會產生記憶體碎片。此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

詳細介紹一下 CMS 垃圾回收器?

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對於要求伺服器響應速度的應用上,這種垃圾回收器非常適合。在啟動 JVM 的引數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。
CMS 使用的是標記-清除的演算法實現的,所以在 gc 的時候回產生大量的記憶體碎片,當剩餘記憶體不能滿足程式執行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的效能將會被降低。

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什麼區別?

●新生代回收器:Serial、ParNew、Parallel Scavenge
●老年代回收器:Serial Old、Parallel Old、CMS
●整堆回收器:G1
新生代垃圾回收器一般採用的是複製演算法,複製演算法的優點是效率高,缺點是記憶體利用率低;老年代回收器一般採用的是標記-整理的演算法進行垃圾回收。

簡述分代垃圾回收器是怎麼工作的?

分代回收器有兩個分割槽:老生代和新生代,新生代預設的空間佔比總空間的 1/3,老生代的預設佔比是 2/3。
新生代使用的是複製演算法,新生代裡有 3 個分割槽:Eden、To Survivor、From Survivor,它們的預設佔比是 8:1:1,它的執行流程如下:
●把 Eden + From Survivor 存活的物件放入 To Survivor 區;
●清空 Eden 和 From Survivor 分割槽;
●From Survivor 和 To Survivor 分割槽交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。
每次在 From Survivor 到 To Survivor 移動時都存活的物件,年齡就 +1,當年齡到達 15(預設配置是 15)時,升級為老生代。大物件也會直接進入老生代。
老生代當空間佔用到達某個值之後就會觸發全域性垃圾收回,一般使用標記整理的執行演算法。以上這些迴圈往復就構成了整個分代垃圾回收的整體執行流程。

記憶體分配策略

簡述java記憶體分配與回收策率以及Minor GC和Major GC

所謂自動記憶體管理,最終要解決的也就是記憶體分配和記憶體回收兩個問題。前面我們介紹了記憶體回收,這裡我們再來聊聊記憶體分配。
物件的記憶體分配通常是在 Java 堆上分配(隨著虛擬機器最佳化技術的誕生,某些場景下也會在棧上分配,後面會詳細介紹),物件主要分配在新生代的 Eden 區,如果啟動了本地執行緒緩衝,將按照執行緒優先在 TLAB 上分配。少數情況下也會直接在老年代上分配。總的來說分配規則不是百分百固定的,其細節取決於哪一種垃圾收集器組合以及虛擬機器相關引數有關,但是虛擬機器對於記憶體的分配還是會遵循以下幾種「普世」規則:

物件優先在 Eden 區分配

多數情況,物件都在新生代 Eden 區分配。當 Eden 區分配沒有足夠的空間進行分配時,虛擬機器將會發起一次 Minor GC。如果本次 GC 後還是沒有足夠的空間,則將啟用分配擔保機制在老年代中分配記憶體。
這裡我們提到 Minor GC,如果你仔細觀察過 GC 日常,通常我們還能從日誌中發現 Major GC/Full GC。
●Minor GC 是指發生在新生代的 GC,因為 Java 物件大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非常快;
●Major GC/Full GC 是指發生在老年代的 GC,出現了 Major GC 通常會伴隨至少一次 Minor GC。Major GC 的速度通常會比 Minor GC 慢 10 倍以上。

大物件直接進入老年代

所謂大物件是指需要大量連續記憶體空間的物件,頻繁出現大物件是致命的,會導致在記憶體還有不少空間的情況下提前觸發 GC 以獲取足夠的連續空間來安置新物件。
前面我們介紹過新生代使用的是標記-清除演算法來處理垃圾回收的,如果大物件直接在新生代分配就會導致 Eden 區和兩個 Survivor 區之間發生大量的記憶體複製。因此對於大物件都會直接在老年代進行分配。

長期存活物件將進入老年代

虛擬機器採用分代收集的思想來管理記憶體,那麼記憶體回收時就必須判斷哪些物件應該放在新生代,哪些物件應該放在老年代。因此虛擬機器給每個物件定義了一個物件年齡的計數器,如果物件在 Eden 區出生,並且能夠被 Survivor 容納,將被移動到 Survivor 空間中,這時設定物件年齡為 1。物件在 Survivor 區中每「熬過」一次 Minor GC 年齡就加 1,當年齡達到一定程度(預設 15) 就會被晉升到老年代。

虛擬機器類載入機制

簡述java類載入機制?

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗,解析和初始化,最終形成可以被虛擬機器直接使用的java型別。

描述一下JVM載入Class檔案的原理機制

Java中的所有類,都需要由類載入器裝載到JVM中才能執行。類載入器本身也是一個類,而它的工作就是把class檔案從硬碟讀取到記憶體中。在寫程式的時候,我們幾乎不需要關心類的載入,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的載入所需要的類。
類裝載方式,有兩種 :
1.隱式裝載, 程式在執行過程中當碰到透過new 等方式生成物件時,隱式呼叫類裝載器載入對應的類到jvm中,
2.顯式裝載, 透過class.forname()等方法,顯式載入需要的類
Java類的載入是動態的,它並不會一次性將所有類全部載入後再執行,而是保證程式執行的基礎類(像是基類)完全載入到jvm中,至於其他類,則在需要的時候才載入。這當然就是為了節省記憶體開銷。

什麼是類載入器,類載入器有哪些?

實現透過類的許可權定名獲取該類的二進位制位元組流的程式碼塊叫做類載入器。
主要有一下四種類載入器:
1啟動類載入器(Bootstrap ClassLoader)用來載入java核心類庫,無法被java程式直接引用。
2擴充套件類載入器(extensions class loader):它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。
3系統類載入器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以透過 ClassLoader.getSystemClassLoader()來獲取它。
4使用者自定義類載入器,透過繼承 java.lang.ClassLoader類的方式實現。

說一下類裝載的執行過程?

類裝載分為以下 5 個步驟:
●載入:根據查詢路徑找到相應的 class 檔案然後匯入;
●驗證:檢查載入的 class 檔案的正確性;
●準備:給類中的靜態變數分配記憶體空間;
●解析:虛擬機器將常量池中的符號引用替換成直接引用的過程。符號引用就理解為一個標示,而在直接引用直接指向記憶體中的地址;
●初始化:對靜態變數和靜態程式碼塊執行初始化工作。

什麼是雙親委派模型?

在介紹雙親委派模型之前先說下類載入器。對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立在 JVM 中的唯一性,每一個類載入器,都有一個獨立的類名稱空間。類載入器就是根據指定全限定名稱將 class 檔案載入到 JVM 記憶體,然後再轉化為 class 物件。

image.png

類載入器分類:
●啟動類載入器(Bootstrap ClassLoader),是虛擬機器自身的一部分,用來載入Java_HOME/lib/目錄中的,或者被 -Xbootclasspath 引數所指定的路徑中並且被虛擬機器識別的類庫;
●其他類載入器:
●擴充套件類載入器(Extension ClassLoader):負責載入\lib\ext目錄或Java. ext. dirs系統變數指定的路徑中的所有類庫;
●應用程式類載入器(Application ClassLoader)。負責載入使用者類路徑(classpath)上的指定類庫,我們可以直接使用這個類載入器。一般情況,如果我們沒有自定義類載入器預設就是用這個載入器。
雙親委派模型:如果一個類載入器收到了類載入的請求,它首先不會自己去載入這個類,而是把這個請求委派給父類載入器去完成,每一層的類載入器都是如此,這樣所有的載入請求都會被傳送到頂層的啟動類載入器中,只有當父載入無法完成載入請求(它的搜尋範圍中沒找到所需的類)時,子載入器才會嘗試去載入類。
當一個類收到了類載入請求時,不會自己先去載入這個類,而是將其委派給父類,由父類去載入,如果此時父類不能載入,反饋給子類,由子類去完成類的載入。

JVM調優

說一下 JVM 調優的工具?

JDK 自帶了很多監控工具,都位於 JDK 的 bin 目錄下,其中最常用的是 jconsole 和 jvisualvm 這兩款檢視監控工具。
●jconsole:用於對 JVM 中的記憶體、執行緒和類等進行監控;
●jvisualvm:JDK 自帶的全能分析工具,可以分析:記憶體快照、執行緒快照、程式死鎖、監控記憶體的變化、gc 變化等。

常用的 JVM 調優的引數都有哪些?

●-Xms2g:初始化推大小為 2g;
●-Xmx2g:堆最大記憶體為 2g;
●-XX:NewRatio=4:設定年輕的和老年代的記憶體比例為 1:4;
●-XX:SurvivorRatio=8:設定新生代 Eden 和 Survivor 比例為 8:2;
●–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器組合;
●-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器組合;
●-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器組合;
●-XX:+PrintGC:開啟列印 gc 資訊;
●-XX:+PrintGCDetails:列印 gc 詳細資訊。

相關文章