Java面試題系列(五)——JVM及垃圾回收機制

1. JVM(https://blog.csdn.net/qq_41701956/article/details/81664921)

  • JVM的作用:解釋運行字節(jié)碼程序消除平臺相關性。jvm將java字節(jié)碼解釋為具體平臺的具體指令。一般的高級語言如要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入JVM后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用模式Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執(zhí)行字節(jié)碼時,把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行。

  • JVM常見問題:https://mp.weixin.qq.com/s/Xo3_ZTVruhFSTMSrmJLvrw

  • JVM知識結構圖


    image.png
  • JVM內(nèi)存結構圖


    image.png
  • 程序計數(shù)器:內(nèi)存空間小,線程私有。字節(jié)碼解釋器工作是就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行指令的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復等基礎功能都需要依賴計數(shù)器完成。唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。

  • 虛擬機棧:線程私有,生命周期和線程一致。描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行時都會床創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行結束,就對應著一個棧幀從虛擬機棧中入棧到出棧的過程。

  • 本地方法棧:Java 虛擬機棧為虛擬機執(zhí)行 Java 方法(也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。

  • 堆:對于絕大多數(shù)應用來說,這塊區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊。線程共享,主要是存放對象實例和數(shù)組。內(nèi)部會劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)??梢晕挥谖锢砩喜贿B續(xù)的空間,但是邏輯上要連續(xù)。

  • 方法區(qū):屬于共享內(nèi)存區(qū)域,存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。

  • 運行時常量池:屬于方法區(qū)一部分,用于存放編譯期生成的各種字面量和符號引用。編譯器和運行期(String 的 intern() )都可以將常量放入池中。內(nèi)存有限,無法申請時拋出 OutOfMemoryError。

  • 直接內(nèi)存:非虛擬機運行時數(shù)據(jù)區(qū)的部分

2. 虛擬機棧結構

image.png

3. Java內(nèi)存模型(JMM)

??Java內(nèi)存模型(簡稱JMM),是一種規(guī)范。JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。線程對變量的操作只能在各自線程的工作內(nèi)存操作。
JMM的三大特性:

  • 可見性
  • 原子性
  • 有序性
    本地內(nèi)存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。其關系模型圖如下圖所示:


    image.png

4. JVM 調(diào)優(yōu),查看 JVM 參數(shù)默認值

  • jps -v 可以查看 jvm 進程顯示指定的參數(shù)
  • 使用 -XX:+PrintFlagsFinal 可以看到 JVM 所有參數(shù)的值
  • jinfo 可以實時查看和調(diào)整虛擬機各項參數(shù)
  • jstat -gc 12538 5000即會每5秒一次顯示進程號為12538的java進程的GC情況

5. OOM排查方法

1)先查看應用進程號pid:ps -ef | grep 應用名
2)查看pid垃圾回收情況:  jstat  -gc  pid  5000(時間間隔)
3)開啟OOM快照: 
  -XX:+HeapDumpOnOutOfMemoryError(開啟堆快照)
  -XX:HeapDumpPath=C:/m.hprof(保存文件到哪個目錄)
4)dump 查看方法棧信息:jstack  -l  pid  >  /home/test/jstack.txt
5)dump 查看JVM內(nèi)存分配以及使用情況:jmap  -heap  pid  >  /home/test/jmapHeap.txt
6)dump jvm二進制的內(nèi)存詳細使用情況

6. 類加載

  • 類加載器:


    image.png
  • 類加載過程:加載,驗證,準備,解析,初始化


    image.png
  • 加載:加載是類加載過程中的一個階段,這個階段會在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的入口。

  • 驗證:確保Class文件的字節(jié)流中包含的信息是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。

  • 準備:準備階段是正式為類變量分配內(nèi)存并設置類變量的初始值階段,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間。

  • 解析:解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。

  • 初始化:初始化階段是執(zhí)行類構造器<client>方法的過程。到了初始階段,才開始真正執(zhí)行類中定義的Java程序代碼。

  • 雙親委派模型

    • 定義:除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器,這里類加載器之間的父子關系一般通過組合(Composition)關系來實現(xiàn),而不是通過繼承(Inheritance)的關系實現(xiàn)。
    • 工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載,而是把這個請求委派給父類加載器,每一個層次的加載器都是如此,依次遞歸,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成此加載請求(它搜索范圍中沒有找到所需類)時,子加載器才會嘗試自己加載。
    • 優(yōu)點:使用雙親委派模型來組織類加載器之間的關系,使得Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關系。例如類java.lang.Object,它存放再rt.jar中,無論哪個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。
    • 打破雙親委派機制的方法:重寫loadclass()方法
    • 打破雙親委派機制的例子:
      • Tomcat,應用的類加載器優(yōu)先自行加載應用目錄下的 class,并不是先委派給父加載器,加載不了才委派給父加載器。打破的目的是為了完成應用間的類隔離。
      • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,當平臺及應用程序類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統(tǒng)模塊中,如果可以找到這樣的歸屬關系,就要優(yōu)先委派給負責那個模塊的加載器完成加載。打破的原因,是為了添加模塊化的特性。
    • 沙箱安全機制:防止惡意代碼污染java源代碼。

7. JVM加載class文件的原理

??JVM中類的裝載是由ClassLoader和它的子類來實現(xiàn)的,Java ClassLoader 是一個重要的Java運行時系統(tǒng)組件,它負責在運行時查找和裝入類文件的類。
??Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內(nèi)存中。在寫程序的時候,我們幾乎不需要關心類的加載,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。
??類裝載方式,有兩種

  • 隱式裝載,程序在運行過程中當碰到通過new 等方式生成對象時,隱式調(diào)用類裝載器加載對應的類到jvm中
  • 顯式裝載,通過class.forname()等方法,顯式加載需要的類,隱式加載與顯式加載的區(qū)別:兩者本質(zhì)是一樣的。
    ??Java類的加載是動態(tài)的,它并不會一次性將所有類全部加載后再運行,而是保證程序運行的基礎類(像是基類)完全加載到jvm中,至于其他類,則在需要的時候才加載。這當然就是為了節(jié)省內(nèi)存開銷。

8. GC Roots

作為GCRoots的對象包括下面幾種:
1)虛擬機棧(棧幀中的局部變量區(qū),也叫做局部變量表)中引用的對象。
2)方法區(qū)中的類靜態(tài)屬性引用的對象。
3)方法區(qū)中常量引用的對象。
4)本地方法棧中JNI(Native方法)引用的對象。

9. 對象可達性分析

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


    image.png

10. 垃圾回收機制

??堆分為新生代和老年代,新生代默認占總空間的 1/3,老年代默認占 2/3。新生代使用復制算法,有 3 個分區(qū):Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1。
??當新生代中的 Eden 區(qū)內(nèi)存不足時,就會觸發(fā) Minor GC,過程如下:
1)在 Eden 區(qū)執(zhí)行了第一次 GC 之后,存活的對象會被移動到其中一個 Survivor 分區(qū);
2)Eden 區(qū)再次 GC,這時會采用復制算法,將 Eden 和 from 區(qū)一起清理,存活的對象會被復制到 to 區(qū);
3)移動一次,對象年齡加 1,對象年齡大于一定閥值會直接移動到老年代
4)Survivor 區(qū)相同年齡所有對象大小的總和 > (Survivor 區(qū)內(nèi)存大小 * 目標使用率)時,大于或等于該年齡的對象直接進入老年代。其中這個使用率通過 -XX:TargetSurvivorRatio 指定,默認為 50%
5)Survivor 區(qū)內(nèi)存不足會發(fā)生擔保分配
6)年齡超過指定大小的對象可以直接進入老年代
7)Major GC,指的是老年代的垃圾清理,但并未找到明確說明何時在進行Major GC
8)FullGC,整個堆的垃圾收集,觸發(fā)條件:

  • 每次晉升到老年代的對象平均大小>老年代剩余空間
  • MinorGC后存活的對象超過了老年代剩余空間
  • 元空間不足
  • System.gc()
  • CMS GC異常,promotion failed:MinorGC時,survivor空間放不下,對象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC時,同時有對象要放入老年代,而老年代空間不足造成
  • 堆內(nèi)存分配很大的對象

11. 垃圾回收算法

  • 引用計數(shù)法:給對象中添加一個引用計數(shù)器,每當一個地方引用這個對象時,計數(shù)器值+1;當引用失效時,計數(shù)器值-1。任何時刻計數(shù)值為0的對象就是不可能再被使用的。
    • 缺點:(1)每次給對象賦值時都要維護引用計數(shù)器,且計數(shù)器本身也有一定的消耗;(2)較難處理循環(huán)引用
  • 復制算法:(年輕代)
    • 優(yōu)點:(1)不產(chǎn)生內(nèi)存碎片;(2)速度快
    • 缺點:浪費10%的內(nèi)存空間
  • 標記清除算法(老年代):先標記出要回收的對象,然后統(tǒng)一回收這些對象
    • 優(yōu)點:節(jié)省空間
    • 缺點:產(chǎn)生內(nèi)存碎片;
  • 標記整理算法(老年代):先標記清除,再次掃描并往一端滑動存活對象。
    • 優(yōu)點:不產(chǎn)生內(nèi)存碎片;
    • 缺點:需要移動對象的成本,耗時嚴重

12. 垃圾收集器

image.png

1)Serial收集器:新生代收集器,使用停止復制算法,使用一個線程進行GC,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內(nèi)存回收(這也是虛擬機在Client模式下運行的默認值)


image.png

2)Serial Old收集器:老年代收集器,單線程收集器,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存的對象,壓縮是將移動對象,將空間填滿保證內(nèi)存分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行標 記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
3)ParNew收集器:新生代收集器,使用停止復制算法,Serial收集器的多線程版,用多個線程進行GC,其它工作線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內(nèi)存;使用-XX:ParallelGCThreads來設置執(zhí)行內(nèi)存回收的線程數(shù)。


image.png

4)Parallel Scavenge 收集器:新生代收集器,使用停止復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行后臺運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。使用-XX:+UseParallelGC開關控制使用 Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執(zhí)行時間占總時間的比例,默認99,即 1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數(shù)只對Parallel Scavenge有效)
5)Parallel Old收集器:老年代收集器,多線程,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復制到預先準備好的區(qū)域,而不是像Sweep(清 理)那樣清理廢棄的對象)算法,在Parallel Old執(zhí)行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現(xiàn)后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現(xiàn)Parallel Scavenge收集器吞吐量優(yōu)先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。


image.png

6)CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于獲取最短回收停頓時間,使用標記清除算法,多線程,優(yōu)點是并發(fā)收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內(nèi)存回收,優(yōu)先使用ParNew+CMS(原因見后面),當用戶線程內(nèi)存不足時,采用備用方案Serial Old收集。解決內(nèi)存碎片問題:讓CMS在進行一定次數(shù)的Full GC(標記清除)的時候進行一次標記整理算法。


image.png
  • 初始標記:標記GC roots直接關聯(lián)的對象,速度快
  • 并發(fā)標記:GC Roots Tracing過程
  • 重新標記:修正并發(fā)標記期間用戶進程繼續(xù)運行而產(chǎn)生變化的標記,耗時比初始標記長,但遠小于并發(fā)標記
  • 并發(fā)清除:清除標記的對象

7)G1(Garbage First)收集器:區(qū)域化垃圾收集器,物理上不區(qū)分新生代和老年代。采用標記整理的算法,與CMS相比:(1)不會產(chǎn)生內(nèi)存碎片;(2)用戶可以指定垃圾回收時間。G1 收集器,相關配置如下:

-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
  • G1將新生代,老年代的物理空間劃分取消了。
  • G1算法將堆劃分為若干個區(qū)域(Region),它仍然屬于分代收集器。不過,這些區(qū)域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區(qū)域,G1收集器通過將對象從一個區(qū)域復制到另外一個區(qū)域,完成了清理工作。H區(qū)域可以是連續(xù)的用來分配比較大的對象


    image.png

8)ZGC
ZGC給Hotspot Garbage Collectors增加了兩種新技術:著色指針和讀屏障。ZGC的標記分為三個階段。

  • 第一階段是STW,其中GC roots被標記為活對象。
  • 第二階段,同時遍歷對象圖并標記所有可訪問的對象。在此階段期間,讀屏障針使用掩碼測試所有已加載的引用,該掩碼確定它們是否已標記或尚未標記,如果尚未標記引用,則將其添加到隊列以進行標記。
  • 在遍歷完成之后,有一個最終的,時間很短的的Stop The World階段,這個階段處理一些邊緣情況(我們現(xiàn)在將它忽略),該階段完成之后標記階段就完成了。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容