簡單說說JVM堆區的相關知識

一、堆概述

  • 一個jvm實例(進程)隻存在一個堆內存,堆也是java內存管理的核心區域。
  • java 堆區在jvm啟動時即被創建,其空間大小也就被確定瞭
  • 《java虛擬機規范》規定,堆可以處於物理上不連續的內存空間,但在邏輯上它應該被稱為連續的
  • 所有線程共享java堆,在這裡和可以劃分線程私有的緩沖區(tlab)
  • 所有對象實例以及數組都應在運行時分配在堆中
  • 方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集時候才會被移除
  • 堆是gc執行垃圾回收的重點區域

1.1 堆內存細分

現代垃圾收集器大部分基於分代收集理論設計,堆空間細分為:

  • java 7 之前堆內存邏輯分為:新生區+老年區+永久區
  • java 8 之後內存邏輯上分為:新生區+老年區+元空間

使用下面命令設置堆空間初始化 10m,最大空間 10m

-Xms10m -Xmx10m

使用java visual 查看 visual gc

可以看出通過參數設置的內存大小 隻與新生代(Eden+s0+s1 ),老年代有關,而在邏輯上還要加上元空間。

1.2 堆空間大小的設置

1.2.1 通過參數設置

-Xms 用來設置堆空間(年輕代+老年代)的初始內存大小
  -X 是jvm的運行參數
  ms 是memory start
  
-Xmx 用來設置堆空間(年輕代+老年代)的初始內存大小
  • 一旦堆區中的內存大小超過“-Xmx”所指定的最大內存時,將會拋出OutOfMemoryError異常。
  • 通常會將 -Xms 和 -Xmx 兩個參數配置相同的值,其目的是為瞭能夠在java垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,從而提高性能

1.2.2 默認空間大小

  • 初始化內存大小為物理內存的 1/64
  • 最大內存大小為物理內存大小 1/4

使用一下代碼查看 當前jvm初始化內存與最大內存

/**
 * @program: jvmDemo
 * @description:
 * @author: wfg
 * @create: 2021-06-14 10:40
 */
public class Test9 {

    public static void main(String[] args) {
        //返回jvm中的內存總量(字節)
          long initialMemory = Runtime.getRuntime().totalMemory()/1024/1024;
        //虛擬機將嘗試使用最大堆內存
         long maxMemory = Runtime.getRuntime().maxMemory()/1024/1024;
        System.out.println("-Xms:"+initialMemory+"m");

        System.out.println("-Xmx:"+maxMemory+"m");


        System.out.println("系統大小:"+initialMemory*64/1024+"G");
        System.out.println("系統大小:"+maxMemory*4/1024+"G");

    }

}

結果(本機運行內存為 8g)

1.2.3 通過參數設置堆空間大小後內存不一致問題

設置300

查看

/**
 * @program: jvmDemo
 * @description:
 * @author: wfg
 * @create: 2021-06-14 10:40
 */
public class Test9 {

    public static void main(String[] args) {
        //返回jvm中的內存總量(字節)
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //虛擬機將嘗試使用最大堆內存
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        
        System.out.println("-Xms:" + initialMemory + "m");

        System.out.println("-Xmx:" + maxMemory + "m");
        
    }

}

結果

分析

在vm參數設置裡面加上

-XX:+PrintGCDetails 

再次運行程序查看

原理

新生代的s0 和 s1 隻能有一個生效

1.3 年輕代與年老代

  • 存儲在jvm中的java對象可以劃分為兩類:

一類是生命周期較短的瞬時對象。

另外一類是對象生命周期非常長。

  • jvm堆區再次進行細分可以分為 年輕代與老年代
  • 其中年輕代可以劃分為Eden空間,survivor0空間和survivor1空間(有時也叫 from區,to區)

  • 新生代與老年代空間大小 默認是 1:2

可以通過 -XX:NewRation設置新生代與老年代的比例,默認值是2.(一般都不會去設置)

  • Eden與s0,s1 的內存默認分配比例為 8:1:1

1.4 對象分配過程

1.new的對象先放伊甸區,此區有大小限制

2.當伊甸區滿的時候,程序需要創建時,jvm的垃圾回收將對伊甸園區進行垃圾回收(minor gc)

3.然後將伊甸園中的剩餘對象移動到辛存者0

4.如果再次觸發垃圾回收,上次幸存下來的放到幸存者0區,沒有回收,就會放到幸存者1區

5.再次經歷垃圾回收會重新放回辛存者0區

6.當在辛存者區達到15次時,就可以去老年區瞭

可以設置參數:-XX:MaxTenuringThreshold=

7.當養老區內存不足時,再次觸發GC:major GC,進行養老區的內存處理

8.若養老區進行處理後,依然無法進行對象的保存,就會產生00m異常

java.lang.outofMemoryError:java heap space

9.總結

  • 針對幸存者s0,s1區總結:復制之後有交換誰空誰是to
  • 垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不再永久區/元空間收集

10.流程

當Eden滿時,會觸發MinorGC算法來回收memory,旨在清理掉再無引用的數據(在內存裡是Tree),意圖存儲到S0. 若此時S0也滿瞭,會再次MinorGC意圖回收S0無引用的數據,把有引用的數據移動到S1。如果S1夠用,此時會清空S0;如果S1滿瞭,會回滾剛存入S1的數據,直接把本次GC的數據存入Old區,S0保持剛剛MinorGC時的狀態。延伸:如果Old也滿瞭,會觸發MajorGC,如果還是不夠,則存入Permanent Generate,不幸這裡也滿瞭,會在允許的范圍內按照內置的規則自動增長,可能不會發生GC,也可能會。當增長的量不夠存時,會觸發Full GC。若FullGC後還是不夠存,自動增長的量也超過瞭允許的范圍,則發生內存溢出。還有一種情況,就是分配的線程棧處於很深的遞歸或死循環時,會發生棧內存溢出。

二、對象分配過程:Tlab

2.1 為什麼要有tlab

  • 堆區是線程共享的,任何線程都可以訪問到堆區中的共享數據
  • 由於對象實例的創建在jvm中非常頻繁,因此在並發環境下從堆區中劃分內存空間是不安全的
  • 而加鎖會影響分配速度

2.2什麼是tlab

  • 從內存模型而不是垃圾收集角度,對eden區域進行劃分,jvm為每個線程分配瞭一個私有緩存區域
  • 多線程同時分配內存時,使用tlab可以避免非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們將其稱為 快速分配策略
  • jvm將tlab作為內存分配的首選
  • 在程序中可以通過參數-XX:UseTLAB設置是否開啟(默認是開啟的)
  • tlab僅占有eden空間大小的1%,可以通過-XX:TLABWasteTargetPercent設置tlab空間所占用eden空間的大小
  • 使用tlab空間分配內存失敗時,jvm會使用加鎖機制確保操作的原子性

對象分配流程

對象分配流程

三、堆空間常用參數設置

  •  -XX:+PrintFlagsInitial: 查看所有參數的默認初始值
  • -XX:+PrintFlagFinal: 查看所有參數的最終值
  • -Xms::初始化堆空間內存(默認為物理內存的1/64)
  • -Xmx: 最大堆空間內存(默認為物理內存的1/4)
  • -Xmn:設置新生代的大小(初始值及最大值)
  • -XX:NewRatio: 配置新生代與老年代在堆結構的占比
  • -XX:SurvivorRatio:設置新生代中 eden和s0、s1空間的比例
  • -XX:MaxTenuringThreshold:設置新生代垃圾最大的年齡
  • -XX:+PrintGCDetails:輸出詳細的gc處理日志
  • -XX:+PrintGC:輸出簡要的gc處理日志

四、堆是分配對象的唯一選擇嗎

  1. 隨著jit編譯期的發展與逃逸分析技術成熟,棧上分配與標量替換優化技術導致所有對象都分配到堆上不那麼絕對瞭
  2. 如果經過逃逸分析後發現,一個對象並沒有逃逸出方法的話,那麼可能被優化成棧上分配

4.1 逃逸分析

  • 當一個對象在方法中被定義後,對象隻在方法內部使用,則認為沒有發生逃逸。
  • 當一個對象在方法中被定義後,被外部方法所引用,則認為發生逃逸瞭

判斷逃逸的方法:看new 的對象實體是否有可能在方法外被調用

在這裡插入圖片描述

4.2 代碼優化

  • 棧上分配。將堆分配轉化為棧分配。
  • 同步省略。如果一個對象被發現隻能從一個線程被訪問到,那麼對於這個對象的操作可以不考慮同步
  • 分離對象或標量替換。有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那麼對象的部分可以不存儲在內存,而是存儲在cpu寄存器中。
  •  標量是指一個無法在分解成更小的數據,相對的其它還可以在分解的稱為聚合量
  • 在jit階段進行逃逸分析,發現對象不會被外界訪問(沒有逃逸發生),經過jit優化,就回把對象分解為成員變量來代替,這個過程就是標量替換

到此這篇關於簡單說說JVM堆區的相關知識的文章就介紹到這瞭,更多相關JVM堆區內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: