面試必問之jvm

問題1 說一下jvm內存模型

問題1.1 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的運行時綁定,並且在一個階段進行過程中也可能會激活後一個階段,而不是等待一個階段結束再進行後一個階段。
在這裏插入圖片描述

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自帶三種類加載器,分別是:
啓動類加載器。
擴展類加載器。
應用程序類加載器
他們的繼承關係如下圖:
在這裏插入圖片描述
雙親委派機制工作過程如下:

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

當前classLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用同樣的策略,首先查看自己的緩存,然後委託父類的父類去加載,一直到bootstrp ClassLoader.

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

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

雙親委派的優點如下:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章