JVM 內存分配和垃圾回收(GC)機制

一  判斷對象是否存活

垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還「活着」,哪些已經"死去」,即不能再被任何途徑使用的對象。php

1.1 引用計數法 (Reference Counting

給對象加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效的時候,計數器值減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。html

引用計數法的實現簡單,判斷效率也很高,可是主流的java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間的循環引用的問題。java

舉個例子:算法

對象objA和對象objB都有字段instance,賦值令objA.instance = objB 以及objB.instance = objA,除此以外,這兩個對象再無任何引用,可是他們互相引用着對方,致使它們的引用計數都不爲0,因而引用計數算法沒法通知GC收集器回收他們。緩存

 1.2 可達性分析算法(根搜索算法 GC Roots Tracing

在主流的商用程序語言中的主流實現中,都是經過可達性分析來斷定對象是否存活的。jvm

這個算法的基本思想是經過一系列稱爲「GC Roots」的對象做爲起始點,若是從GC roots到這個對象不可達,即一個對象到GC Roots 沒有任何引用鏈相連,則證實此對象是不可用的。post

在java語言中,可做爲GC Roots 的對象包括如下幾種:性能

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 本地方法棧中JNI(Java Native Interface 通常說的native方法)引用的對象
  • 方法區中常量引用的對象
  • 方法區中類靜態屬性引用的對象

 1.3 引用

java中將引用分爲強引用、軟引用、弱引用、虛引用4種,這4種引用強度依次逐漸減弱。學習

強引用:ui

  強引用是指程序代碼之中廣泛存在的,相似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用:

  軟引用是用來描述一些還有用但非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。

弱引用:

  被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。

虛引用:

  爲一個對象設置虛引用的目的是能在這個對象被垃圾收集機制回收時收到一個系統通知。

 

二 垃圾收集算法

2.1 標記 - 清除算法(Mark-Sweep

  先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。
  缺點:1 回收了被標記的對象後,因爲未通過整理,因此致使不少內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集機制。
                  2 效率問題,標記和清除兩個過程效率都不高 。

      圖解:綠色是被標記爲可回收的,當回收後,未使用的內存空間很是零碎,產生內存碎片

2.2 複製算法(Copying

  將可用的內存按容量劃分爲大小相等的兩塊(from,to),每次只是用其中一塊(總有一塊是空的【to區域】)。當這塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後把已使用過的內存空間一次清理完。  
  通常不須要按照1:1的比例來劃份內存空間,而是將一塊內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間(From Survivor 和 To Survivor),每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔使用過的Survivor空間。
  HotSpot虛擬機默認Eden和Survivor大小的比例是8:1,也就是每次新生代中可用的內存空間爲整個新生代容量的90%,只有10%的內存時被浪費的。
  缺點:浪費內存空間,若是對象存活率較高時要執行較多的複製操做,效率下降。
       優勢:不產生內存碎片
 
    圖解:有一塊內存區域是空的,通常是to區域。保留區域每次回收後都由於複製的時候讓他們變爲連續的地址空間,全部不產生內存碎片。
 

2.3 標記-整理算法(Mark-Compact

  複製收集算法在對象存活率較高的時候就要進行較多的複製操做,效率將會變低。
  標記-整理算法的「標記」過程和標記-清除算法一致,只是後面並非直接對可回收對象進行整理,而是讓全部存活的對象都向一段移動,而後直接清理掉端邊界意外的內存。
 
    圖解:因爲標記後繼續整理,能夠很明顯的看出未使用的地址空間都是連續的,不會產生內存碎片。
 

2.4 分代收集算法(Generational Collection

  當前商業虛擬機的垃圾收集都採用「分代收集」算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把java堆分爲新生代和老年代。
  在新生代,每次垃圾收集時都會發現有大批對象死去,只有少許存活,那就使用  複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
  而老年代中由於對象存活率較高,沒有額外空間進行分配擔保,就必須使用 標記 - 清除  或者 標記 - 整理 算法來進行回收。
 

補充:分代劃份內存介紹

    整個JVM內存總共劃分爲三代:年輕代(Young Generation)、年老代(Old Generation)、(JDK 1.7 沒了)持久代(Permanent Generation)

    一、年輕代:全部新生成的對象首先都放在年輕代內存中。年輕代的目標就是儘量快速的手機掉那些生命週期短的對象。年輕代內存分爲一塊較大的Eden空間和兩塊較小的Survior空間,每次使用Eden和其中的一塊Survior.當回收時,將Eden和Survior中還存活的對象一次性拷貝到另一塊Survior空間上,最後清理Eden和剛纔用過的Survior空間。

    二、年老代:在年輕代經歷了N次GC後,仍然存活的對象,就會被放在老年代中。所以能夠認爲老年代存放的都是一些生命週期較長的對象。

    三、持久代:基本固定不變,用於存放靜態文件,例如Java類和方法。持久代對GC沒有顯著的影響。持久代能夠經過-XX:MaxPermSize=<N>進行設置。

 

三  內存分配和回收策略

3.1 Jvm怎麼判斷對象能夠回收了?

  對象沒有引用

  做用域發生未捕獲異常

  程序在做用域正常執行完畢

   程序執行了System.exit()

  程序發生意外終止(被殺線程等)

在Java程序中不能顯式的分配和註銷緩存,由於這些事情JVM都幫咱們作了,那就是GC。

有些時候咱們能夠將相關的對象設置成null 來試圖顯示的清除緩存,可是並非設置爲null 就會必定被標記爲可回收,有可能會發生逃逸。

將對象設置成null 至少沒有什麼壞處,可是使用System.gc() 便不可取了,使用System.gc() 時候並非立刻執行GC操做,而是會等待一段時間,甚至不執行,並且System.gc() 若是被執行,會觸發Full GC ,這很是影響性能。

3.2 JVM GC何時執行?

eden區空間不夠存放新對象的時候,執行Minor GC。升到老年代的對象大於老年代剩餘空間的時候執行Full GC,或者小於的時候被HandlePromotionFailure 參數強制Full GC 。調優主要是減小 Full GC 的觸發次數,能夠經過 NewRatio 控制新生代轉老年代的比例,經過MaxTenuringThreshold 設置對象進入老年代的年齡閥值(後面會介紹到)。

3.3 JVM分別對新生代和老年代採用不一樣的垃圾回收機制

在 Java 中,堆被劃分紅兩個不一樣的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young )又被劃分爲

三個區域:Eden、From Survivor、To Survivor。

   這樣劃分的目的是爲了使 JVM 可以更好的管理堆內存中的對象,包括內存的分配以及回收。

   堆的內存模型大體爲:

     

    從圖中能夠看出: 堆大小 =新生代 + 老年代。其中,堆的大小能夠經過參數 –Xms、-Xmx 來指定。

 

新生代(Young generation):絕大多數最新被建立的對象都會被分配到這裏,因爲大部分在建立後很快變得不可達,不少對象被建立在新生代,而後「消失」。對象從這個區域「消失」的過程咱們稱之爲:Minor GC 。

老年代(Old generation):對象沒有變得不可達,而且重新生代週期中存活了下來,會被拷貝到這裏。其區域分配的空間要比新生代多。也正因爲其相對大的空間,發生在老年代的GC次數要比新生代少得多。對象從老年代中消失的過程,稱之爲:Major GC 或者 Full GC。

(JDK7後 沒了)持久代(Permanent generation)也稱之爲 方法區(Method area):用於保存類常量以及字符串常量。注意,這個區域不是用於存儲那些從老年代存活下來的對象,這個區域也可能發生GC。發生在這個區域的GC事件也被算爲 Major GC 。只不過在這個區域發生GC的條件很是嚴苛,必須符合如下三種條件纔會被回收:

一、全部實例被回收

二、加載該類的ClassLoader 被回收

三、Class 對象沒法經過任何途徑訪問(包括反射)

可能咱們會有疑問:

若是老年代的對象須要引用新生代的對象,會發生什麼呢?

爲了解決這個問題,老年代中存在一個 card table ,它是一個512byte大小的塊。全部老年代的對象指向新生代對象的引用都會被記錄在這個表中。當針對新生代執行GC的時候,只須要查詢 card table 來決定是否能夠被回收,而不用查詢整個老年代。這個 card table 由一個write barrier 來管理。write barrier給GC帶來了很大的性能提高,雖然由此可能帶來一些開銷,但徹底是值得的。

 

新生代空間的構成與邏輯

爲了更好的理解GC,咱們來學習新生代的構成,它用來保存那些第一次被建立的對象,它被分紅三個空間:

· 一個伊甸園空間(Eden)

· 兩個倖存者空間(Fron Survivor、To Survivor)

默認新生代空間的分配:Eden : Fron : To = 8 : 1 : 1

 

老年代空間的構成與邏輯

老年代空間的構成其實很簡單,它不像新生代空間那樣劃分爲幾個區域,它只有一個區域,裏面存儲的對象並不像新生代空間絕大部分都是朝聞道,夕死矣。這裏的對象幾乎都是從Survivor 空間中熬過來的,它們毫不會輕易的狗帶。所以,Full GC(Major GC)發生的次數不會有Minor GC 那麼頻繁,而且作一次Major GC 的時間比Minor GC 要更長(約10倍)。

 

Java 中的堆也是 GC收集垃圾的主要區域。GC 分爲兩種:Minor GC、FullGC ( 或稱爲 Major GC )。

Minor GC 是發生在新生代中的垃圾收集動做,所採用的是複製算法。新生代幾乎是全部 Java 對象出生的地方,即 Java 對象申請的內存以及存放都是在這個地方。Java 中的大部分對象一般不需長久存活,具備朝生夕滅的性質。當一個對象被斷定爲 "死亡" 的時候,GC 就有責任來回收掉這部分對象的內存空間。新生代是 GC 收集垃圾的頻繁區域。當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在通過一次 Minor GC後,如果對象還存活,而且可以被另一塊 Survivor 區域所容納(上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另一塊 Survivor 區域 ( 即 to 區域 ) 中,而後清理所使用過的 Eden以及 Survivor 區域 ( 即from 區域 ),而且將這些對象的年齡設置爲1,之後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,能夠經過參數 -XX:MaxTenuringThreshold 來設定),這些對象就會成爲老年代。但這也不是必定的,對於一些較大的對象 (即須要分配一塊較大的連續內存空間 ) 則是直接進入到老年代

 

Full GC 是發生在老年代的垃圾收集動做,所採用的是標記-清除算法。現實的生活中,老年代的人一般會比新生代的人"早死"。堆內存中的老年代(Old)不一樣於這個,老年代裏面的對象幾乎個個都是在 Survivor 區域中熬過來的,它們是不會那麼容易就 "死掉" 了的。所以,Full GC發生的次數不會有 Minor GC 那麼頻繁,而且作一次 Full GC 要比進行一次 Minor GC 的時間更長。

 

另外,標記-清除算法收集垃圾的時候會產生許多的內存碎片 (即不連續的內存空間 ),此後須要爲較大的對象分配內存空間時,若沒法找到足夠的連續的內存空間,就會提早觸發一次 GC 的收集動做。

 

 

3.4 JVM參數選項

    下面只列舉其中的幾個經常使用和容易掌握的配置選項

-Xms

初始堆大小。如:-Xms256m

-Xmx

最大堆大小。如:-Xmx512m

-Xmn

新生代大小。一般爲 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間爲 = Eden + 1 個 Survivor,即 90% 

-Xss

JDK1.5+ 每一個線程堆棧大小爲 1M,通常來講若是棧不是很深的話, 1M 是絕對夠用了的。

-XX:NewRatio

新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3

-XX:SurvivorRatio

新生代中 Eden 與 Survivor 的比值。默認值爲 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10 

-XX:PermSize

永久代(方法區)的初始大小

-XX:MaxPermSize

永久代(方法區)的最大值

-XX:+PrintGCDetails

打印 GC 信息

 

 

 

 

 

3.5 jvm調優問題--full gc太過頻繁該如何處理?

1 full gc頻繁說明old區很快滿了。

2 若是是一次full gc後,剩餘對象很少。那麼說明你eden區設置過小,致使短生命週期的對象進入了old區。

3 若是一次full gc後,old區回收率不大,那麼說明old區過小。

 

 

參考

《深刻理解java虛擬機》

http://hllvm.group.iteye.com/group/topic/38223#post-248757

http://www.iteye.com/topic/1119491

http://www.importnew.com/1993.html

相關文章
相關標籤/搜索