jvm-堆詳解

1.堆概述

在這裏插入圖片描述
方法區和堆是線程共享的,是每個進程唯一的,一個java程序對應一個進程,一個進程對應jvm實例,一個jvm實例擁有一個單例的運行時數據區,堆是java內存管理的核心區域

  • 堆的大小可以被調節,通過-Xms和-Xmx參數調節
  • 堆在jvm啓動時即創建,空間大小也就隨之確定
  • 所有線程共享堆,可以設置線程私有緩衝區TLAB(文章稍後介紹)
  • 堆在物理上不連續,在邏輯上連續,方法區是堆的邏輯上的一部分

2.堆內存細分

在這裏插入圖片描述
java堆是垃圾收集器管理的內存區域,故也叫GC堆,從內存回收的角度看,現代垃圾收集器大部分是基於分代收集理論設計的,所以java堆中會經常出現如圖所示的內存劃分(新生代包括伊甸區和倖存區),根據不同jdk版本(以jdk8劃分),堆內存劃分也不同:
在這裏插入圖片描述

2.1設置堆內存大小

  • -Xms:堆起始內存,相當於-XX:InitialHeapSpace
  • -Xmx:堆最大內存,相當於-XX:MaxHeapSpace

一般情況下將這兩個參數的值設爲相同,當堆區的內存超出設置的最大內存時,將會拋出OutOfMemoryError異常,jvm在計算堆內存的方式與我們不同,比如我們計算堆內存是新生代+老年代,而jvm則是不同,如圖所示,堆內存爲600m,jvm計算出來只有575m在這裏插入圖片描述

2.2新生代和老年代

在上面我們提到了,新生代分爲Eden,Survivor(S0,S1/From Survivor,To Survivor)區,爲什麼這樣劃分呢?原因是java中的對象生命週期導致:

  • 對於生命週期較短的對象,創建和消亡都非常迅速
  • 某些對象生命週期很長,甚至可以與jvm生命週期一致

新生代與老年代在堆中內存佔比: -XX:NewRation=2,表示新生代佔1,老年代佔2
關於新生代中Eden和Survivor區的內存劃分::-XX:Survivor=8,默認情況下Eden和sos1的劃分是8:1:1
以上兩個參數一般不需要調整,這裏需要說明的是:

  • 幾乎所有的對象都是在Eden區被new出來的,且絕大部分的對象的銷燬都在新生代中進行
  • 在養老區,當內存不足時,會觸發Major GC,對老年代的對象進行垃圾回收,如果還是內存不足,則會產生OOM異常

3.對象分配過程

  • 1.對象優先在Eden中分配
  • 2.當Eden空間填滿時,程序還要繼續創建對象,這是會觸發YGC/Minor GC對新生代(Eden和Survivor)進行垃圾回收,將Eden中沒有引用的對象銷燬,再創建新的對象到Eden中
  • 3.在Minor GC回收後的Eden中,沒有被銷燬的對象移動到Survivor0
  • 4.若Eden區滿了再次觸發垃圾回收,並且垃圾回收過後,此前在Survivor0中仍然存在的對象會被移到Survivor1中,經歷多次移動,達到jvm規定的閾值(15次),就會被移動到養老區了,至於閾值,可以通過:-XX:MaxTenuringThreshold=< N >進行設置

在這裏插入圖片描述
注意:Eden滿了會觸發Minor GC,而Survivor區滿了不會觸發,且Minor GC會對新生代進行垃圾回收,且垃圾回收在新生代頻繁進行,很少在養老區蒐集,幾乎不再方法區蒐集

3.1對象分配特殊情況

在創建對象的時候,會遇到一些特殊情況,比如在創建一個非常大的對象的時候,因爲是在Eden中創建,當對象的大小大於Eden區的大小,會觸發垃圾回收,在回收後仍然無法存放對象,一般會去老年區嘗試存放,老年區比Eden內存空間大,如果老年區也無法存放就會拋出OOM異常,下面這張圖清晰的展示了遇到特殊情況時JVM的處理過程
在這裏插入圖片描述

4.幾種垃圾收集比較

在jvm進行垃圾回收時,會根據內存區域的不同進行垃圾回收,以HotSpot VM爲例,它的GC按照回收區域劃分爲部分收集(Partial gc)和整堆收集(Full GC)

  • 部分收集
    • 新生代收集:Minor GC/Young GC,新生代的垃圾收集
    • 老年代收集:Major GC/Old GC,老年代的垃圾收集,只有CMS GC會有單獨收集老年代的行爲
    • 混合收集(Mixed GC):收集整個新生代和部分老年代的垃圾收集,目前只有G1 GC有這種行爲
  • 整堆收集(Full GC):收集整個java堆和方法區的垃圾收集

下面分別介紹這幾種垃圾收集機制,這裏只是簡單介紹這些機制的用處,並不會深入解析

4.1Minor GC

  • 當年輕代空間(Eden)空間不足時會觸發Minor GC,Survivor滿不會觸發Minor GC
  • 因爲Java對象大多都具備朝生夕死的特徵,所以Minor GC非常頻繁,所以垃圾回收主要發生在新生代
  • Minor GC會引發STW,暫停其他用戶線程,當垃圾回收結束,用戶線程才恢復運行

4.2Major GC

即老年代GC觸發機制、

  • 當對象從老年代消失,我們就會說Major GC或Full GC
  • 出現Major GC,經常會伴隨至少一次的Minor GC(當老年代空間不足時,會先嚐試觸發Minor GC,如果之後空間還不足,就會觸發Major GC)
  • Major GC的速度一般比Minor GC慢10倍以上,STW時間更長
  • Major GC之後內存還不足,就會OOM

4.3Full GC

當出現以下幾種情況會觸發Full GC

  • 調用System.gc()
  • 老年代空間不足
  • 方法區空間不足
  • 通過Minor GC後進入老年代的平均大小大於老年代的可用內存
  • 由Eden,S0(From區)向S1(To區)複製時,對象大小大於To區可用內存,則把對象轉入老年代,且老年代可用空間大小小於該對象大小

5.爲什麼要分代

前面我們說過,不同對象的生命週期不同,且70%-99%的對象都是臨時對象,如果不分代,就相當於將所有對象放在一塊管理,這樣對GC性能影響太大,而分代的唯一理由就是優化GC性能,新生代存儲的對象大多都是朝生夕死,這個區域也是GC最頻繁的地區,類似數據庫連接池對象則放在老年代,這樣的分代管理就大大優化GC性能

6.本地線程緩衝TLAB

6.1爲什麼要有TLAB(Thread Local Allocation Buffer)

我們都知道堆是線程共享的,並且對象在虛擬機中的創建是非常頻繁的,在併發情況下是不安全的,比如正在給A對象分配內存,指針還沒來得及修改,對象B又使用了原來的指針,解決這個問題有兩種方式,一種是對分配空間的動作進行同步,這種方式影響分配速度,另一種就是每個線程在堆中預先分配一小塊內存,稱爲本地線程分配緩衝

6.2什麼是TLAB

從內存模型的角度來看,在堆中的Eden區域,jvm爲每個線程分配了一小塊空間,如圖所示
在這裏插入圖片描述
通過TLAB,可以避免一些線程安全問題,因爲每個線程的對象創建銷燬都是在本地線程的TLAB空間中進行,屬於線程私有,不會造成併發問題,並且能夠提升內存分配的吞吐量,因此這種內存分配方式被稱爲快速分配策略,JVM將TLAB作爲內存分配的首選,我們可以在程序中通過-XX:UseTLAB設置是否開啓TLAB空間

默認情況下,TLAB空間的內存非常小,僅佔有整個Eden空間的1%,通過選項-XX:TLABWasteTargetPertcent重新設置所佔Eden空間百分比,一旦對象在TLAB空間分配內存失敗,JVM就會嘗試通過加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配內存

在這裏插入圖片描述

7.堆是否是對象存儲的唯一選擇

到目前爲止我們所說的對象都是在堆上創建的,在《深入理解Java虛擬機》中有這樣一段描述:

隨着JIT編譯期的發展和逃逸分析技術逐漸成熟,棧上分配,標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那麼絕對了

言外之意,通過其他技術類似逃逸分析技術的發展,對象可以不只分配在對上,也可以在棧上分配了
在這裏插入圖片描述
那麼什麼是逃逸分析

7.1逃逸分析

舉個例子

class Demo{
    void fun(){
        Demo d = new Demo();
    }
}

在fun方法中過的對象僅僅作用在這個方法內,即沒有逃逸到方法外,對於這種對象,可以將其分配到棧上,隨着方法的結束進行彈棧,棧空間被移除,對象也就被銷燬,不存在GC問題,所以經過逃逸分析,我們可以對代碼進行多種優化方式

7.2棧上分配

和在逃逸分析中舉得例子一樣, 對象儘可能的寫成局部變量的方式,可以避免垃圾回收,提高程序性能,但值得注意的是:

  • 逃逸技術並不是很成熟,且HotSpot虛擬機並沒有採用逃逸分析技術,所以到目前爲止,所有的對象還是分配到java堆上的

7.3同步省略

線程的同步往往會帶來程序性能的下降,舉個例子:
在這裏插入圖片描述
未發生逃逸的對象可以省略同步代碼塊,進而提高性能

7.4標量分配

首先要知道什麼是標量(Scalar),值無法再分解的數據,對應着java的原始數據,與之相對的就叫做聚合量,對象就是聚合量,看下面的代碼:

public class heapTest {
    public static void main(String[] args){
        alloc();
    }
    static void alloc(){
        Demo d = new Demo();
        System.out.println(d.x + " " + d.y);
    }
}
class Demo{
    int x;
    int y;

}

在alloc函數中,對象未發生逃逸,進而可以將代碼拆分爲:

static void alloc(){
        int x;
        int y;
        System.out.println(d.x + " " + d.y);
    }

在經過拆分後,大大減少了堆內存的佔用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章