JVM 面試考點

  1. 現在JVM 在很多大廠面試中都有問道,並且在我們實際開發中這個也是對我們實際的開發有重大幫助的,也是你走到高級程序員道路的必修之路。

  2. 那麼我們先看看整體的架構圖,然後在把架構圖裏面的內容一一講解
    JVM 圖.png

  3. 那麼JVM 到底是什麼呢? 首先他是一個虛擬機,然後它是運行在操作系統上的,它又硬件沒有直接交互。

  4. 類裝載器ClassLoader

    • 負責加載class文件,class文件在文件的開頭有特定的文件標識,將class文件字節碼內容加載到內存中,並將這些內容轉換成方法區(Java8 爲原空間)中運行時數據結構並且ClassLoader只負責class文件的加載,至於它是否可以運行,則由Execution Engine 決定。(主要加載的類都在以下目錄中)
      1. echo %JAVA_HOME%
      2. echo %PATH%
      3. echo %CLASSPATH%
        image.png
    • 類加載器有多少種呢 ? 默認JDK 給我們提供三種
      image.png
      1. 虛擬機自帶的加載器
        • 啓動器加載器(Bootstrap)C++
        • 擴展類加載器(Extension)Java
        • 應用程序類加載器(AppClassLoader)Java也叫系統類加載器,加載當前應用的classload的所有類。
      2. 自定義加載器
        • Java.lang.ClassLoader的子類,用戶可以制定類的加載方式。
    • 雙親委派機制
      1. 當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次的類加載器都是如此,因此所有的加載請求都應該傳送到啓動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候(在 它的加載路徑下沒有找到所需加載Class),子類加載器纔會嘗試自己去加載。
      2. 採用雙親委派的一個好處是比如加載位於rt.jar包中的類java.lang.Object,不管是那個加載器加載這個類,最終都委派給頂層的啓動類加載器進行加載,這樣就算使用不同的類加載器最終得到的都是同一個Object對象。
  5. Execution Engine 執行引擎負責解釋命令,提交操作系統執行。

  6. Native Interface 本地接口

    • 本地接口的作用是融合不同的編程語言爲Java所使用,它的初衷是融合C/C++ 程序,Java誕生的時候是C/C++橫行的時候,要想立足,必須調用C/C++程序,於是就在內存中專門開闢了一塊區域處理標記爲native方法,在Execution Engine 執行時加載Native libraies .
    • 目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用已經很少見了,因爲現在異構領域間的通信很發達,比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹。
  7. Native Method Stack

    • 它的具體做法是Native Method Stack 中登記native 方法,在Execution Engine執行時加載本地方法庫。
  8. PC寄存器

    • 每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將執行),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以
      忽略不記。
    • 這塊內存區域很小,它是當前線程執行字節碼的行號指示器,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
    • 如果執行的是一個Native方法,那這個計數器是空的。
    • 用以完成分支、循環、跳轉、異常處理、線程恢復等基礎功能。不會發生內存溢出(OutOfMemory=OOM)錯誤。
  9. Methon Area 方法區

    • 供各個線程運行時的內存區域的,它存儲了每一個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容。上面講的是規範,在不同虛擬機裏頭實現是不一樣的,最典型的就是永久代(PermGen space)和元空間(Metespace )But 實例變量存在堆內存中,和方法區無關。
  10. Stack 棧

    • 棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放了,對於棧來說,不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致,是線程私有的。8種基本類型的變量+對象的引用變量+實例方法都是在函數的棧內存中分佈。
    • 棧中主要存儲的內容有什麼內容? (主要保存3類數據)
      1. 本地變量(local Variables):輸入參數和輸出參數以及方法內的變量。
      2. 棧操作(Operand Stack):記錄出棧,入棧的操作;
      3. 棧幀數據(Frame Data ):包括類文件,方法等等。
    • 棧運行原理:
      1. 棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀F1,並被壓入了棧中,A方法又調用了B方法,於是產生棧幀F2 也被壓入了棧,B方法又調用了C方法,於是產生了棧幀F3也被壓入了棧,… 執行完畢後,先彈出F3棧幀,在彈出F2棧幀,在彈出F1棧幀… 遵循先進後出/後進先出的原則,
      2. 每個方法在執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口、方法出口等信息,每一個方法從調用直至執行完畢的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在256k756k之間,約等於1Mb左右。
    • 棧運行時候,也會出現錯誤
      1. Exception in thread “main” java.lang.StackOverflowError(一般產生這種錯誤主要是棧內存溢出,一般遞歸算法會產生這種異常)
        image.png
    • 一個JVM實例只存儲在一個堆內存,堆內存的大小是可以調節的。類加載器讀取了類文件後,需要把類、方法、常變量放到堆內存中,保存所有引用類型的真實信息,以方便執行器執行,堆內存主要分爲三個部份。
      1. Young Generation Space 新生區 Young/New
      2. Tenure Generation Space 養老區 Old/Tenure
      3. Permanent Space 永久區 Perm
    • Heap 堆(JDK7 以前)
      1. 堆內存邏輯上分爲三個部分:新生、養老、永久。
        image.png
      2. 新生區
        • 新生區是類的誕生、生長、消亡的區域,一個類在這裏生產,應用,最後被垃圾回收器收集,結束生命。新生區又分爲兩部分:伊甸區(Eden space )和倖存者區(Survivor space),所有的類都是在伊甸區被new 出來的。倖存者區分爲兩個:0區(Survivor 0 Space )和 1 區(Survivor 1 Space )。當伊甸區的空間用完了時,程序又需要創建對象,JVM 的垃圾回收器將對伊甸區進行垃圾回收(Minor GC) ,將伊甸園區中的不再被其他對象所引用的對象進行銷燬,然後將伊甸園中剩餘的對象移動到倖存者0區。若0區也滿了 ,在對該區進行垃圾回收,然後移動到1區,如果1區也滿了呢?在移動到養老區。若養老區也滿了,那麼這個給時候將產生Major GC(Full GC) ,進行養老區的內存清理.若養老區執行了Full GC 之後發現依然無法進行對象保存,就會產生OOM異常(OutOfMemoryError)。
        • 如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內存不夠。原因有二:
          • Java 虛擬機的堆內存設置不夠,可以使用-Xms -Xmx 來調整。
          • 代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)
        • Java 堆從GC的角度還可以細分爲新生代(Eden區,From Survior區和To Survior區) 和老年代。
          image.png
        • 新生區的MinorGC過程(複製-> 清空-> 互換)
          1. eden、Survivor From 複製到Survivor To ,年齡 + 1
            • 首先,當Eden區滿的時候會觸發第一次GC,把還活着的對象拷貝到SurvivorFrom區,當Eden在次觸發GC的時候會掃描Eden區和From區域,對這兩個區域進行垃圾回收,經過這次回收後存活的對象,則直接複製到To區域(如果有對象的年齡已經達到了老年的標準,則複製到老年區),同時把這些對象的年齡+1。
          2. 清空Eden SurvivorFrom
            • 然後,清空Eden 和 SurvivorFrom 中的對象,也即複製之後有交換,誰空誰是To。
          3. SurvivorTo 和 SurvivorFrom 互換。
            • 最後,SurvivorTo和SurvivorFrom 互換,原SurvivorTo成爲下一次SurvivorFrom區。部分對象會在From和To區域中複製來複制去,如果交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是15),最終如果還存活,就存入老年代。
    • Sun HotSpot內存管理
      1. 分代管理
        image.png
      2. 爲什麼呢?
        • 因爲不同的對象的生命週期不同,98%的對象是臨時對象。
  • 實際而言,方法區(Method Area)和堆一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯後的代碼等等,雖然JVM規範將方法區描述爲堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
    • 對於HotSpot虛擬機,很多開發者習慣將方法區稱之爲“永久代(Parmanent Gen)” ,但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而已,永久代是方法區(相當於是一個接口interface)的一個實現,jdk1.7的版本中,已經將原本放在永久代的字符串常量池移走。
    • 永久區(java7之前有)
      • 永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 纔會釋放此區域所佔用的內存。
  1. 棧、堆、方法區的交互關係
    image.png

    • HotSpot是使用指針的方式來訪問對象:Java堆中會存放類元數據的地址,reference 存儲的就直接是對象的地址。
  2. JVM 參數調優

    • 先讓我們看看JDK7和JDK8 的區別
      image.png
    • JDK8 以後將最初的永久代取消了,由元空間取代。
      image.png
      1. Java8中,永久代已經被移除,被一個稱爲元空間的區域所取代,元空間的本質和永久帶類似。
      2. 元空間與永久代之間最大的區別在於:永久帶使用JVM的堆內存,但是Java8以後元空間並不在虛擬機中而是使用本機物理內存。
      3. 因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入native memory,字符串池使用和類的靜態變量放入java堆中,這樣可以加載多少類的元數據就不再由MaxPermsize 控制,而由系統的實際可用空間來控制。
    • 堆內存調優
    //查看堆大小
    //返回 Java     虛擬機試圖使用的最大內存量。
    long maxMemory = Runtime.getRuntime().maxMemory() ;
    //返回 Java 虛擬機中的內存總量。
    long totalMemory = Runtime.getRuntime().totalMemory() ;
    System.out.println("MAX_MEMORY = " + maxMemory + "(字節)、" + (maxMemory / (double)1024 / 1024) + "MB");
    System.out.println("TOTAL_MEMORY = " + totalMemory + "(字節)、" + (totalMemory / (double)1024 / 1024) + "MB");
    
     1. Xms 設置初始分配大小,默認爲物理內存的1/64.
     2. Xmx 最大分配大小,默認爲物理內存的 1/4.
     3. XX:+PrintGCDetails 輸出詳細的GC處理日誌。
    
    • 詳細GC收集日誌
      image.png
    • GC 是什麼 ?
      1. 次數上頻繁收集Young 區
      2. 次數上較少收集Old區
      3. 基本不動元空間
    • GC 回收算法
      GC.png
      1. JVM在進行GC時,並非每次都對上面三個內存區域一起回收的,大部分時候回收的都是指新生代。因此GC按照回收的區域又分爲兩種,一種時普通GC(minor GC),一種是全局GC(magor GC 或 Full GC)
      2. Minor GC 和 Full GC的區別
        • 普通GC(Minor GC):只針對新生代區域的GC,指發生在新生代的垃圾收集動作,因爲大多數Java對象存活率不高,所以Minor GC 非常頻繁,一般回收速度也比較快。
        • 全局GC(Major GC 或者 Full GC): 指發生在老年代的垃圾收集動作,出現了Major GC ,經常會伴隨至少一次的Minor GC (但並不是絕對的).Major GC的速度要比Minor GC 上慢上10倍以上。
      3. 四大回收算法
        • 引用計數法(一般基本不採用這種方式)
          image.png
          • 缺點: 每次對對象賦值時均要維護引用計數器,且計數器本身也有一定的消耗:教難處理循環引用。
        • 複製算法
          • 年輕代中使用的Minor GC,這種GC算法採用的複製算法(Copying)
          • HotSpot JVM 把年輕代分爲三部分:一個Eden區和兩個Survivor區(分別叫 From To) 。默認比例爲8:1:1,一般情況下,新創建的對象都會被分配到Eden區(一些對象特殊處理),這些對象經過 第一次Minor GC 後,如果仍然存貨,將會被移動到Survivor區。對象在Survivor區中每傲過一次Minor GC ,年齡就會增加1歲,當它的年齡增加到一定程度時,就會移動到老年代中。因爲年輕代中的對象基本都是很快就會死的(90%以上),所以在年輕代的垃圾回收算法使用的複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另外一塊上面。
          • 優點:複製算法不會產生內存碎片。
          • 缺點:它浪費了一半的內存,這太要命了。如果對象的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有對象都複製一遍,並將所有引用地址重置一遍。複製這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視。 所以從以上描述不難看出,複製算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內存的浪費。
          • 原理: 從根集合(GC Root )開始,通過Tracing從From中找到存活對象拷貝到To中;From和To交換身份,下次內存分配從To開始;
            image.png
          • 在GC開始的時候,對象只會存在於Eden區和名爲“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。
            image.png
        • 標記清楚算法(Mark-Sweep)
          • 老年代一般是由標記清楚或者標記清楚於標記壓縮(標記整理)的混合實現。

          • 算法分成標記和清楚兩個階段,先標記出要回收的對象,然後統一回收這些對象,
            image.png

          • 標記清楚算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將要回收的對象標記一遍,最後統一回收這些對象,完成標記清理工作接下來便讓應用程序恢復運行。

          • 主要運行兩項工作,第一項則是標記,第二則是清楚。

            • 標記:從引用跟節點開始標記遍歷所有的GC Roots ,先標記出要回收的對象。
            • 清楚:遍歷整個堆,把標記的對象清楚。
            • 缺點:此算法需要暫停整個應用,會產生內存碎片。
        • 標記壓縮(Mark-Compact)
          image.png
          • 整理壓縮階段,不再對標記的對象做回收,而是通過所有存活的對象都向一端移動,然後直接清楚邊界以外的內存。可以看到,標記的存貨對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉,如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。
          • 標記/清理算法不僅可以彌補/標記清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。
          • 缺點:標記整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。從效率上看,標記/整理算法要低於複製算法。
      4. GC 算法總結
        • 內存效率:複製算法>標記清楚算法>標記整理算法
        • 內存整齊度:複製算法=標記整理算法>標記清楚算法
        • 內存利用率:標記整理算法=標記清楚算法>複製算法
        • 可以看出,效率上來說,複製算法是當之無愧的老大,但是卻浪費了太多內存,而爲了儘量兼顧上面所提到的三個指標,標記/整理算法相對來說更平滑一些,但效率上依然不盡如人意,它比複製算法多了一個標記的階段,又比標記/清除多了一個整理內存的過程
        • 難道就沒有一種最優算法嗎?沒有的,沒有最好的,只有最適合的算法。=======> 分代收集算法。
        • 年輕代(Young Gen)
          1. 年輕代特點是區域相對老年代較小,對像存活率低。這種情況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對像大小有關,因而很適用於年輕代的回收。而複製算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。
        • 老年代(Tenure Gen)
          1. 老年代的特點是區域較大,對像存活率高。這種情況,存在大量存活率高的對像,複製算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。
          2. 標記(Mark)階段的開銷與存活對像的數量成正比,這點上說來,對於老年代,標記清除或者標記整理有一些不符,但可以通過多核/線程利用,對併發、並行的形式提標記效率。
          3. Compact階段的開銷與存活對像的數據成開比,如上一條所描述,對於大量對像的移動是很大開銷的,做爲老年代的第一選擇並不合適。
          4. 基於上面的考慮,老年代一般是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS(Compact-Mark-Sweep)回收器爲例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器做爲補償措施:當內存回收不佳(碎片導致的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。
  3. JMM 內存模型

    • JMM(Java內存模型Java Memory Model)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規劃通過規範定製了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式,JMM關於同步規定:
      1. 線程解鎖前,必須把共享變量的值刷新回主內存。
      2. 線程加鎖前,必須讀取主內存的最新值到自己的工作內存
      3. 加鎖解鎖是同一把鎖。
    • 由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(有些地方成爲棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作空間,然後對變量進行操作,操作完成再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存儲存着主內存中的變量副本拷貝,因此不同的線程無法訪問對方的工作內存,此案成間的通訊(傳值) 必須通過主內存來完成,其簡要訪問過程如下圖:
      image.png
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章