天天看點

面試必問之jvm

問題1 說一下jvm記憶體模型

問題1.1 jvm記憶體模型

面試必問之jvm

在這裡插入圖檔描述

棧區:

棧分為java虛拟機棧和本地方法棧

重點是Java虛拟機棧,它是線程私有的,生命周期與線程相同。

每個方法執行都會建立一個棧幀,用于存放局部變量表,操作棧,動态連結,方法出口等。每個方法從被調用,直到被執行完。對應着一個棧幀在虛拟機中從入棧到出棧的過程。

通常說的棧就是指局部變量表部分,存放編譯期間可知的8種基本資料類型,及對象引用和指令位址。局部變量表是在編譯期間完成配置設定,當進入一個方法時,這個棧中的局部變量配置設定記憶體大小是确定的。

會有兩種異常StackOverFlowError和 OutOfMemoneyError。當線程請求棧深度大于虛拟機所允許的深度就會抛出StackOverFlowError錯誤;虛拟機棧動态擴充,當擴充無法申請到足夠的記憶體空間時候,抛出OutOfMemoneyError。

本地方法棧為虛拟機使用到本地方法服務(native)

堆區:

堆被所有線程共享區域,在虛拟機啟動時建立,唯一目的存放對象執行個體。

方法區:

被所有線程共享區域,用于存放已被虛拟機加載的類資訊,常量,靜态變量等資料。被Java虛拟機描述為堆的一個邏輯部分。習慣是也叫它永久代(permanment generation)

垃圾回收很少光顧這個區域,不過也是需要回收的,主要針對常量池回收,類型解除安裝。

常量池用于存放編譯期生成的各種位元組碼和符号引用,常量池具有一定的動态性,裡面可以存放編譯期生成的常量;運作期間的常量也可以添加進入常量池中,比如string的intern()方法。

程式計數器:

目前線程所執行的行号訓示器。通過改變計數器的值來确定下一條指令,比如循環,分支,跳轉,異常處理,線程恢複等都是依賴計數器來完成。

Java虛拟機多線程是通過線程輪流切換并配置設定處理器執行時間的方式實作的。為了線程切換能恢複到正确的位置,每條線程都需要一個獨立的程式計數器,是以它是線程私有的。

唯一一塊Java虛拟機沒有規定任何OutofMemoryError的區塊。

1.2 jvm堆空間是怎麼劃分的

通常情況下分為兩個區塊年輕代和年老代。更細一點年輕代又分為Eden區最要放新建立對象,From survivor 和 To survivor 儲存gc後幸存下的對象,預設情況下各自占比 8:1:1。

不過很多文章介紹分為3個區塊,把方法區算着為永久代。這大概是基于Hotspot虛拟機劃分,然後比如IBM j9就不存在永久代概論。不管怎麼分區,都是存放對象執行個體。

1.3 jvm記憶體有哪些初始化參數

1.JVM運作時堆的大小

-Xms堆的最小值
-Xmx堆空間的最大值           

2.新生代堆空間大小調整

-XX:NewSize新生代的最小值
-XX:MaxNewSize新生代的最大值
-XX:NewRatio設定新生代與老年代在堆空間的大小
-XX:SurvivorRatio新生代中Eden所占區域的大小           

3.永久代大小調整

-XX:MaxPermSize           

問題2 jvm垃圾回收機制有了解嗎?

在java中,程式員是不需要顯示的去釋放一個對象的記憶體的,而是由虛拟機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,隻有在虛拟機空閑或者目前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何引用的對象,并将它們添加到要回收的集合中,進行回收。

問題2.1 java中垃圾收集的方法有哪些?

  1. 标記-清除: 這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是标記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,标記和清除的效率都很低;2.會産生大量不連續的記憶體碎片,導緻以後程式在配置設定較大的對象時,由于沒有充足的連續記憶體而提前觸發一次GC動作。
  2. 複制算法: 為了解決效率問題,複制算法将可用記憶體按容量劃分為相等的兩部分,然後每次隻使用其中的一塊,當一塊記憶體用完時,就将還存活的對象複制到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再将第二塊上的對象複制到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。 于是将該算法進行了改進,記憶體區域不再是按照1:1去劃分,而是将記憶體劃分為8:1:1三部分,較大那份記憶體交Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就将對象複制到第二塊記憶體區上,然後清除Eden區,如果此時存活的對象太多,以至于Survivor不夠時,會将這些對象通過配置設定擔保機制複制到老年代中。(java堆又分為新生代和老年代)
  3. 标記-整理 該算法主要是為了解決标記-清除,産生大量記憶體碎片的問題;當對象存活率較高時,也解決了複制算法的效率問題。它的不同之處就是在清除對象的時候現将可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會産生記憶體碎片了。
  4. 分代收集 現在的虛拟機垃圾收集大多采用這種方式,它根據對象的生存周期,将堆分為新生代和老年代。在新生代中,由于對象生存期短,每次回收都會有大量對象死去,那麼這時就采用複制算法。老年代裡的對象存活率較高,沒有額外的空間進行配置設定擔保,是以可以使用标記-整理 或者 标記-清除。

問題2.2 jvm垃圾收集器有哪幾種?

  • Serial收集器: 單線程的收集器,收集垃圾時,必須stop the world,使用複制算法。
  • ParNew收集器: Serial收集器的多線程版本,也需要stop the world,複制算法。
  • Parallel Scavenge收集器: 新生代收集器,複制算法的收集器,并發的多線程收集器,目标是達到一個可控的吞吐量。如果虛拟機總共運作100分鐘,其中垃圾花掉1分鐘,吞吐量就是99%。
  • Serial Old收集器: 是Serial收集器的老年代版本,單線程收集器,使用标記整理算法。
  • Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多線程,标記-整理算法。
  • CMS(Concurrent Mark Sweep) 收集器: 是一種以獲得最短回收停頓時間為目标的收集器,标記清除算法,運作過程:初始标記,并發标記,重新标記,并發清除,收集結束會産生大量空間碎片。
  • G1收集器: 标記整理算法實作,運作流程主要包括以下:初始标記,并發标記,最終标記,篩選标記。不會産生空間碎片,可以精确地控制停頓。

問題2.3 CMS收集器和G1收集器的差別:

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  • G1收集器收集範圍是老年代和新生代,不需要結合其他收集器使用;
  • CMS收集器以最小的停頓時間為目标的收集器;
  • G1收集器可預測垃圾回收的停頓時間
  • CMS收集器是使用“标記-清除”算法進行的垃圾回收,容易産生記憶體碎片
  • G1收集器使用的是“标記-整理”算法,進行了空間整合,降低了記憶體空間碎片。

問題2.4 JVM中一次完整的GC流程是怎樣的,對象如何晉升到老年代

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

問題2.5 什麼情況下會觸發fullgc

  • 老年代空間不足 老年代空間隻有在新生代對象轉入及建立為大對象、大數組時才會出現不足的現象,當執行Full GC後空間仍然不足,則抛出如下錯誤: java.lang.OutOfMemoryError: Java heap space 為避免以上兩種狀況引起的Full GC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。
  • 永生區空間不足 JVM規範中運作時資料區域中的方法區,在HotSpot虛拟機中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的資訊、常量、靜态變量等資料,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被占滿,在未配置為采用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會抛出如下錯誤資訊: java.lang.OutOfMemoryError: PermGen space 為避免Perm Gen占滿造成Full GC現象,可采用的方法為增大Perm Gen空間或轉為使用CMS GC。
  • CMS GC時出現promotion failed和concurrent mode failure 對于采用CMS進行老年代GC的程式而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能 會觸發Full GC。 promotion failed是在進行Minor GC時,survivor space放不下、對象隻能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在 執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足造成的(有時候“空間不足”是CMS GC時目前的浮動垃圾過多導緻暫時性的空間不足觸發Full GC)。 對措施為:增大survivor space、老年代空間或調低觸發并發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由于JDK的bug29導緻CMS在remark完畢 後很久才觸發sweeping動作。對于這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(機關為ms)來避免。 統計得到的Minor GC晉升到舊生代的平均大小大于老年代的剩餘空間 這是一個較為複雜的觸發情況,Hotspot為了避免由于新生代對象晉升到舊生代導緻舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之 前統計所得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩餘空間,那麼就直接觸發Full GC。 例如程式第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大于6MB,如果小于6MB, 則執行Full GC。 當新生代采用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否 大于6MB,如小于,則觸發對舊生代的回收。 除了以上4種狀況外,對于使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java - Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。
  • 堆中配置設定很大的對象 所謂大對象,是指需要大量連續記憶體空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來配置設定給目前對象,此種情況就會觸發JVM進行Full GC。 為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用于在“享受”完Full GC服務之後額外免費贈送一個碎片整理的過程,記憶體整理的過程無法并發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用于設定在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。

問題2.6 怎麼判斷一個對象是否存活

jvm中有兩種方式判斷對象是否存活

  • 引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決對象互相循環引用的問題。
  • 可達性分析(Reachability Analysis):從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鍊。當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的,不可達對象。

問題2.7 哪些對象可作為GC Roots對象?

  • 虛拟機棧中應用的對象
  • 方法區裡面的靜态對象
  • 方法區常量池的對象
  • 本地方法棧JNI應用的對象

問題3 jvm類加載原理

問題3.1 說一下類的生命周期

java類加載過程:加載-->驗證-->準備-->解析-->初始化,之後類就可以被使用了。絕大部分情況下是按這

樣的順序來完成類的加載全過程的。但是是有例外的地方,解析也是可以在初始化之後進行的,這是為了支援

java的運作時綁定,并且在一個階段進行過程中也可能會激活後一個階段,而不是等待一個階段結束再進行後一個階段。

面試必問之jvm

在這裡插入圖檔描述

1.加載

加載時jvm做了這三件事:

1)通過一個類的全限定名來擷取該類的二進制位元組流

 2)将這個位元組流的靜态存儲結構轉化為方法區運作時資料結構

 3)在記憶體堆中生成一個代表該類的java.lang.Class對象,作為該類資料的通路入口           

2.驗證

驗證、準備、解析這三步可以看做是一個連接配接的過程,将類的位元組碼連接配接到JVM的運作狀态之中

驗證是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,不會威脅到jvm的安全

驗證主要包括以下幾個方面的驗證:

  1)檔案格式的驗證,驗證位元組流是否符合Class檔案的規範,是否能被目前版本的虛拟機處理

2)中繼資料驗證,對位元組碼描述的資訊進行語義分析,確定符合java語言規範           

  3)位元組碼驗證 通過資料流和控制流分析,确定語義是合法的,符合邏輯的

  4)符号引用驗證 這個校驗在解析階段發生

3.準備 為類的靜态變量配置設定記憶體,初始化為系統的初始值。對于final static修飾的變量,

直接指派為使用者的定義值。如下面的例子:這裡在準備階段過後的初始值為0,而不是7

public static int a=7 4.解析

解析是将常量池内的符号引用轉為直接引用(如實體記憶體位址指針)

5.初始化

到了初始化階段,jvm才真正開始執行類中定義的java代碼

1)初始化階段是執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收集

       類中的所有類變量的指派動作和靜态語句塊(static塊)中的語句合并産生的。

  2)當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先觸發其父類的初始化。

  3)虛拟機會保證一個類的<clinit>()方法在多線程環境中被正确加鎖和同步。           

問題3.2 說一下類加載機制

雙親委派 jvm自帶三種類加載器,分别是: 啟動類加載器。 擴充類加載器。 應用程式類加載器 他們的繼承關系如下圖:

面試必問之jvm

在這裡插入圖檔描述

雙親委派機制工作過程如下:

目前ClassLoader首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接傳回原來已經加載的類。每個類加載器都有自己的加載緩存,當一個類被加載了以後就會放入緩存,等下次加載的時候就可以直接傳回了。

目前classLoader的緩存中沒有找到被加載的類的時候,委托父類加載器去加載,父類加載器采用同樣的政策,首先檢視自己的緩存,然後委托父類的父類去加載,一直到bootstrp ClassLoader.

當所有的父類加載器都沒有加載的時候,再由目前的類加載器加載,并将其放入它自己的緩存中,以便下次有加載請求的時候直接傳回。

為啥要搞這麼複雜?自己處理不好嗎?

雙親委派的優點如下:

避免重複加載。當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。 為了安全。避免核心類,比如String被替換。