JVM—深刻理解內存模型與垃圾收集機制

前言

Java是一種跨平臺的語言,當初其設計初衷也是爲了解決各個平臺編譯環境具備差別,對程序移植性問題形成困難這一痛點,因而推出了Java語言。這麼多年Java受業界追捧的緣由除了其面向對象的特性之外就是其可移植性強,而可移植性這一特性正式創建在JVM虛擬機這一基礎上的,JVM在其內存模型和垃圾回收機制的設計上堪稱神做,瞭解JVM虛擬機是每個Java開發工程師必備的技能。面試

你瞭解Java的內存模型嗎

  • 內存簡介

    要說清楚內存,首先要提計算機程序是如何運行的。計算機程序指的就是可讓計算機運行的一些指令集合,簡單地說就是咱們平時寫的代碼,而真正在計算機中運行的是進程進程=代碼+數據,而要操做數據,則應該先將數據加載進內存中,才能對其進行進一步的操做。而內存就是一系列地址空間,地址空間又分爲內核空間用戶空間內核空間是計算機操做系統運行時所需的空間,如虛擬內存、聯網、操做系統調度等所需的空間,而Java進程實際運行時使用的空間是咱們的用戶空間算法

  • JVM架構圖

    JVM架構圖

    類裝載器(ClassLoader):依據特定格式,class文件加載到內存。
    執行引擎(Execution Engine):對命令進行解析。
    本地庫接口(Native Interface):融合不一樣開發語言的原生庫爲Java所用。
    內存區域(Runtime Data Area):JVM內存空間結構模型。
    數組

  • 劃分

    從Java的內存區域中能夠看到,其分爲五個區,分別是 1.程序計數器、2.虛擬機棧、3.本地方法棧、4.堆區、5.方法區(元空間)而在這五個區中,分爲線程私有共享區域緩存

    • 線程私有

      1.程序計數器(Program Counter Register):當前線程所執行字節碼(class文件)行號指示器(邏輯),它改變自身的計數器的值來選取下一條須要執行的字節碼指令,爲了程序執行不互相沖突,因此每一個線程必須私有程序計數器,保證程序運行不衝突。注:若是是執行Native方法,則計數器值爲Undefined。 程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變, 因此此區域不會出現 OutOfMemoryError 的狀況。

      2.Java虛擬機棧(Stack):Java虛擬機棧即咱們平時所說的Java內存模型裏的棧內存,其存放的最小單位爲棧幀,Java虛擬機棧中的每一個棧幀主要存儲局部變量表、操做數棧、動態連接、返回地址,當方法調用結束時,該棧幀隨即被銷燬,棧幀內的局部變量也隨即被銷燬。這裏說一下局部變量表操做棧局部變量表包含了方法執行過程當中的全部變量,而操做數棧主要實現入棧、出棧、複製、交換、產生消費變量等。該區域會產生兩種異常,即 當線程請求的棧大小超過棧的總深度,拋出StackOverflowError異常(例如遞歸),當棧進行擴展時沒法獲得足夠的內存,則拋出OutOfMemoryError異常。

      3.本地方法棧(Native Method Stack):與虛擬機棧類似,主要存非Java語言的方法。一樣會拋出StackOverflowError和OutOfMemoryError異常

      bash

    • 全部線程共享

      1.方法區(Method Area):方法區主要存儲Class的相關信息,包括Method和field等等,說這個以前首先說元空間(MetaSpace)永久代(PermGen)的區別,在Java1.7後,將方法區中的字符串常量池移動到Java堆中,而且Java1.7以後將永久代變爲元空間,它們兩個最大的區別就是元空間使用本地內存而永久代使用JVM內存,這一改變最大的變化就是,不會再看到ParmGen出現內存溢出的異常了,並且字符串常量池存在永久代中,容易出現性能問題和內存溢出,類和方法的信息大小難以肯定,給永久代的大小指定帶來困難。
      2.Java堆(Heap):該區域是Java內存模型中最大的一塊,該區域存儲全部對象的實例,即咱們在寫代碼時new出來的對象,都存在堆區,當堆沒法再分配內存時,將會拋出OutOfMemoryError異常。該區域是GC管理的主要區域,所以Java堆又被稱爲GC堆,因爲GC在垃圾回收的時候使用分代收集,因此堆內存也能夠被分爲新生代老年代,老年代佔堆內存的2/3,新生代佔1/3,新生代又能夠細分爲Eden區From區To區,Eden區的Eden伊甸園的意思,聖經記載,亞當和夏娃在伊甸園偷食禁果,因此伊甸區是人類的起源地,名字也就來源於此,咱們在程序中new出的對象(除大對象,大對象直接進入老年代),都存在於Eden區,當屢次GC後沒有被回收,則會進入老年代,這一塊在說垃圾回收機制的時候會細說,這裏只要知道Java堆大概分爲這幾個區域便可。
      網絡

關於Java內存模型的面試題

  • JVM三大性能調優參數-Xms -Xmx -Xss的含義

    答: 1.-Xss:規定了每一個線程虛擬機棧(堆棧)的大小。
    2.-Xms:堆的初始值。
    3.-Xmx:堆能達到的最大值。一般將堆的初始值和最大值設爲相同值,防止堆擴容時產生內存抖動問題。
    多線程

  • Java內存模型中堆和棧的區別——內存分配策略

    靜態存儲:編譯時肯定每一個數據目標在運行時的存儲空間需求。
    棧式存儲:數據區需求在編譯時未知,運行時模塊入口前肯定。
    堆式存儲:編譯時或運行時模塊入口都沒法肯定,動態存儲。
    堆和棧的聯繫,引用對象、數組時,棧裏定義變量保存堆中對象目標的首地址。
    堆和棧的不一樣,棧中的變量在方法運行結束後當即被清除(自動釋放),而堆中的對象即便失去引用變爲不可達對象,也需等待GC纔會被清除,即清除時間時不肯定的(須要GC)。
    棧的空間較堆空間小,且棧產生的碎片遠小於堆。
    棧的效率比堆高。
    架構

    堆和棧

  • JDK6和JDK6以後的版本對intern()方法的區別

    JDK6:當調用intern方法時,若是字符串常量池先前已建立出該字符串對象,則返回池中的該字符串的引用。不然,將此字符串對象添加到字符串常量池中,而且返回該字符串對象的引用。函數

    JDK6+:當調用intern方法時,若是字符串常量池先前已建立出該字符串對象,則返回池中的該字符串的引用,不然,若是該字符串對象已經存在於Java堆中,則將堆中對此對象的引用添加到字符串常量池中,而且返回該引用;若是堆中不存在,則在池中建立該字符串並返回其引用。性能

    注:Java1.8中 已經將字符串常量池已經從方法區移動到堆中

Java垃圾回收機制

  • 對象被斷定爲垃圾的標準

    在垃圾回收機制中,把沒有被其它對象引用的對象斷定爲垃圾,而垃圾回收機制的各類算法也是基於這一標準,主要的中心即放在如何斷定一個對象是否被引用如何被回收

  • 引用計數算法

    引用計數算法中,主要是經過計算一個對象的引用數量來判斷對象是否爲垃圾,是否應該被回收。其實現方式是對存在於堆中的每個對象都置一個引用數量計數器。當建立一個對象時,將該對象實例分配給一個引用對象,則將該對象的引用數量計數器的值加一,完成引用則減一。所以,當該實例對象的引用計數器值爲0時,則能夠將該對象視爲垃圾,在GC調用時,則將會回收該對象的空間。
    引用計數算法的優劣:引用計數算法其優勢是執行效率高,程序執行受影響較小,由於其運行時只需將引用數量計數器的值加一或減一,運算量極小,效率極高,能夠交織在程序運行中。其缺點也是十分明顯的,引用計數算法有一個致命的缺陷,就是它沒法處理循環引用的狀況,所謂循環引用就是當A引用B,B又引用A,兩個對象互相引用,實際上這兩個對象是能夠被回收的,但因爲其引用計數器的值均爲1,因此形成了此種算法斷定這兩個對象爲不可回收,致使內存泄漏。因此Java中的GC並不會採用此種算法。

    循環引用

  • 可達性分析算法

    可達性分析算法是經過判斷對象的引用鏈是否可達,來決定對象是否能夠被回收,該算法從離散數學中的圖論引入,程序之間的引用關係能夠看做是一個十分複雜的圖,經過一系列的名爲GC Root的節點做爲起始點,向下搜索,搜索中走過的路徑就被稱爲引用鏈(Reference Chain),當一個對象從GC Root沒有任何的引用鏈,則證實該對象是不可達的,該對象就會被標記爲垃圾。

    例如圖中Object五、Object六、Object7均爲不可達,因此這三個對象將會在下一次GC中被清除。

    可達性分析算法

    可做爲GC Root的對象: 1.虛擬機棧中引用的對象(棧幀中的局部變量表)
    2.方法區中常量引用的對象
    3.方法區中類靜態屬性引用的對象
    4.本地方法棧中Native方法中的引用對象
    5.活躍線程的引用對象
    簡單來講:就是全部被引用的對象(包括靜態對象和非靜態對象)+線程+Native方法中的對象,均可以做爲GC Root的對象。

垃圾回收算法

這裏可能有人會蒙,剛纔不是談了垃圾回收算法了嗎,怎麼又開始說垃圾回收算法了,其實從這裏開始,纔是真正的垃圾回收算法,上面的兩個算法能夠算是垃圾回收前的準備工做,即對要回收的對象進行標記判斷。這個對象是否應該被回收,是上面那兩個算法的工做,而這個對象應該怎麼被回收,回收後要對內存作哪些工做,這就是垃圾回收算法所要考慮的事情。

  • 標記-清除算法(Mark and Sweep)

    標記-清除算法將算法分爲兩個步驟,即標記清除,所謂標記,就是從根節點進行掃描,對存活的對象進行標記。所謂清除,就是對堆內存中從頭至尾進行線性遍歷,回收未被標記的對象內存,即不可達對象內存,最後將原來作過標記對象的標記清空,爲下一次GC作準備

    標記-清除算法的優缺點:標記-清除算法的優點是其效率高,僅需掃描一遍內存便可將全部的垃圾進行回收。可是其缺陷也是十分的明顯,在標記-清除算法中,只要某對象被標記爲垃圾,則調用GC時就會直接進行回收,這勢必會帶來一個問題,就是內存的碎片化。所謂內存碎片化,即在GC過程當中,因爲垃圾所處的內存空間並不連續,致使回收事後會存在不少的不連續的內存空間。
    舉個例子,有兩個對象A and B,A佔用1B內存,B佔用1B內存,他們兩個所處的位置並不連續,而當它們被同時標記爲垃圾並被回收了以後,就會產生兩塊1B的內存,此時來了一個2B的對象,可是它就沒法使用這兩塊不連續的1B存儲空間了。若是此時內存已滿,將會拋出OutOfMemoryException,這就是內存碎片所形成的後果。

    碎片化

  • 複製算法(Copying)

    複製算法,將內存分爲對象面空閒面,對象只存在於對象面上。當複製算法運行時,首先會像標誌-清除算法同樣,對存在引用的對象作標記,而後將帶有標記的對象複製到空閒面上,而且按照內存順序存儲,當所有帶標記的對象都被移動到空閒面上後,將對象面的全部對象一併清除,而後將空閒面和對象面進行互相轉換,即此時對象面變爲空閒面,空閒面變爲對象面。因爲複製操做也存在效率問題,因此這種算法適用於對象存活率低的場景,由於這樣就不會有不少的對象須要複製。實際上這種算法是應用在堆內存中的新生代中的,由於在大量的實踐中證實,在新生代區的對象,最後存活下來的比例大概只有10%,因此至關適合這種算法。至於在新生代中這種算法的運行步驟是怎樣的,放在下文中說。
    因爲複製算法對複製後的對象按照內存順序存儲,因此它解決了標記-清除算法中內存碎片化的問題。

    複製算法

  • 標記-整理算法(Compacting)

    標記-整理算法採用和標記-清除算法同樣的步驟,從根集合進行掃描,對存活的對象進行標記,但在清除時,這個算法會移動全部存活的對象,且按照內存地址次序依次進行排列,而後將末端內存地址之後的內存所有進行回收。因爲此種算法在標記-清除的基礎上,加之對對象進行整理,因此其效率更低,但解決了內存碎片化的問題。
    該算法因爲一次GC會有較高的資源消耗,因此該算法適用於存活率高的場景,例如堆內存中的老年代

    標記整理算法

  • 分代收集算法(Generational Collector)

    有了上述三種的垃圾回收算法,有些同窗可能心存疑慮,到底JVM中使用的是哪種算法來對垃圾進行回收呢?其實JVM使用了上述幾種算法的組合拳,即分代收集算法。從嚴格意義上來講,分代收集算法並非一種新的算法,它只是將上述幾種算法進行了一個整合。按照對象生命週期的不一樣劃分區域,採用不一樣的垃圾回收算法。

    這裏先說一下JVM內存模型對象生命週期之間的關係,在咱們new一個普通對象時,這個對象會在Eden區被建立,假如在一次GC事後,這個對象沒有被清除,則稱這個對象是倖存者,將其年齡屬性加一,然後將會移動到From 區或To區,這兩個區域也被統稱爲Servivor區,(Eden區:From區:To區=8:1:1),當一個對象經歷了15次GC後都沒有被回收,則會直接被移動到堆區中的老年代,老年代中的對象被認爲是回收可能性不大的對象,由於經歷了15次GC都沒有被回收的對象,經歷150次GC被回收的可能性也不大。

    因此瞭解了這個原理以後,再說分代收集算法就將會變得簡單。上文提到,複製算法因爲其複製對象到空閒區須要消耗資源,因此適合對象存活率不高的場景,而新生代就很好地知足了這個條件,因此新新生代一般使用複製算法進行垃圾回收。在屢次的實踐中證實,一批被新建的對象,最終存活率大概在10%左右,因此這一批對象將會被複制到Servivor區,而複製完成後當即回收Eden區。而新生代中的From區和To區又和複製算法中的空閒區和對象區相對應。這就對複製算法的施行製造了很好的環境。

    老年代因爲其存儲的對象具備不易被GC這個特色,因此上文中提到的標記-整理算法將會變得十分合適,標記-整理算法因爲須要在清除後對存活的對象進行一次整理以消除內存碎片化,因此若是有大量的內存碎片,將很是不利於這種算法的運行,而老年代則給了適合這種算法的土壤。

    在分代收集算法中還有兩個重要的概念是不曾提到的Minor GCFull GC,存在於新生代的GC因爲其垃圾回收範圍較小,被稱爲MinorGC,而在老年代的GC中一般伴隨着全部內存的GC,因此其又被稱爲Full GC。Full GC效率低,可是不常被觸發。

    觸發Full GC的條件

    1.老年代空間不足
    2.永久代空間不足(已移除)
    3.CMS GC時出現Promotion failed,concurrent mode failure
    4.Minor GC晉升到老年代的平均大小大於老年代的剩餘空間
    5.調用System.gc()
    6.使用RMI進行RPC或管理的JDK應用,每小時執行一次Full GC

    經常使用的調優參數
    1.-XX:SurvivalRatio:Eden和Servivor的比值,默認8:1
    2.-XX:NewRatio:老年代和年輕代的內存大小的比例
    3.-XX:MaxTenuriingThreshold:對象從年輕代晉升到老年代通過GC的最大閾值

    堆內存

經常使用的垃圾收集器

在說垃圾收集器以前,先得明白兩個概念

  • Stop-the-World

    什麼是Stop-the-World?JVM因爲要執行GC,而中止應用程序的執行,這就是Stop-the-World,這種現象在任何一種GC算法中都會發生,因此如何讓Stop-the-World發生的次數愈來愈少,以優化GC性能,是大多數垃圾收集器優化GC的策略。

  • Safe Point

    這個詞相對來講很好理解,在GC過程當中,會有程序不斷地產生垃圾對象,這會形成一邊打掃一邊扔的效果,因此GC是以快照方式進行垃圾回收的,在程序運行到特定位置時,例如跳轉,會生成一個Safe Point,而GC將會根據這個Safe Point中的垃圾進行回收。

  • 垃圾收集器

經常使用的垃圾收集器
上圖中上半部分爲新生代垃圾收集器,下半部分爲老年代垃圾收集器。
兩個垃圾收集器之間若是有連線,表明能夠配合使用。

新生代垃圾收集器

Serial收集器是目前JVM運行在Client模式下的默認收集器,使用複製算法。由於它是單線程收集的,進行垃圾收集時必須暫停全部工做線程。
ParNew收集器 是多線程垃圾收集器,除了多線程這個特色,其他的行爲、特色和Serial收集器同樣。它是Server模式下JVM默認的垃圾收集器。

老年代垃圾收集器

Serial Old收集器 使用標記-整理算法,單線程收集,進行垃圾收集時,必須暫停全部工做線程。簡單高效,是Client模式下默認的老年代垃圾收集器。
CMS收集器 使用標記-整理算法,多線程收集,GC線程幾乎能夠和工做線程同時工做。

GC相關面試題

  • Object的finalize()方法做用是否與C++的析構函數做用相同
    答:Object的finalize()方法不能保證在調用時當即回收目標對象,而是要等一次GC才能開始回收,所以它是不肯定的。而C++中的析構函數是肯定的。

  • Java中的強引用、軟引用、弱引用、虛引用有什麼用。
    強引用:指該對象存在至少一個引用對象引用的狀況,這時GC毫不會回收該對象,當內存不足時,即便報OutOfMemoryException也不會回收該對象。
    軟引用:對象處於有用但非必須的狀態,只有當內存不足時,GC纔會回收該引用的內存。可用來實現內存敏感的高速緩存,由於在內存不足就被回收這一特性,咱們不用太擔憂OutOfMenoryException這一異常 用法:

    String str = new String("abc");//強引用
    SoftReference<String> softRef = new SoftReference<String>(str);//軟引用
    複製代碼

    弱引用:非必須的對象,比軟引用更弱一些,GC時會被回收。被回收的機率也不大,由於GC線程優先級比較低,適用於偶爾被使用且不影響垃圾收集的對象。
    用法:

    String str = new String("abc");//強引用
    WeakReference<String> weakRef = new WeakReferences<String>(str);//弱引用
    複製代碼

    虛引用:不會決定對象的生命週期,在任什麼時候候均可能被垃圾收集器回收,它能夠跟蹤對象被垃圾收集器回收的活動。必須與ReferenceQueue聯用。
    用法:

    String str = new String("abc");
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference ref = new PhantomReference(str,queue);
    複製代碼

    綜上,強引用>軟引用>弱引用>虛引用

結語

在寫這篇以前,我看過一篇文章,名字記不太清了,大體是,《面試官:求求大家了,再問大家Java內存模型不要再和我說堆區和棧區了》,當時我瞭解的JVM也僅限於此,看完那篇文後就有了去了解JVM的想法,只有本身實際瞭解過以後,才意識到本身是「學而後知不足,教而後知困」。也許在之後再回過頭來看這篇文章,我依然會有這種感受,但我但願能夠有那個時候。

本文圖片來自網絡,侵刪。

歡迎你們訪問個人我的博客:Object's Blog

相關文章
相關標籤/搜索