日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

运行数据区②---堆

發(fā)布時(shí)間:2023/12/20 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 运行数据区②---堆 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本篇目錄

  • 1. 核心概述
    • 1.1 配置jvm及查看jvm進(jìn)程
    • 1.2 分析SimpleHeap的jvm情況
    • 1.3 堆的細(xì)分內(nèi)存結(jié)構(gòu)
      • JDK 7以前: 新生區(qū)+老年區(qū)+永久區(qū)
      • JDK 8以后: 新生區(qū)+老年區(qū)+元空間
  • 2.設(shè)置堆內(nèi)存大小與OOM
    • 2.1 查看堆內(nèi)存大小
    • 2.2 堆大小分析
    • 2.3 OOM
  • 3.年輕代與老年代
  • 4.圖解對(duì)象分配過(guò)程
    • 4.1 一般分配過(guò)程
    • 4.2 對(duì)象分配的特殊情況
    • 4.3 常用調(diào)優(yōu)工具
  • 5.Minor GC、Major GC、Full GC
    • 5.1 年輕代GC(Minor GC)觸發(fā)機(jī)制:
    • 5.2 老年代GC(Major GC/Full GC)觸發(fā)機(jī)制
    • 5.3 Full GC觸發(fā)機(jī)制
  • 6.堆空間分代思想
  • 7.內(nèi)存分配策略
  • 8.為對(duì)象分配內(nèi)存:TLAB(線程私有緩存區(qū)域)
    • 8.1 為什么有TLAB(Thread Local Allocation Buffer)
    • 8.2 什么是TLAB
    • 8.3 TLAB對(duì)象分配過(guò)程
  • 9. 堆空間的參數(shù)設(shè)置
  • 10.堆是分配對(duì)象的唯一選擇么
    • 10.1 代碼分析
    • 10.2 逃逸分析
    • 10.3 參數(shù)設(shè)置
    • 10.4 代碼優(yōu)化
      • ① 棧上分配
      • ② 同步省略
      • ③ 分離對(duì)象或標(biāo)量替換
    • 10.5 逃逸分析小結(jié)

1. 核心概述

一個(gè)進(jìn)程對(duì)應(yīng)一個(gè)jvm實(shí)例,一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),又包含多個(gè)線程,這些線程共享了方法區(qū)和堆,每個(gè)線程包含了程序計(jì)數(shù)器、本地方法棧和虛擬機(jī)棧。

  • 一個(gè)jvm實(shí)例只存在一個(gè)堆內(nèi)存,堆也是java內(nèi)存管理的核心區(qū)域
  • Java堆區(qū)在JVM啟動(dòng)的時(shí)候即被創(chuàng)建,其空間大小也就確定了。是JVM管理的最大一塊內(nèi)存空間(堆內(nèi)存的大小是可以調(diào)節(jié)的)
  • 《Java虛擬機(jī)規(guī)范》規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的
  • 所有的線程共享java堆,在這里還可以劃分線程私有的緩沖區(qū)(TLAB:Thread Local Allocation Buffer).(堆空間一定是所有線程共享的么?不是,TLAB線程在堆中獨(dú)有的)
  • 《Java虛擬機(jī)規(guī)范》中對(duì)java堆的描述是:所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在運(yùn)行時(shí)分配在堆上。
    ? 從實(shí)際使用的角度看,“幾乎”所有的對(duì)象的實(shí)例都在這里分配內(nèi)存 (‘幾乎’是因?yàn)榭赡艽鎯?chǔ)在棧上)
  • 數(shù)組或?qū)ο笥肋h(yuǎn)不會(huì)存儲(chǔ)在棧上,因?yàn)闂斜4嬉?#xff0c;這個(gè)引用指向?qū)ο蠡蛘邤?shù)組在堆中的位置
  • 在方法結(jié)束后,堆中的對(duì)象不會(huì)馬上被移除,僅僅在垃圾收集的時(shí)候才會(huì)被移除
  • 堆,是GC(Garbage Collection,垃圾收集器)執(zhí)行垃圾回收的重點(diǎn)區(qū)域
  • 1.1 配置jvm及查看jvm進(jìn)程

    • 編寫HeapDemo/HeapDemo1代碼
    public class HeapDemo {public static void main(String[] args) {System.out.println("start...");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("end...");} }
    • 首先對(duì)虛擬機(jī)進(jìn)行配置,如圖 Run-Edit configurations
    • 在jdk目錄,我的是…/jdk1.8.0_171.jdk/Contents/Home/bin下找到j(luò)visualvm 運(yùn)行(或者直接終端運(yùn)行jvisualvm),查看進(jìn)程,可以看到我們?cè)O(shè)置的配置信息

    1.2 分析SimpleHeap的jvm情況

    public class SimpleHeap {private int id;//屬性、成員變量public SimpleHeap(int id) {this.id = id;}public void show() {System.out.println("My ID is " + id);}public static void main(String[] args) {SimpleHeap sl = new SimpleHeap(1);SimpleHeap s2 = new SimpleHeap(2);int[] arr = new int[10];Object[] arr1 = new Object[10];} }

    1.3 堆的細(xì)分內(nèi)存結(jié)構(gòu)

    JDK 7以前: 新生區(qū)+老年區(qū)+永久區(qū)

    • Young Generation Space:又被分為Eden區(qū)和Survior區(qū) Young/New
    • Tenure generation Space: Old/Tenure
    • Permanent Space: Perm

    JDK 8以后: 新生區(qū)+老年區(qū)+元空間

    • Young Generation Space:又被分為Eden區(qū)和Survior區(qū) Young/New
    • Tenure generation Space: Old/Tenure
    • Meta Space: Meta

    2.設(shè)置堆內(nèi)存大小與OOM

    ? Java堆區(qū)用于存儲(chǔ)java對(duì)象實(shí)例,堆的大小在jvm啟動(dòng)時(shí)就已經(jīng)設(shè)定好了,可以通過(guò) "-Xmx"和 "-Xms"來(lái)進(jìn)行設(shè)置

    • -Xms 用于表示堆的起始內(nèi)存,等價(jià)于 -XX:InitialHeapSize
      • -Xms 用來(lái)設(shè)置堆空間(年輕代+老年代)的初始內(nèi)存大小

        • -X 是jvm的運(yùn)行參數(shù)
        • ms 是memory start
      • -Xmx 用于設(shè)置堆的最大內(nèi)存,等價(jià)于 -XX:MaxHeapSize

    ? 一旦堆區(qū)中的內(nèi)存大小超過(guò) -Xmx所指定的最大內(nèi)存時(shí),將會(huì)拋出OOM異常
    ? 通常會(huì)將-Xms和-Xmx兩個(gè)參數(shù)配置相同的值,其目的就是為了能夠在java垃圾回收機(jī)制清理完堆區(qū)后不需要重新分隔計(jì)算堆區(qū)的大小,從而提高性能
    ? 默認(rèn)情況下,初始內(nèi)存大小:物理內(nèi)存大小/64;最大內(nèi)存大小:物理內(nèi)存大小/4

    • 手動(dòng)設(shè)置:-Xms600m -Xmx600m

    ? 查看設(shè)置的參數(shù):

    • 方式一: 終端輸入jps , 然后 jstat -gc 進(jìn)程id
    • 方式二:(控制臺(tái)打印)Edit Configurations->VM Options 添加 -XX:+PrintGCDetails

    2.1 查看堆內(nèi)存大小

    public class HeapSpaceInitial {public static void main(String[] args) {//返回Java虛擬機(jī)中的堆內(nèi)存總量long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;//返回Java虛擬機(jī)試圖使用的最大堆內(nèi)存量long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms : " + initialMemory + "M");//-Xms : 575MSystem.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 575MSystem.out.println("系統(tǒng)內(nèi)存大小為:" + initialMemory * 64.0 / 1024 + "G");//系統(tǒng)內(nèi)存大小為:35.9375GSystem.out.println("系統(tǒng)內(nèi)存大小為:" + maxMemory * 4.0 / 1024 + "G");//系統(tǒng)內(nèi)存大小為:2.24609375Gtry {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}} }

    2.2 堆大小分析

    設(shè)置堆大小為600m,打印出的結(jié)果為575m,這是因?yàn)樾掖嬲邊^(qū)S0和S1各占據(jù)了25m,但是他們始終有一個(gè)是空的,存放對(duì)象的是伊甸園區(qū)和一個(gè)幸存者區(qū)

    C:\Users\Administrator>jps 1568 17344 Jps 17732 Launcher 2020 HeapSpaceInitial 8824 MainC:\Users\Administrator>jstat -gc 2020S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 25600.0 25600.0 0.0 0.0 153600.0 15360.2 409600.0 0.0 4480.0 675.0 384.0 66.6 0 0.000 0 0.000 0.000C:\Users\Administrator>

    2.3 OOM

    java.lang.OutOfMemoryError: Java heap space

    /*** -Xms600m -Xmx600m*/ public class OOMTest {public static void main(String[] args) {ArrayList<Picture> list = new ArrayList<>();while(true){try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}list.add(new Picture(new Random().nextInt(1024 * 1024)));}} } class Picture{private byte[] pixels;public Picture(int length) {this.pixels = new byte[length];} }//結(jié)果輸出: Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat com.oujiong.jvm.Picture.<init>(OOMTest.java:33)at com.oujiong.jvm.OOMTest.main(OOMTest.java:26)Process finished with exit code 1

    3.年輕代與老年代

    ? 存儲(chǔ)在JVM中的java對(duì)象可以被劃分為兩類:

  • 一類是生命周期較短的瞬時(shí)對(duì)象,這類對(duì)象的創(chuàng)建和消亡都非常迅速
  • 另外一類對(duì)象是生命周期非常長(zhǎng),在某些情況下還能與JVM的生命周期保持一致
  • ? Java堆區(qū)進(jìn)一步細(xì)分可以分為年輕代(YoungGen)和老年代(OldGen)
    ? 其中年輕代可以分為Eden空間、Survivor0空間和Survivor1空間(有時(shí)也叫frmo區(qū),to區(qū))

    ? 配置新生代與老年代在堆結(jié)構(gòu)的占比

  • 默認(rèn)-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個(gè)堆的1/3
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個(gè)堆的1/5
    • 在hotSpot中,Eden空間和另外兩個(gè)Survivor空間缺省所占的比例是8:1:1,開發(fā)人員可以通過(guò)選項(xiàng) -XX:SurvivorRatio 調(diào)整空間比例,如-XX:SurvivorRatio=8

    • 幾乎所有的Java對(duì)象都是在Eden區(qū)被new出來(lái)的

    • 絕大部分的Java對(duì)象都銷毀在新生代了(IBM公司的專門研究表明,新生代80%的對(duì)象都是“朝生夕死”的)

    • 可以使用選項(xiàng)-Xmn設(shè)置新生代最大內(nèi)存大小(這個(gè)參數(shù)一般使用默認(rèn)值就好了)

    測(cè)試代碼:

    /*** -Xms600m -Xmx600m** -XX:NewRatio : 設(shè)置新生代與老年代的比例。默認(rèn)值是2.* -XX:SurvivorRatio :設(shè)置新生代中Eden區(qū)與Survivor區(qū)的比例。默認(rèn)值是8* -XX:-UseAdaptiveSizePolicy :關(guān)閉自適應(yīng)的內(nèi)存分配策略 '-'關(guān)閉,'+'打開 (暫時(shí)用不到)* -Xmn:設(shè)置新生代的空間的大小。 (一般不設(shè)置)**/ public class EdenSurvivorTest {public static void main(String[] args) {System.out.println("我只是來(lái)打個(gè)醬油~");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}} }

    4.圖解對(duì)象分配過(guò)程

    4.1 一般分配過(guò)程

    為新對(duì)象分配內(nèi)存是件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù),JVM的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配、在哪里分配的問題,并且由于內(nèi)存分配算法與內(nèi)存回收算法密切相關(guān),所以還需要考慮GC執(zhí)行完內(nèi)存回收后是否會(huì)在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。

  • new的對(duì)象先放伊甸園區(qū)。此區(qū)有大小限制。
  • 當(dāng)伊甸園的空間填滿時(shí),程序又需要?jiǎng)?chuàng)建對(duì)象,JVM的垃圾回收器將對(duì)伊甸園區(qū)進(jìn)行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷毀,再加載新的對(duì)象放到伊甸園區(qū)
  • 然后將伊甸園中的剩余對(duì)象移動(dòng)到幸存者0區(qū)。
  • 如果再次觸發(fā)垃圾回收,此時(shí)上次幸存下來(lái)的放到幸存者0區(qū)的,如果沒有回收,就會(huì)放到幸存者1區(qū)。
  • 如果再次經(jīng)歷垃圾回收,此時(shí)會(huì)重新放回幸存者0區(qū),接著再去幸存者1區(qū)。
  • 啥時(shí)候能去老年區(qū)呢?可以設(shè)置次數(shù)。默認(rèn)是15次。·可以設(shè)置參數(shù):-XX:MaxTenuringThreshold=進(jìn)行設(shè)置。
  • 在老年,相對(duì)悠閑。當(dāng)老年區(qū)內(nèi)存不足時(shí),再次觸發(fā)GC:Major GC,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理。
  • 若養(yǎng)老區(qū)執(zhí)行了Major GC之后發(fā)現(xiàn)依然無(wú)法進(jìn)行對(duì)象的保存,就會(huì)產(chǎn)生OOM異常。
  • 總結(jié)
    針對(duì)幸存者s0,s1區(qū):復(fù)制之后有交換,誰(shuí)空誰(shuí)是to
    關(guān)于垃圾回收:頻繁在新生區(qū)收集,很少在老年區(qū)收集,幾乎不在永久區(qū)/元空間收集。

    4.2 對(duì)象分配的特殊情況

    4.3 常用調(diào)優(yōu)工具

    ? JDK命令行
    ? Eclipse:Memory Analyzer Tool
    ? Jconsole
    ? VisualVM
    ? Jprofiler
    ? Java Flight Recorder
    ? GCViewer
    ? GC Easy

    5.Minor GC、Major GC、Full GC

    JVM在進(jìn)行GC時(shí),并非每次都針對(duì)上面三個(gè)內(nèi)存區(qū)域(新生代、老年代、方法區(qū))一起回收的,大部分時(shí)候回收都是指新生代。

    針對(duì)hotSpot VM的實(shí)現(xiàn),它里面的GC按照回收區(qū)域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)

  • 部分收集:不是完整收集整個(gè)Java堆的垃圾收集。其中又分為:
    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集

    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集

      ---- 目前,只有CMS GC會(huì)有單獨(dú)收集老年代的行為。注意,很多時(shí)候Major GC 會(huì)和 Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收

    • 混合收集(Mixed GC):收集整個(gè)新生代以及部分老年代的垃圾收集
      ----目前,只有G1 GC會(huì)有這種行為

  • 整堆收集(Full GC):收集整個(gè)java堆和方法區(qū)的垃圾收集
  • 5.1 年輕代GC(Minor GC)觸發(fā)機(jī)制:

    ? 當(dāng)年輕代空間不足時(shí),就會(huì)觸發(fā)Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會(huì)引發(fā)GC.(每次Minor GC會(huì)清理年輕代的內(nèi)存,Survivor是被動(dòng)GC,不會(huì)主動(dòng)GC)
    ? 因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性,所以Minor GC 非常頻繁,一般回收速度也比較快,這一定義既清晰又利于理解。
    ? Minor GC 會(huì)引發(fā)STW(Stop the World),暫停其他用戶的線程,等垃圾回收結(jié)束,用戶線程才恢復(fù)運(yùn)行。

    5.2 老年代GC(Major GC/Full GC)觸發(fā)機(jī)制

    ? 指發(fā)生在老年代的GC,對(duì)象從老年代消失時(shí),Major GC 或者 Full GC 發(fā)生了
    ? 出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC(不是絕對(duì)的,在Parallel Scavenge 收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過(guò)程)

    • 也就是老年代空間不足時(shí),會(huì)先嘗試觸發(fā)Minor GC。如果之后空間還不足,則觸發(fā)Major GC

    ? Major GC速度一般會(huì)比Minor GC慢10倍以上,STW時(shí)間更長(zhǎng)
    ? 如果Major GC后,內(nèi)存還不足,就報(bào)OOM了

    5.3 Full GC觸發(fā)機(jī)制

    • 觸發(fā)Full GC執(zhí)行的情況有以下五種:
      ①調(diào)用System.gc()時(shí),系統(tǒng)建議執(zhí)行Full GC,但是不必然執(zhí)行
      ②老年代空間不足
      ③方法區(qū)空間不足
      ④通過(guò)Minor GC后進(jìn)入老年代的平均大小小于老年代的可用內(nèi)存
      ⑤由Eden區(qū),Survivor S0(from)區(qū)向S1(to)區(qū)復(fù)制時(shí),對(duì)象大小小于To Space可用內(nèi)存,則把該對(duì)象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存小于該對(duì)象大小
    • 說(shuō)明:Full GC 是開發(fā)或調(diào)優(yōu)中盡量要避免的,這樣暫停時(shí)間會(huì)短一些


    代碼演示:
    Young GC ->Full GC -> OOM

    /** 測(cè)試GC分代回收* 測(cè)試MinorGC 、 MajorGC、FullGC* -Xms9m -Xmx9m -XX:+PrintGCDetails*/ public class GCTest {public static void main(String[] args) {int i = 0;try {List<String> list = new ArrayList<>();String a = "testGC";while (true) {list.add(a);a = a + a;i++;}} catch (Throwable t) {t.printStackTrace();System.out.println("遍歷次數(shù)為:" + i);}} }

    輸出結(jié)果:

    [GC (Allocation Failure) [PSYoungGen: 2048K->507K(2560K)] 2048K->994K(9728K), 0.0023094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2457K->504K(2560K)] 2944K->1630K(9728K), 0.0022268 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2113K->491K(2560K)] 3240K->2994K(9728K), 0.0018447 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2083K->491K(2560K)] 4586K->4546K(9728K), 0.0019428 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 1329K->0K(2560K)] [ParOldGen: 7126K->4169K(7168K)] 8456K->4169K(9728K), [Metaspace: 3347K->3347K(1056768K)], 0.0106081 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) --[PSYoungGen: 1562K->1562K(2560K)] 5731K->7267K(9728K), 0.0013447 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 1562K->0K(2560K)] [ParOldGen: 5705K->5705K(7168K)] 7267K->5705K(9728K), [Metaspace: 3352K->3352K(1056768K)], 0.0039439 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5705K->5705K(9728K), 0.0005427 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 5705K->5595K(7168K)] 5705K->5595K(9728K), [Metaspace: 3352K->3352K(1056768K)], 0.0165113 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:3332)at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)at java.lang.StringBuilder.append(StringBuilder.java:136)at com.oujiong.jvm.GCTest.main(GCTest.java:26) 遍歷次數(shù)為:17 HeapPSYoungGen total 2560K, used 87K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd15e80,0x00000000fff00000)from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)ParOldGen total 7168K, used 5595K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 78% used [0x00000000ff600000,0x00000000ffb76f90,0x00000000ffd00000)Metaspace used 3384K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 363K, capacity 388K, committed 512K, reserved 1048576KProcess finished with exit code 0

    6.堆空間分代思想

    為什么要把Java堆分代?不分代就不能正常工作了么

    • 經(jīng)研究,不同對(duì)象的生命周期不同。70%-99%的對(duì)象都是臨時(shí)對(duì)象。
      ? 新生代:有Eden、Survivor構(gòu)成(s0,s1 又稱為from to),to總為空
      ? 老年代:存放新生代中經(jīng)歷多次依然存活的對(duì)象
    • 其實(shí)不分代完全可以,分代的唯一理由就是優(yōu)化GC性能。如果沒有分代,那所有的對(duì)象都在一塊,就如同把一個(gè)學(xué)校的人都關(guān)在一個(gè)教室。GC的時(shí)候要找到哪些對(duì)象沒用,這樣就會(huì)對(duì)堆的所有區(qū)域進(jìn)行掃描,而很多對(duì)象都是朝生夕死的,如果分代的話,把新創(chuàng)建的對(duì)象放到某一地方,當(dāng)GC的時(shí)候先把這塊存儲(chǔ)“朝生夕死”對(duì)象的區(qū)域進(jìn)行回收,這樣就會(huì)騰出很大的空間出來(lái)。

    7.內(nèi)存分配策略

    • 如果對(duì)象在Eden出生并經(jīng)過(guò)第一次Minor GC后依然存活,并且能被Survivor容納的話,將被移動(dòng)到Survivor空間中,把那個(gè)將對(duì)象年齡設(shè)為1.對(duì)象在Survivor區(qū)中每熬過(guò)一次MinorGC,年齡就增加一歲,當(dāng)它的年齡增加到一定程度(默認(rèn)15歲,其實(shí)每個(gè)JVM、每個(gè)GC都有所不同)時(shí),就會(huì)被晉升到老年代中
      ? 對(duì)象晉升老年代的年齡閾值,可以通過(guò)選項(xiàng) -XX:MaxTenuringThreshold來(lái)設(shè)置
    • 針對(duì)不同年齡段的對(duì)象分配原則如下:
      ? 優(yōu)先分配到Eden
      ? 大對(duì)象直接分配到老年代。盡量避免程序中出現(xiàn)過(guò)多的大對(duì)象
      ? 長(zhǎng)期存活的對(duì)象分配到老年代
      ? 動(dòng)態(tài)對(duì)象年齡判斷: 如果Survivor區(qū)中相同年齡的所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象可以直接進(jìn)入到老年代。無(wú)需等到MaxTenuringThreshold中要求的年齡
      ? 空間分配擔(dān)保: -XX: HandlePromotionFailure

    代碼示例:
    分配60m堆空間,新生代 20m ,Eden 16m, s0 2m, s1 2m,buffer對(duì)象20m,Eden 區(qū)無(wú)法存放buffer, 直接晉升老年代

    /** 測(cè)試:大對(duì)象直接進(jìn)入老年代* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails*/ public class YoungOldAreaTest {// 新生代 20m ,Eden 16m, s0 2m, s1 2m// 老年代 40mpublic static void main(String[] args) {//Eden 區(qū)無(wú)法存放buffer 晉升老年代byte[] buffer = new byte[1024 * 1024 * 20];//20m} }

    日志輸出:

    8.為對(duì)象分配內(nèi)存:TLAB(線程私有緩存區(qū)域)

    8.1 為什么有TLAB(Thread Local Allocation Buffer)

    • 堆區(qū)是線程共享區(qū)域,任何線程都可以訪問到堆區(qū)中的共享數(shù)據(jù)
    • 由于對(duì)象實(shí)例的創(chuàng)建在JVM中非常頻繁,在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
    • 為避免多個(gè)線程操作同一地址,需要使用加鎖等機(jī)制,進(jìn)而影響分配速度

    8.2 什么是TLAB

    • 從內(nèi)存模型而不是垃圾收集的角度,對(duì)Eden區(qū)域繼續(xù)進(jìn)行劃分,JVM為每個(gè)線程分配了一個(gè)私有緩存區(qū)域,它包含在Eden空間內(nèi)
    • 多線程同時(shí)分配內(nèi)存時(shí),使用TLAB可以避免一系列的非線程安全問題,同時(shí)還能夠提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方式稱之為快速分配策略
    • 所有OpenJDK衍生出來(lái)的JVM都提供了TLAB的設(shè)計(jì)

    說(shuō)明
    ? 盡管不是所有的對(duì)象實(shí)例都能夠在TLAB中成功分配內(nèi)存,但JVM明確是將TLAB作為內(nèi)存分配的首選
    ? 在程序中,開發(fā)人員可以通過(guò)選項(xiàng)“-XX:UseTLAB“ 設(shè)置是否開啟TLAB空間
    ? 默認(rèn)情況下,TLAB空間的內(nèi)存非常小,僅占有整個(gè)EDen空間的1%,當(dāng)然我們可以通過(guò)選項(xiàng) ”-XX:TLABWasteTargetPercent“ 設(shè)置TLAB空間所占用Eden空間的百分比大小
    ? 一旦對(duì)象在TLAB空間分配內(nèi)存失敗時(shí),JVM就會(huì)嘗試著通過(guò)使用加鎖機(jī)制確保數(shù)據(jù)操作的原子性,從而直接在Eden空間中分配了內(nèi)存

    代碼演示:
    終端輸入 jps,查看TLABArgsTest進(jìn)程id
    jinfo -flag UseTLAB 64566(進(jìn)程id),輸出-XX:+UseTLAB,證明TLAB默認(rèn)是開啟的

    /*** 測(cè)試-XX:UseTLAB參數(shù)是否開啟的情況:默認(rèn)情況是開啟的*/ public class TLABArgsTest {public static void main(String[] args) {System.out.println("我只是來(lái)打個(gè)醬油~");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}} }//查看結(jié)果 C:\Users\Administrator>jps 1568 18528 TLABArgsTest 13384 Jps 8824 Main 8396 LauncherC:\Users\Administrator>jinfo -flag UseTLAB 18528 -XX:+UseTLABC:\Users\Administrator>

    8.3 TLAB對(duì)象分配過(guò)程

    9. 堆空間的參數(shù)設(shè)置

    • -XX:PrintFlagsInitial: 查看所有參數(shù)的默認(rèn)初始值

    • -XX:PrintFlagsFinal:查看所有的參數(shù)的最終值(可能會(huì)存在修改,不再是初始值)
      ? 具體查看某個(gè)參數(shù)的指令:
      ? jps:查看當(dāng)前運(yùn)行中的進(jìn)程
      ? jinfo -flag SurvivorRatio 進(jìn)程id: 查看新生代中Eden和S0/S1空間的比例

    • -Xms: 初始堆空間內(nèi)存(默認(rèn)為物理內(nèi)存的1/64)

    • -Xmx: 最大堆空間內(nèi)存(默認(rèn)為物理內(nèi)存的1/4)

    • -Xmn: 設(shè)置新生代大小(初始值及最大值)

    • -XX:NewRatio: 配置新生代與老年代在堆結(jié)構(gòu)的占比

    • -XX:SurvivorRatio:設(shè)置新生代中Eden和S0/S1空間的比例

    • -XX:MaxTenuringThreshold:設(shè)置新生代垃圾的最大年齡(默認(rèn)15)

    • -XX:+PrintGCDetails:輸出詳細(xì)的GC處理日志
      ? 打印gc簡(jiǎn)要信息:① -XX:+PrintGC ② -verbose:gc

    • -XX:HandlePromotionFailure:是否設(shè)置空間分配擔(dān)保

    說(shuō)明:
    在發(fā)生Minor Gc之前,虛擬機(jī)會(huì)檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象的總空間。

    • 如果大于,則此次Minor GC是安全的
    • 如果小于,則虛擬機(jī)會(huì)查看-XX:HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。(JDK 7以后的規(guī)則HandlePromotionFailure可以認(rèn)為就是true)
      • 如果HandlePromotionFailure=true,那么會(huì)繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升到老年代的對(duì)象的平均大小。
        • √如果大于,則嘗試進(jìn)行一次Minor GC,但這次Minor GC依然是有風(fēng)險(xiǎn)的;
        • √如果小于,則改為進(jìn)行一次FullGC。
      • √如果HandlePromotionFailure=false,則改為進(jìn)行一次Full GC。

    在JDK6 Update24之后(JDK7),HandlePromotionFailure參數(shù)不會(huì)再影響到虛擬機(jī)的空間分配擔(dān)保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數(shù),但是在代碼中已經(jīng)不會(huì)再使用它。JDK6 Update24之后的規(guī)則變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對(duì)象總大小或者歷次晉升的平均大小就會(huì)進(jìn)行Minor GC,否則將進(jìn)行Full GC。

    10.堆是分配對(duì)象的唯一選擇么

    堆不是分配對(duì)象的唯一選擇

    在《深入理解Java虛擬機(jī)》中關(guān)于Java堆內(nèi)存有這樣一段描述:隨著JIT編譯期的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化,所有的對(duì)象都分配到堆上也漸漸變得不那么“絕對(duì)”了。
    ??在Java虛擬機(jī)中,對(duì)象是在Java堆中分配內(nèi)存的,這是一個(gè)普遍的常識(shí)。但是,有一種特殊情況,那就是如果經(jīng)過(guò)逃逸分析(Escape Analysis)后發(fā)現(xiàn),一個(gè)對(duì)象并沒有逃逸出方法的話,那么就可能被優(yōu)化成棧上分配。這樣就無(wú)需在堆上分配內(nèi)存,也無(wú)須進(jìn)行垃圾回收了。這也是最常見的堆外存儲(chǔ)技術(shù)。
    ??此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中創(chuàng)新的GCIH(GCinvisible heap)技術(shù)實(shí)現(xiàn)off-heap,將生命周期較長(zhǎng)的Java對(duì)象從heap中移至heap外,并且GC不能管理GCIH內(nèi)部的Java對(duì)象,以此達(dá)到降低GC的回收頻率和提升GC的回收效率的目的。

    • 如何將堆上的對(duì)象分配到棧,需要使用逃逸分析手段。
    • 這是一種可以有效減少Java程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。
    • 通過(guò)逃逸分析,Java Hotspot編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。
    • 逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:
      ? 當(dāng)一個(gè)對(duì)象在方法中被定義后,對(duì)象只在方法內(nèi)部使用,則認(rèn)為沒有發(fā)生逃逸。
      ? 當(dāng)一個(gè)對(duì)象在方法中被定義后,它被外部方法所引用,則認(rèn)為發(fā)生逃逸。例如作為調(diào)用參數(shù)傳遞到其他地方中。
    • 如何快速的判斷是否發(fā)生了逃逸分析,就看new的對(duì)象實(shí)體是否有可能在方法外被調(diào)用

    10.1 代碼分析

    public void method(){V v = new V();//use V//......v = null; }

    1、沒有發(fā)生逃逸的對(duì)象,則可以分配到棧上,隨著方法執(zhí)行的結(jié)束,棧空間就被移除。



    public static StringBuffer createStringBuffer(String s1,String s2){StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }

    2、由于上述方法返回的sb在方法外被使用,發(fā)生了逃逸,上述代碼如果想要StringBuffer sb不逃出方法,可以這樣寫:

    public static String createStringBuffer(String s1,String s2){StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); }

    10.2 逃逸分析

    /*** 逃逸分析** 如何快速的判斷是否發(fā)生了逃逸分析,就看new的對(duì)象實(shí)體是否有可能在方法外被調(diào)用。*/ public class EscapeAnalysis {public EscapeAnalysis obj;/*方法返回EscapeAnalysis對(duì)象,發(fā)生逃逸*/public EscapeAnalysis getInstance(){return obj == null? new EscapeAnalysis() : obj;}/*為成員屬性賦值,發(fā)生逃逸*/public void setObj(){this.obj = new EscapeAnalysis();}//思考:如果當(dāng)前的obj引用聲明為static的?仍然會(huì)發(fā)生逃逸。/*對(duì)象的作用域僅在當(dāng)前方法中有效,沒有發(fā)生逃逸*/public void useEscapeAnalysis(){EscapeAnalysis e = new EscapeAnalysis();}/*引用成員變量的值,發(fā)生逃逸*/public void useEscapeAnalysis1(){EscapeAnalysis e = getInstance();//getInstance().xxx()同樣會(huì)發(fā)生逃逸} }

    10.3 參數(shù)設(shè)置

    • 在JDK 6u23版本之后,HotSpot中默認(rèn)就已經(jīng)開啟了逃逸分析
    • 如果使用了較早的版本,開發(fā)人員可以通過(guò)
      ? -XX:DoEscapeAnalysis 顯式開啟逃逸分析
      ? -XX:+PrintEscapeAnalysis查看逃逸分析的篩選結(jié)果

    結(jié)論
    開發(fā)中能使用局部變量的,就不要使用在方法外定義

    10.4 代碼優(yōu)化

    使用逃逸分析,編譯器可以對(duì)代碼做如下優(yōu)化:

  • 棧上分配:將堆分配轉(zhuǎn)化為棧分配。如果一個(gè)對(duì)象在子線程中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,對(duì)象可能是棧分配的候選,而不是堆分配
  • 同步省略:如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步
  • 分離對(duì)象或標(biāo)量替換:有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問到,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ)在CPU寄存器中。
  • ① 棧上分配

    ? JIT編譯器在編譯期間根據(jù)逃逸分析的結(jié)果,發(fā)現(xiàn)如果一個(gè)對(duì)象并沒有逃逸出方法的話,就可能被優(yōu)化成棧上分配。分配完成之后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對(duì)象也被回收。這樣就無(wú)須進(jìn)行垃圾回收了
    ? 常見的棧上分配場(chǎng)景:給成員變量賦值、方法返回值、實(shí)例引用傳遞

    代碼分析:
    以下代碼,關(guān)閉逃逸分析(-XX:-DoEscapeAnalysi),維護(hù)1000000個(gè)對(duì)象,如果開啟逃逸分析,只維護(hù)少量對(duì)象

    /*** 棧上分配測(cè)試* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails*/ public class StackAllocation {public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {alloc();}// 查看執(zhí)行時(shí)間long end = System.currentTimeMillis();System.out.println("花費(fèi)的時(shí)間為: " + (end - start) + " ms");// 為了方便查看堆內(nèi)存中對(duì)象個(gè)數(shù),線程sleeptry {Thread.sleep(1000000);} catch (InterruptedException e1) {e1.printStackTrace();}}private static void alloc() {User user = new User();//未發(fā)生逃逸}static class User {} }

    啟動(dòng)測(cè)試程序,這個(gè)時(shí)候程序中的線程處于睡眠狀態(tài),進(jìn)入cmd窗口中,通過(guò)jps和jmap來(lái)查看相應(yīng)的內(nèi)存分配情況。
    關(guān)閉逃逸分析結(jié)果如下:

    開啟逃逸分析結(jié)果如下:

    ② 同步省略

    ? 線程同步的代價(jià)是相當(dāng)高的,同步的后果是降低并發(fā)性和性能
    ? 在動(dòng)態(tài)編譯同步塊的時(shí)候,JIT編譯器可以借助逃逸分析來(lái)判斷同步塊所使用的鎖對(duì)象是否只能夠被一個(gè)線程訪問而沒有被發(fā)布到其他線程。如果沒有,那么JIT編譯器在編譯這個(gè)同步塊的時(shí)候就會(huì)取消對(duì)這部分代碼的同步。這樣就能大大提高并發(fā)性和性能。這個(gè)取消同步的過(guò)程就叫同步省略,也叫鎖消除

    /*** 同步省略說(shuō)明*/ public class SynchronizedTest {public void f() {Object hollis = new Object();synchronized(hollis) {System.out.println(hollis);}}//代碼中對(duì)hollis這個(gè)對(duì)象進(jìn)行加鎖,但是hollis對(duì)象的生命周期只在f()方法中//并不會(huì)被其他線程所訪問控制,所以在JIT編譯階段就會(huì)被優(yōu)化掉。//優(yōu)化為 ↓public void f2() {Object hollis = new Object();System.out.println(hollis);} }

    ③ 分離對(duì)象或標(biāo)量替換

    ? 標(biāo)量Scalar是指一個(gè)無(wú)法再分解成更小的數(shù)據(jù)的數(shù)據(jù)。Java中的原始數(shù)據(jù)類型就是標(biāo)量
    ? 相對(duì)的,那些還可以分解的數(shù)據(jù)叫做聚合量(Aggregate),Java中對(duì)象就是聚合量,因?yàn)樗梢苑纸獬善渌酆狭亢蜆?biāo)量
    ? 在JIT階段,如果經(jīng)過(guò)逃逸分析,發(fā)現(xiàn)一個(gè)對(duì)象不會(huì)被外界訪問的話,那么經(jīng)過(guò)JIT優(yōu)化,就會(huì)把這個(gè)對(duì)象拆解成若干個(gè)其中包含的若干個(gè)成員變量來(lái)替代。這個(gè)過(guò)程就是標(biāo)量替換

    public class ScalarTest {public static void main(String[] args) {alloc(); }public static void alloc(){Point point = new Point(1,2);} } class Point{private int x;private int y;public Point(int x,int y){this.x = x;this.y = y;} }

    以上代碼,經(jīng)過(guò)標(biāo)量替換后,就會(huì)變成:

    public static void alloc(){int x = 1;int y = 2; }

    可以看到,Point這個(gè)聚合量經(jīng)過(guò)逃逸分析后,發(fā)現(xiàn)他并沒有逃逸,就被替換成兩個(gè)標(biāo)量了。那么標(biāo)量替換有什么好處呢?就是可以大大減少堆內(nèi)存的占用。因?yàn)橐坏┎恍枰獎(jiǎng)?chuàng)建對(duì)象了,那么就不再需要分配堆內(nèi)存了。
    ???標(biāo)量替換為棧上分配提供了很好的基礎(chǔ)。

    測(cè)試代碼:

    /*** 標(biāo)量替換測(cè)試* -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations*/ public class ScalarReplace {public static class User {public int id;//標(biāo)量(無(wú)法再分解成更小的數(shù)據(jù))public String name;//聚合量(String還可以分解為char數(shù)組)}public static void alloc() {User u = new User();//未發(fā)生逃逸u.id = 5;u.name = "www.atguigu.com";}public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {alloc();}long end = System.currentTimeMillis();System.out.println("花費(fèi)的時(shí)間為: " + (end - start) + " ms");} }

    10.5 逃逸分析小結(jié)

    ? 關(guān)于逃逸分析的論文在1999年就已經(jīng)發(fā)表了,但直到JDK1.6才有實(shí)現(xiàn),而且這項(xiàng)技術(shù)到如今也并不是十分成熟的。
    ? 其根本原因就是無(wú)法保證逃逸分析的性能消耗一定能高于其他的消耗。雖然經(jīng)過(guò)逃逸分析可以做標(biāo)量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過(guò)程。
    ? 一個(gè)極端的例子,就是經(jīng)過(guò)逃逸分析之后,發(fā)現(xiàn)沒有一個(gè)對(duì)象是不逃逸的。那這個(gè)逃逸分析的過(guò)程就白白浪費(fèi)掉了。
    ? 雖然這項(xiàng)技術(shù)并不十分成熟,但是它也是即時(shí)編譯器優(yōu)化技術(shù)中一個(gè)十分重要的手段。
    ? 注意到有一些觀點(diǎn),認(rèn)為通過(guò)逃逸分析,JVM會(huì)在棧上分配那些不會(huì)逃逸的對(duì)象,這在理論上是可行的,但是取決于JVM設(shè)計(jì)者的選擇。Oracle HotspotJVM中并未這么做,這一點(diǎn)在逃逸分析相關(guān)的文檔里已經(jīng)說(shuō)明,所以可以明確所有的對(duì)象實(shí)例都是創(chuàng)建在堆上。
    ? 目前很多書籍還是基于JDK7以前的版本,JDK已經(jīng)發(fā)生了很大變化,intern字符串的緩存和靜態(tài)變量曾經(jīng)都被分配在永久代上,而永久代已經(jīng)被元數(shù)據(jù)區(qū)取代。但是,intern字符串緩存和靜態(tài)變量并不是被轉(zhuǎn)移到元數(shù)據(jù)區(qū),而是直接在堆上分配,所以這一點(diǎn)同樣符合前面一點(diǎn)的結(jié)論:對(duì)象實(shí)例都是分配在堆上。
    ? 年輕代是對(duì)象的誕生、生長(zhǎng)、消亡的區(qū)域,一個(gè)對(duì)象在這里產(chǎn)生、應(yīng)用、最后被垃圾回收器收集、結(jié)束生命
    ? 老年代防止長(zhǎng)生命周期對(duì)象,通常都是從Survivor區(qū)域篩選拷貝過(guò)來(lái)的Java對(duì)象。當(dāng)然,也有特殊情況,我們知道普通的對(duì)象會(huì)被分配在TLAB上,如果對(duì)象較大,JVM會(huì)試圖直接分配在Eden其他位置上;如果對(duì)象再大,完全無(wú)法在新生代找到足夠長(zhǎng)的連續(xù)空閑空間,JVM就會(huì)直接分配到老年代
    ? 當(dāng)GC只發(fā)生在年輕代中,回收年輕對(duì)象的行為被稱為MinorGC。當(dāng)GC發(fā)生在老年代時(shí)則被稱為MajorGC或者FullGC。一般的,MinorGC的發(fā)生頻率要比MajorGC高很多,即老年代中垃圾回收發(fā)生的頻率大大低于年輕代。

    總結(jié)

    以上是生活随笔為你收集整理的运行数据区②---堆的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。