JVM面試題總結


 

1、介紹下 Java 內存區域(運行時數據區)

Java 虛擬機在執行 Java 程序的過程中會把它管理的內存划分成若干個不同的數據區域。

JDK 1.8之前主要分為:堆、方法區、虛擬機棧、本地方法棧、程序計數器。其中堆和方法區是線程共享的,虛擬機棧、本地方法棧、程序計數器是線程私有的。

JDK 1.8 的時候,方法區(HotSpot的永久代)被徹底移除了,取而代之是元空間元空間使用的是直接內存

程序計數器

  可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一次需要執行的字節碼的指令,分支、循環、跳轉、異常處理等都需要依賴這個計數器來完成。

  在多線程的情況下,程序計數器用於記錄當前線程執行的位置,因此為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,即線程私有。

  如果線程正在執行一個Java方法,這個計數器記錄的正在執行的虛擬機字節碼指令的地址;如果正在執行Native方法,這個計數器值為空。

  【此內存區域是唯一一個不會拋出OutOfMemoryError的區域】

虛擬機棧

  虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

  【有兩種異常StackOverFlowError和 OutOfMemoneyError:當線程請求棧深度大於虛擬機所允許的深度就會拋出StackOverFlowError錯誤;虛擬機棧動態擴展,當擴展無法申請到足夠的內存空間時候,拋出OutOfMemoneyError。】

局部變量表:存放了編譯期可知的各種基本數據類型(boolean、byte、int、long、float、double、char、boolean)、對象引用(reference類型,它可能是一個指向對象起始地址的指針,也可能是指向一個代表對象的句柄)和returnAddress類型(指向了一條字節碼指令的地址)。

【注】64位的long和double類型數據占用2個局部變量空間(slot),其余的數據類型只占1個。局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。

操作數棧:用來存放操作數。Java 程序編譯之后就變成了一條條字節碼指令,Java字節碼指令的操作數存放在操作數棧中,當執行某條帶 n個操作數的指令時,就從棧頂取n個操作數,然后把指令的計算結果(如果有的話)入棧。

動態鏈接:class文件的常量池中有大量的符號引用,字節碼中的方法調用指令就以常量池指向的方法的符號引用作為參數,這些符號引用一部分會在類加載階段(解析階段)或者第一次使用的時候就轉化為直接引用,這種轉化成為靜態解析,另一部分在沒一次運行期間轉化為直接引用,這部分成為動態連接。

方法出口:,即方法返回地址。一個方法在執行時,只有兩種方式退出這個方法:正常完成出口和異常完成出口:

  • 正常完成出口:執行引擎遇到一個方法返回的字節碼指令,這時候執行引擎讀取棧幀中的方法返回地址,將返回值傳遞給上層的方法調用者。
  • 異常完成出口:在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,也就是在本地異常表內沒有搜索到匹配的異常處理器,就會導致方法退出。這時候執行引擎不會讀取方法返回地址而直接停止執行,上層調用者不會得到任何返回值。

本地方法棧

  和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。

  Java 虛擬機所管理的內存中最大的一塊,此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存

  Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC堆。由於現在收集器基本都采用分代垃圾收集算法,所以Java堆還可以細分為:新生代和老年代:再細致一點有:Eden空間、From Survivor、To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能划分出多個線程私有的分配緩沖區(TLAB)。不過無論如何划分,都與存放內容無關,無論哪個區域存儲的都是對象實例,進一步划分的目的是為了更好地回收內存或更快地分配內存。

  【如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoneyError】

方法區

  用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據

  運行時常量池是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。常量池具有一定的動態性,常量並不一定只在編譯期產生,運行期間的常量也可以添加進入常量池中,比如string的intern()方法。

  垃圾回收很少光顧方法區,不過也是需要回收的,主要針對常量池回收,類型卸載。

  【當方法區無法滿足內存分配要求時,將拋出OutOfMemoneyError異常,當常量池無法再申請到內存時也會拋出OutOfMemoneyError】

2、Java 對象的創建過程(五步,建議能默寫出來並且要知道每一步虛擬機做了什么)

類加載檢查 虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程

分配內存 在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆中划分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定(“標記-清除”、“標記-壓縮”)。

 初始化零值 內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用。

 設置對象頭 初始化零值完成之后,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。

 執行 init 方法執行 new 指令之后會接着執行 <init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來

3、對象的訪問定位的兩種方式(句柄和直接指針兩種方式)

 Java程序需要通過棧上的reference數據來操作堆上的具體對象。

  • 句柄: 如果使用句柄的話,那么Java堆中將會划分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息

使用句柄

  • 直接指針 如果使用直接指針訪問reference 中存儲的直接就是對象的地址

使用直接指針

這兩種對象訪問方式各有優勢:

  • 使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改
  • 使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷

4、內存是如何分配和回收的?

  • Java 堆是垃圾收集器管理的主要區域,Java 堆可以分為:新生代和老年代:再細致一點可以分為:Eden空間、From Survivor、To Survivor、tentired。
  • 大部分對象都會首先在 Eden 區域分配,而大對象(需要大量連續內存空間的對象,比如:字符串、數組)直接進入老年代。
  • 在一次新生代垃圾回收后,如果對象還存活,則會進入 s0 或者 s1,並且對象的年齡還會加 1。對象在 Survivor 中每熬過一次年輕代垃圾回收,年齡就增加1歲。當它的年齡增加到一定程度(默認為15歲),就會被晉升到老年代中。
  • 為了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需達到要求的年齡。

5、如何判斷對象是否死亡(兩種方法)

引用計數法可達性分析法

  • 引用計數法給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任何時候計數器為0的對象就是不可能再被使用的。
    • 難以解決對象之間相互循環引用的問題(這樣的話計數器永遠不為0)
  • 可達性分析法:通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。

 對於可達性分析算法而言,不可達的對象並非是“非死不可”的若要宣判一個對象死亡,至少需要經歷兩次標記階段

  1. 如果對象在進行可達性分析后發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件為是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視作不必要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法並且該finalize方法並沒有被執行過,那么,這個對象會被放置在一個叫F-Queue的隊列中,之后會由虛擬機自動建立的、優先級低的Finalizer線程去執行,而虛擬機不必要等待該線程執行結束,即虛擬機只負責建立線程,其他的事情交給此線程去處理。
  2. 對F-Queue中對象進行第二次標記,如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其他變量,那么在第二次標記的時候該對象將從“即將回收”的集合中移除,如果對象還是沒有拯救自己,那就會被回收。

6、簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)

  • 強引用:垃圾回收器絕不會回收它當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
  • 軟引用:當內存空間不足,垃圾回收器就會回收它;內存空間足夠就不會回收。
  • 弱引用:當垃圾回收器掃描它所管轄的內存區域時,一旦發現了具有弱引用的對象,無論內存空間是否足夠都會進行回收。
  • 虛引用:在任何時候都可能被垃圾回收,垃圾回收時會收到一個系統通知。

虛引用與軟引用和弱引用的一個區別在於:

   虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器准備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動

使用軟引用的好處:

  可以加速JVM對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。

7、如何判斷一個常量是廢棄常量

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

8、如何判斷一個類是無用的類

類需要同時滿足下面3個條件才能算是 “無用的類” :

  • 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

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

9、垃圾收集有哪些算法,各自的特點?

標記-清除算法、復制算法、標記-整理算法、分代收集算法

(1)標記-清除:算法分為“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。

  • 效率高,但標記清除后會產生大量不連續的碎片。

 (2)復制:將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。

  •  空間浪費,只能使用一半空間

 (3)標記-整理:根據老年代的特點特出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。

(4)分代收集:一般將java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。

10、HotSpot 為什么要分為新生代和老年代?

 將java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。

11、常見的垃圾回收器有那些?

 Serial收集器、ParNew收集器、Parallel Scavenge收集器、CMS收集器、G1收集器

(1)Serial(串行)收集器是一個單線程收集器。它的 “單線程” 的意義不僅僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結束

(2)ParNew收集器:就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。

(3)Parallel Scavenge收集器關注點是吞吐量高效率的利用CPU)。CMS等垃圾收集器關注點更多的是用戶線程的停頓時間(提高用戶體驗)

(4)CMS垃圾收集器:以獲取最短回收停頓時間為目標的收集器,是HotSpot虛擬機第一款真正意義上的並發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。 “標記-清除”算法實現的

(5)G1收集器:是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征

12、介紹一下 CMS,G1 收集器

(1)CMS收集器

  CMS收集器是 HotSpot 虛擬機第一款真正意義上的並發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。它是一種以獲取最短回收停頓時間為目標的收集器,非常符合在注重用戶體驗的應用上使用。

  CMS 收集器是基於 “標記-清除”算法實現的,它的運作過程分為四個步驟:

  1. 初始標記: 暫停所有的其他線程,並標記直接與 GC Roots 相連的對象,速度很快
  2. 並發標記: 同時開啟 GC 和用戶線程,用一個閉包結構去記錄可達對象(標記可達對象)。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發生引用更新的地方。
  3. 重新標記: 重新標記階段就是為了修正並發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短。
  4. 並發清除: 開啟用戶線程,同時 GC 線程開始對為標記的區域做清掃。

CMS 垃圾收集器

  CMS垃圾收集器主要優點:並發收集、低停頓

  但是它有下面三個明顯的缺點:

  • 對 CPU 資源敏感;
  • 無法處理浮動垃圾;
  • 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生

(2)G1收集器

  G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征.

  與其他GC收集器相比,G1具備如下特點:

  • 並行與並發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過並發的方式讓java程序繼續執行。
  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
  • 空間整合:與CMS的“標記--清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“復制”算法實現
  • 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內

  在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,但G1不再是這樣。 使用G1收集器時,它將整個Java堆划分成多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,他們都是一部分獨立區域的集合

  G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計划地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值)在后台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這種使用Region划分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率(把內存化整為零)。

  G1收集器的運作大致分為以下幾個步驟:

  • 初始標記:標記GC Roots能直接關聯到的對象,並修改TAMS的值讓下一階段用戶程序並發運行時能在正確可用的Region中創建新對象。這階段需要停頓線程,但耗時很短。
  • 並發標記:從GC Roots開始對堆中對象進行可達性分析,找出存活的對象。這階段耗時較長,但可與用戶並發執行。
  • 最終標記:為了修正在並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remember Set Logs里面,最終標記階段需要把Remember Set Logs的數據合並到Rememberd Set中,這階段需要停頓線程,但是可並發執行。
  • 篩選回收:首先對各個Region的回收價值和成本進行排序,然后根據用戶所期望的GC停頓時間來制定回收計划。

13、Minor Gc 和 Full GC 有什么不同呢?

  • 新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
  • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。

 14、JDK 監控

JDK 命令行,這些命令在 JDK 安裝目錄下的 bin 目錄下:

  • jps (JVM Process Status): 類似 UNIX 的 ps 命令。用戶查看所有 Java 進程的啟動類、傳入參數和 Java 虛擬機參數等信息;
  • jstat( JVM Statistics Monitoring Tool): 用於收集 HotSpot 虛擬機各方面的運行數據;
  • jinfo (Configuration Info for Java) : Configuration Info forJava,顯示虛擬機配置信息;
  • jmap (Memory Map for Java) :生成堆轉儲快照;
  • jhat (JVM Heap Dump Browser ) : 用於分析 heapdump 文件,它會建立一個 HTTP/HTML 服務器,讓用戶可以在瀏覽器上查看分析結果;
  • jstack (Stack Trace for Java):生成虛擬機當前時刻的線程快照,線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合。

15、Java類加載過程

系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分為三步:驗證->准備->解析

  類加載過程

(1)在加載階段,虛擬機需要完成以下3件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流 到JVM內部。
  2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口

  (注:數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。)

(2)驗證這一階段的目的是為了確保Class文件的字節流中所包含的信息符合當前虛擬機的要求,並且不會危害迅疾自身的安全

  從整體上看,驗證階段大致上會完成下面4個階段的校驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證

  

(3)准備:正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。

   注:

  1. 這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中
  2. 這里所設置的初始值"通常情況"下是數據類型默認的零值(如0、0L、null、false等)

   (比如我們定義了public static int value=111 ,那么 value 變量在准備階段的初始值就是 0 而不是111把value賦值為111的動作在初始化階段才會執行)。特殊情況:比如給 value 變量加上了 fianl 關鍵字public static final int value=111 ,那么准備階段 value 的值就被復制為 111。)

 (4)解析:是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量

  在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機為每個類都准備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。

 (5)初始化:開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

看我的這篇文章:https://www.cnblogs.com/toria/p/11161080.html

 何時觸發初始化?

  1. 當遇到 new 、getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用時 ,如果類沒初始化,需要觸發其初始化。
  3. 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
  4. 當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。
  5. 當使用 JDK1.7 的動態動態語言時,如果一個 MethodHandle 實例的最后解析結構為 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,並且這個句柄沒有初始化,則需要先觸發器初始化。

16、什么是類加載器?

  通過一個類的全限定名來獲取描述此類的二進制字節流,實現這個動作的代碼模塊稱為“類加載器”。

  (負責讀取 Java 字節代碼,並轉換成java.lang.Class類的一個實例)

17、類加載器與類的”相同“判斷

  比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

  這里所指的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關系判定等情況。

18、類加載器的種類?

啟動類加載器(Bootstrap ClassLoader):加載 %JAVA_HOME%/lib/ext 目錄下的jar包和類,或者被-Xbootclasspath參數指定的路徑中的所有類。啟動類加載器無法被java程序直接調用。

擴展類加載器(Extension ClassLoade):加載 %JRE_HOME%/lib/ext 目錄下的jar包和類,或者被java.ext.dirs系統變量所指定的路徑下的jar包。

系統類加載器(Application ClassLoader):加載當前應用classpath下的所有jar包和類。如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

自定義類加載器:通過繼承ClassLoader實現,一般是加載我們的自定義類

19、雙親委派模型

  每一個類都有一個對應它的類加載器。系統中的類加載器在協同工作的時候會默認使用雙親委派模型。除了啟動類加載器,每個類都有其父類加載器(父子關系由組合(不是繼承)來實現)。

雙親委派模型工作過程:

  如果一個類加載器收到了類加載的請求,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。加載的時候它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成因此所有的加載請求最終都應該傳送到頂層的啟動類加載器(Bootstrap ClassLoader)中。只有當父加載器無法處理(它的搜索范圍中沒有找到所需的類)時,子下載器才會嘗試自己去加載 當父類加載器為null時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器。

雙親委派好處:

  • 避免同一個類被多次加載;
  • 每個加載器只能加載自己范圍內的類;

 20、如何創建自定義類加載器?

   繼承java.lang.ClassLoader類,然后覆蓋它的findClass(String name)方法即可,即指明如何獲取類的字節碼流。

  • 如果要符合雙親委派規范,則重寫findClass方法(用戶自定義類加載邏輯);要破壞的話,重寫loadClass方法(雙親委派的具體邏輯實現)

 

21.Minor Gc和Full GC?

  • 新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
  • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。

gc觸發條件

  • Minor GC的觸發條件:大多數情況下直接在 Eden 區中進行分配。如果 Eden區域沒有足夠的空間,那么就會發起一次 Minor GC。
  • Full GC(Major GC)的觸發條件:如果老年代沒有足夠空間的話,那么就會進行一次Full GC。

  但這只是一般情況,實際上,需要考慮一個空間分配擔保的問題:

  在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間。如果大於則進行Minor GC,如果不大於則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗(不允許則直接Full GC)。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於則嘗試Minor GC(如果嘗試失敗也會觸發Full GC),如果小於則進行Full GC。

22.可能觸發full GC的機制? Full GC頻繁怎么優化?

(1)老年代空間不足

【原因】老年代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象,當執行Full GC后空間仍然不足,則拋出如下錯誤:java.lang.OutOfMemoryError: Java heap space

【優化】為避免以上兩種狀況引起的Full GC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間以及不要創建過大的對象及數組。

(2)方法區或者元數據空間不足

【原因】方法區中存放的為一些類信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,方法區可能會被占滿,在未配置為采用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那么JVM會拋出如下錯誤信息:

java.lang.OutOfMemoryError: PermGen space

【優化】為避免PermGen(方法區)占滿造成Full GC現象,可采用的方法為增大PermGen空間或轉為使用CMS GC。

(3)System.gc()方法調用

【原因】此方法的調用是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率。

【優化】建議能不使用此方法就別使用,讓虛擬機自己去管理它的內存,可通過通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

(4)CMS GC時出現promotion failed和concurrent mode failure

【原因】對於采用CMS進行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。

promotion failed是在進行Minor GC時,survivor區放不下、對象只能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足造成的(有時候“空間不足”是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC)。

【優化】對應措施為:增大survivor區、老年代的空間 或 調低觸發並發GC的比率。

(5)Minior GC時晉升老年代的內存平均值大於老年代剩余空間

【優化】Hotspot為了避免由於新生代對象晉升到老生代導致老年代空間不足的現象,在進行Minor GC時,做了一個判斷:如果之前統計所得到的Minor GC晉升到老年代的平均大小大於老年代的剩余空間,那么就直接觸發Full GC。例如程序第一次觸發Minor GC后,有6MB的對象晉升到老年代,那么當下一次Minor GC發生時,首先檢查老年代的剩余空間是否大於6MB,如果小於6MB,則執行Full GC。

(6)有連續的大對象需要分配

【優化】所謂大對象,是指需要大量連續內存空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩余空間,但是無法找到足夠大的連續空間來分配給當前對象,此種情況就會觸發JVM進行Full GC。

為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用於在“享受”完Full GC服務之后額外免費贈送一個碎片整理的過程,內存整理的過程無法並發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC后,跟着來一次帶壓縮的。

 

參考https://www.jianshu.com/p/27703ef3de65

23.內存泄漏排查和解決方法?

  • 內存溢出:程序在申請內存時,沒有足夠的內存空間供其使用,出現OutOfMemoryError。例如你申請了10個字節的空間,但是你在這個空間寫入11或以上字節的數據,出現溢出。
  • 內存泄漏:指程序在申請內存后,無法釋放已申請的內存空間。例如你用new申請了一塊內存,后來很長時間都不再使用了(按理應該釋放),但是因為一直被某個或某些實例所持有導致 GC 不能回收,也就是該被釋放的對象沒有釋放。

  內存泄漏過多必然會導致內存溢出。

jvm內存泄漏排查流程:

1.查詢cpu消耗最大的進程

  • jps 找出正在運行的虛擬機進程
  • top 命令查看哪些些java進程消耗的cpu比較大(看PID)

2.找到你需要監控的ID,再利用虛擬機統計信息監視工具jstat監視虛擬機各種運行狀態信息,找出頻繁Full GC對象。(可以得到服務器的Eden區、兩個Survivor區、老年代的已使用百分比、程序運行以來共發生Minor GC、Full GC的次數與耗時、以及總耗時等信息)

  jstat -gcutil 20954 1000  (假設209564是要查的ID,意思是每1000毫秒查詢一次,一直查。gcutil的意思是已使用空間站總空間的百分比。)

3.使用jmap命令查看存活對象情況,發現有數據不正常,十有八九就是泄露的。

  使用命令如下:jmap -histo:live 20954

4.定位到代碼,有很多種方法,比如通過MAT查看Histogram即可找出是哪塊代碼。也可以使用BTrace。

 

參考https://cloud.tencent.com/developer/article/1144256

 

 

 

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

class TestClassLoad {
    @Override
    public String toString() {
        return "類加載成功。";
    }
}
public class PathClassLoader extends ClassLoader {
    private String classPath;

    public PathClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getData(String className) {
        String path = classPath + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0, num);
            }
            return stream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }



    public static void main(String args[]) throws ClassNotFoundException,
            InstantiationException, IllegalAccessException {
        ClassLoader pcl = new PathClassLoader("D:\\ProgramFiles\\eclipseNew\\workspace\\cp-lib\\bin");
        Class c = pcl.loadClass("classloader.TestClassLoad");//注意要包括包名
        System.out.println(c.newInstance());//打印類加載成功.
    }
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM