JVM從零開始(二) -垃圾回收機制以及內存分代模型

JVM中垃圾回收的斷定標準

最終目的是將內存中無用的對象回收掉。具體的斷定方法有:算法

  • 引用計數法,不採用,指的是維護對象被引用的次數,次數爲0則意味着是垃圾。
  • 可達性算法-GC Roots tracing,指的是從GC Roots開始往下遍歷全部引用的對象,(每一個GC Root就是一個樹狀圖),全部被引用到的對象就是須要存活的對象,其餘對象能夠被回收。GC Root指的是,虛擬機棧(棧幀中的本地變量表)中引用的對象,方法區中非基本類型的類靜態變量(一個地址)所引用的對象,本地方法棧中JNI(即通常說的Native方法)引用的對象。

JVM中內存相關參數

  • -Xms Java堆內存初始大小
  • -Xmx Java堆內存最大大小
  • -Xmn Java堆內存中的新生代大小,扣除它就是老年代大小
  • -XX:PermSize(1.8以後:-XX:MetaspaceSize) 永久代初始大小
  • -XX:MaxPerSize(1.8以後:-XX:MaxMetaspaceSize) 永久代最大大小
  • -Xss 每一個線程的棧內存大小

注:一般狀況下,Xms和Xmx,-XX:PermSize和-XX:MaxPerSize都會設置爲同樣。sql

  • -XX:MaxTenuringThreshold 多少歲進入老年代-默認15
  • -XX:PretenureSizeThreshold 超過多少字節的大對象直接進入老年代
  • -XX:HandlePromotionFailure MinorGC時,若是老年代剩餘空間小於新生代對象總大小,可是若是大於以前平均進入老年代對象的大小,是否嘗試進行MinorGC(默認開啓)
  • -XX:SurvivorRatio=8 Eden區的比例

上面看不懂的參數不要深究,等下提到回過頭再來看,這裏只是將全部參數羅列出來方便查找。優化

JVM中的內存分代模型

JVM中,將對象在內存中分爲了三代:spa

  • 年輕代:很快被回收的對象,存在於堆,具體還在內存中分爲了1個eden區,和2個survivor區。
  • 老年代:長期存在的對象,存在於堆
  • 永久代:指的就是方法區(存放Class元數據),回收條件較苛刻,需知足:該類全部實例對象全部已經從堆內存被回收,該類classLoader已經被回收,該類Class對象沒有任何引用

爲何要分代勒,由於針對每一個年齡代,都有不一樣的垃圾回收算法,以及內存分配機制。若是將全部對象放在一塊兒,第一是會形成頻繁遍歷判斷回收的開銷,第二是會形成複製、移動的開銷,爲何會有複製、移動,由於回收內存必然會形成內存碎片,而內存碎片會致使空間浪費,因此必須經過複製、移動來清理隨便,使得空閒內存連續。線程

JVM中具體的內存分配模型

如上圖,至於年輕代爲何要如此分配,與特定的回收算法有關。3d

對象在內存分代中如何流轉

年輕代

大部分對象剛建立的時候都會分配在年輕代的Eden區,只要年輕代空間不夠就會觸發MinorGC(只回收年輕代內存),minorGC採起復制算法進行回收,當JVM運行觸發第一輪minorGC時,會將eden區存活的對象先複製到一個suprivor區。而後刪除eden區對象,當觸發下一輪minorGC時,又把suprivor區和eden區的存活的對象轉移到另外一個suprivor區,而後刪除這兩個區的全部對象。依次類推。至於爲何要用複製算法,包括老年代的標記整理算法,這是考慮到了避免內存碎片。若是對象內存不連續,會形成不少的空間浪費。中間件

老年代

老年代的對象都是從年輕代根據必定的規則流轉過來的。 具體有幾類流轉方式:對象

  • 超過指定年齡(參數-XX:MaxTenuringThreshold 配置,默認15),這裏年齡指的是沒有被垃圾回收,存活下來一次理解爲增長一歲。流轉到老年代。blog

  • 大對象直接進入,超過參數指定字節數(-XX:PretenureSizeThreshold)設置的字節數的大對象會直接進入老年代,這是由於對象越大,複製開銷就越大。內存

  • 動態年齡判斷規則進入,意思是不必定要到指定年齡再流轉到15,若是某一年齡以上的對象到達必定大小,也會提早進入老年代。當躲過一輪GC的對象加起來超過surrvivor區50%,如年齡1+年齡2+年齡n一直累加,直到年齡n的時候發現加起來超過了surrvivor空間的50%,則年齡n以上的對象直接進入老年代

  • minorGC發生時,suprivor區放不下,則全部存活對象轉移到老年代。這裏涉及一個老年代分配擔保規則,指的是每次MinorGC發生時,都會判斷老年代可用內存大不大於,年輕代存活對象內存之和,若是大於則直接進行minorGC,若是小於則要看參數XX:HandlePromotionFailure是否啓用(默認啓用),若是啓用則對老年代此次須要承載的轉移對象內存進行預估(取前面minorGC被轉移的平均內存大小),若大於則也進行MinorGC,若意料情況外轉移內存超出了老年代可用空間,則進行FullGC,若fullGC仍是不夠,則拋出OOM錯誤。FullGC是採起的標記整理算法,指的是移動存活對象,讓內存連續,而後刪除須要回收的對象,爲何使用標記整理?由於認爲老年代對象存活概率高,複製算法不划算。

永久代

永久代存放的是元數據信息,當類加載時,類元數據信息寫入永久代,fullGC時永久代數據被回收,回收條件是:該類全部實例對象全部已經從堆內存被回收,該類classLoader已經被回收,該類Class對象沒有任何引用。

附圖:

談一個JVM優化實例

現一個日處理量上億數據的計算系統,不斷從Mysql和其餘數據中間件中提取數據進行計算處理。每分鐘執行500次數據提取和計算任務,每次任務處理耗時10秒,每次處理1萬條數據(每條數據20個字段),可是集羣部署,共5臺機器,1臺機器每分鐘處理100次任務,每臺機器是4核8G內配置,JVM分了4G,3G堆內存,1.5G年輕代,1.5G老年代。

咱們先來估算一下內存佔用:

  • 每條數據20個字段,能夠估算一條數據爲1KB大小左右
  • 每次計算1W條,那麼一個任務佔用內存就是1KB*1W=10MB數據左右,一臺機器每分鐘處理100次任務,暫用內存約爲1G左右,基本上一分鐘多點後Eden區就被佔滿了。

實際生產環境是怎麼樣的勒?

  • 一分鐘以後的第一次GC,此時的內存狀況:

每一個任務處理10S,意味着還有大概六分之一的數據應該存活,算200M,可是200M放不進Survivor區,因此會嘗試往老年代放,老年代如今大於1.2G因此直接放就是了。

  • 每次MinorGC都會有大概200M進入老年代,當進行到第三次時

此時老年代可用容量小於年輕代對象總內存,默認判斷老年代剩餘空間是否大於平均每次MinorGC轉移過來的老年代對象容量,這裏是大於,因此仍是繼續MInorGC。

  • 當進行到第八次時

此時會觸發FullGC清理老年代,因而老年代的對象被所有清理掉了:

  • 而後繼續回到原來的第一次,每8次進行一次FullGC,也就是8分鐘進行一次

優化策略

  • 從新調整新生代老年代比例,擴大新生代內存爲2GB,老年代1GB

此時一個Survivor區有200MB,每次MinorGC後都能存放的下存活對象,不用往老年代轉移(固然仍是有轉移,這只是避免了suprivor區太小被迫轉移的對象)。JVM優化的策略最核心的就是減小FullGC次數,由於掃描對象多了一個老年代和永久代、永久代標記算法略微複雜、老年代整理時因爲對象較多比較慢的緣由,FullGC效率是遠遠低於MinorGC的,通常時間是minorGC的10倍以上。

相關文章
相關標籤/搜索