3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)
3.JVM內(nèi)存分配
3.1.內(nèi)存分配概述
3.2.內(nèi)存分配–Eden區(qū)域
3.3.內(nèi)存分配–大對(duì)象直接進(jìn)老年代
3.3.1.背景
3.3.2.解析
3.4.內(nèi)存分配–長(zhǎng)期存活的對(duì)象進(jìn)去老年代
3.5.內(nèi)存分配–空間分配擔(dān)保
3.5.1.堆空間參數(shù)
3.5.2.-XX:HandlePromotionFailure
3.6.內(nèi)存分配–逃逸分析與棧上分配
3.6.1.逃逸分析
3.6.1.1.方法逃逸
3.6.1.2.線程分配
3.6.2.棧上分配
3.6.3.逃逸分析/棧上分配的優(yōu)勢(shì)分析
3.6.3.1.同步消除
3.6.4.標(biāo)量替換
3.6.5.什么情況下會(huì)發(fā)生逃逸?
3.7.直接內(nèi)存
3.8.Java內(nèi)存區(qū)域-直接內(nèi)存和運(yùn)行時(shí)常量池
3.8.1.運(yùn)行時(shí)常量池簡(jiǎn)介
3.8.2.Class文件中的信息常量池
3.8.3.常量池的好處
3.8.4.基本類(lèi)型的包裝類(lèi)和常量池
3.9.對(duì)象在內(nèi)存中的布局-對(duì)象的創(chuàng)建
3.10.探究對(duì)象的結(jié)構(gòu)
3.11.深度理解對(duì)象的訪問(wèn)定位
3.12.Java對(duì)象訪問(wèn)方式
3.12.1.通過(guò)句柄訪問(wèn)
3.12.2.通過(guò)直接指針訪問(wèn)
3.13.對(duì)象分配內(nèi)存的策略
3.13.1.線程安全問(wèn)題
3.13.1.1.本地線程分配緩沖----TLAB
3.13.1.2.TLAB生命周期
3.13.1.3.TLAB的大小
3.13.1.4.總結(jié)
3.13.1.5.參數(shù)總結(jié)
3.14.垃圾回收-判斷對(duì)象是否存活算法-引用計(jì)數(shù)法詳解
3.15.垃圾回收-判斷對(duì)象是否存活算法-可達(dá)性分析法詳解
3.15.1.可達(dá)性分析算法
3.15.2.finalize()方法最終判定對(duì)象是否存活
3.15.3.Java引用
3.15.3.1.強(qiáng)引用
3.15.3.2.軟引用
3.15.3.3.弱引用
3.15.3.4.虛引用
3.15.3.5.軟引用和弱引用進(jìn)一步說(shuō)明
3.15.3.6.虛引用進(jìn)一步說(shuō)明:
3.JVM內(nèi)存分配
3.1.內(nèi)存分配概述
3.1.1.優(yōu)先分配到eden
3.1.2.大對(duì)象直接分配到老年代
3.1.3.長(zhǎng)期存活的對(duì)象分配到老年代
3.1.4.空間分配擔(dān)保
3.1.5.動(dòng)態(tài)對(duì)象年齡判斷
Java對(duì)象所占用的內(nèi)存主要在堆上實(shí)現(xiàn),因?yàn)槎咽蔷€程共享的,因此在堆上分配內(nèi)存時(shí)需要進(jìn)行加鎖,這就導(dǎo)致了創(chuàng)建對(duì)象的開(kāi)銷(xiāo)比較大。當(dāng)堆上空間不足時(shí),會(huì)觸發(fā)GC,如果GC后空間仍然不足,則會(huì)拋出OutOfMemory異常。
為了提升內(nèi)存分配效率,在年輕代的Eden區(qū)HotSpot虛擬機(jī)使用了兩種技術(shù)來(lái)加快內(nèi)存分配 ,分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)。由于Eden區(qū)是連續(xù)的,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個(gè)對(duì)象,在對(duì)象創(chuàng)建時(shí),只需要檢查最后一個(gè)對(duì)象后面是否足夠的內(nèi)存即可,從而大大加快內(nèi)存分配速度;而對(duì)于TLAB技術(shù)是對(duì)于多線程而言的,它會(huì)為每個(gè)新創(chuàng)建的線程在新生代的Eden Space上分配一塊獨(dú)立的空間,這塊空間成為T(mén)LAB(Thread Local Allocation Buffer),其大小由JVM根據(jù)運(yùn)行情況計(jì)算而得。通過(guò)XX:TLABWasteTargetPercent來(lái)設(shè)置其可占用的Eden Space的百分比,默認(rèn)是1%。在TLAB上分配內(nèi)存不需要加鎖,一般JVM會(huì)優(yōu)先在TLAB上分配內(nèi)存,如果對(duì)象過(guò)大或者TLAB空間已經(jīng)用完,則仍然在堆上進(jìn)行分配。因此,在編寫(xiě)程序時(shí),多個(gè)小對(duì)象比大的對(duì)象分配起來(lái)效率更高??稍趩?dòng)參數(shù)上增加-XX:+PrintTLAB來(lái)查看TLAB空間的使用情況。
對(duì)象如果在年輕代存活了足夠長(zhǎng)的時(shí)間而沒(méi)有被清理掉(即在幾次Minor GC后存活了下來(lái)),則會(huì)被復(fù)制到年老代,年老代的空間一般比年輕代大,能存放更多的對(duì)象,在年老代上發(fā)生的GC次數(shù)也比年輕代少。當(dāng)年老代內(nèi)存不足時(shí),將執(zhí)行Major GC,也叫 Full GC。
可以使用**-XX:+UseAdaptiveSizePolicy**開(kāi)關(guān)來(lái)控制是否采用動(dòng)態(tài)控制策略,如果動(dòng)態(tài)控制,則動(dòng)態(tài)調(diào)整Java堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡。
如果對(duì)象比較大(比如長(zhǎng)字符串或大數(shù)組),年輕代空間不足,則大對(duì)象會(huì)直接分配到老年代上(大對(duì)象可能觸發(fā)提前GC,應(yīng)少用,更應(yīng)避免使用短命的大對(duì)象)。用-XX:PretenureSizeThreshold來(lái)控制直接升入老年代的對(duì)象大小,大于這個(gè)值的對(duì)象會(huì)直接分配在老年代上。
3.2.內(nèi)存分配–Eden區(qū)域
Java執(zhí)行的時(shí)候,默認(rèn)使用parallel收集器。對(duì)象優(yōu)先到Eden中:
案例:
創(chuàng)建Main類(lèi):
在Eclipse中配置VM arguments參數(shù)(-verbose:gc -XX:+PrintGCDetails):
Eden是新生代上的一部分區(qū)域,當(dāng)運(yùn)行上面的代碼的時(shí)候,GC日志輸出中可以看到優(yōu)先到Eden,輸出結(jié)果如下:
上面輸出ParOldGen,說(shuō)明使用的parallel收集器。
使用serialGC的時(shí)候,打印的gc日志(-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC):
運(yùn)行后輸出結(jié)果:
由于上面的b1分配的內(nèi)存是4 * 1024 * 1024 即4M
而上圖可以看到只有eden space 138816K,占比8%??梢缘贸鼋Y(jié)論:創(chuàng)建的對(duì)象優(yōu)先進(jìn)入eden區(qū)域。
當(dāng)把b1變成200M時(shí)(即大對(duì)象):
說(shuō)明:大對(duì)象直接分配到老年代。
再如案例:
修改VM參數(shù):
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8輸出結(jié)果:
-XX:SurvivorRatio=8表示Survivor是Eden的1/8。
3.3.內(nèi)存分配–大對(duì)象直接進(jìn)老年代
3.3.1.背景
講到大對(duì)象主要指字符串和數(shù)組,虛擬機(jī)提供了一個(gè)-XX:PretenureSizeThreshold參數(shù),大于這個(gè)值的參數(shù)直接在老年代分配。
這樣做的目的是避免在Eden區(qū)和兩個(gè)Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代采用復(fù)制算法)。
3.3.2.解析
有兩種情況,對(duì)象會(huì)直接分配到老年代:
?如果在新生代分配失敗且對(duì)象是一個(gè)不含任何對(duì)象引用的大數(shù)組,可被直接分配到老年代。通過(guò)在老年代的分配避免新生代的一次垃圾回收。
?XX:PretenureSizeThreshold=<字節(jié)大小>可以設(shè)分配到新生代分配內(nèi)存。任何比這個(gè)大的對(duì)象都不會(huì)嘗試在新生代分配,將在老年代分配內(nèi)存。
?PretenureSizeThreshold默認(rèn)值是0,意味著任何對(duì)象都會(huì)現(xiàn)在新生代分配內(nèi)存。
案例:
設(shè)置虛擬機(jī)參數(shù):
-Xms表示初始化堆內(nèi)存
-Xmx表示最大堆內(nèi)存
-Xmn表示新生代的內(nèi)存
-XX:SurvivorRatio=8表示新生代的Eden占8/10,S1和S2各占1/10
因此Eden的內(nèi)存大小為:0.8 * 1024 * 1024 * 1024字節(jié) 約為819 * 1024 * 1024
上代碼:
輸出結(jié)果:
當(dāng)把代碼改成:
3.4.內(nèi)存分配–長(zhǎng)期存活的對(duì)象進(jìn)去老年代
用法: -XX:MaxTenuringThreshold=15
該參數(shù)主要是控制新生代需要經(jīng)歷多少次GC晉升到老年代中的最大閾值。在JVM中用4個(gè)bit存儲(chǔ)(放在對(duì)象頭中),(1111)所以其最大值是15。
但并非意味著,對(duì)象必須要經(jīng)歷15次YGC才會(huì)晉升到老年代中。例如,當(dāng)Survivor區(qū)空間不夠時(shí),便會(huì)提前進(jìn)入到老年代中,但這個(gè)次數(shù)一定不大于設(shè)置的最大閾值。
那么JVM到底是如何來(lái)計(jì)算S區(qū)對(duì)象晉升到Old區(qū)的呢?
首先介紹另一個(gè)重要的JVM參數(shù):
-XX:TargetSurvivorRatio:一個(gè)計(jì)算期望S區(qū)存活大小(Desired survivor size)的參數(shù)。默認(rèn)值為50,即50%。
當(dāng)一個(gè)S區(qū)中所有的age對(duì)象的大小如果大于等于Desired survivor size,則重新計(jì)算threshold,以age和MaxTenuringThreshold兩者的最小值為準(zhǔn)。
以一個(gè)Demo為例。設(shè)置VM參數(shù)值:
-Xmx200M -Xmn50m -XX:TargetSurvivorRatio=60 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxTenuringThreshold=3 -XX:+PrintTenuringDistribution代碼:
package com.toto.jvm.demo3;/*** -Xmx200M -Xmn50m -XX:TargetSurvivorRatio=60 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxTenuringThreshold=3* 最小堆為50M,默認(rèn)SurvivorRatio為8,那么可以知道Eden區(qū)為40M,S0和S1為5M* * 可以在JVM啟動(dòng)參數(shù)中加上-XX:+PrintTenuringDistribution,該參數(shù)可以輸出age的額外信息。*/ public class App {public static void main(String[] args) throws InterruptedException {// main方法作為主線程,變量不會(huì)被回收byte[] byte1 = new byte[1 * 1024 * 1024];byte[] byte2 = new byte[1 * 1024 * 1024];YGC(40);Thread.sleep(3000);YGC(40);Thread.sleep(3000);YGC(40);Thread.sleep(3000);// 這次再ygc時(shí), 由于byte1和byte2的年齡經(jīng)過(guò)3次ygc后已經(jīng)達(dá)到3(-XX:MaxTenuringThreshold=3),// 所以會(huì)晉升到oldYGC(40);// ygc后, s0(from)/s1(to)的空間為0Thread.sleep(3000);// 達(dá)到TargetSurvivorRatio這個(gè)比例指定的值,即5M(S區(qū))*60%(TargetSurvivorRatio)=3M(Desired survivor size)byte[] byte4 = new byte[1 * 1024 * 1024];byte[] byte5 = new byte[1 * 1024 * 1024];byte[] byte6 = new byte[1 * 1024 * 1024];// 這次ygc時(shí), 由于s區(qū)已經(jīng)占用達(dá)到了60%(-XX:TargetSurvivorRatio=60),// 所以會(huì)重新計(jì)算對(duì)象晉升的min(age, MaxTenuringThreshold) = 1YGC(40);Thread.sleep(3000);// 由于前一次ygc時(shí)算出age=1, 所以這一次再ygc時(shí), byte4, byte5, byte6就要晉升到Old,// 而不需要等MaxTenuringThreshold這么多次, 此次ygc后, s0(from)/s1(to)的空間再次為0,// 對(duì)象全部晉升到oldYGC(40);Thread.sleep(3000);System.out.println("GC end!");}// 塞滿Eden區(qū),局部變量會(huì)被回收,作為觸發(fā)GC的小工具private static void YGC(int edenSize) {for (int i = 0; i < edenSize; i++) {byte[] byte1m = new byte[1 * 1024 * 1024];}}}輸出結(jié)果:
2021-05-03T10:53:15.791+0800: [GC (Allocation Failure) 2021-05-03T10:53:15.791+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 3 (max 3) - age 1: 2649352 bytes, 2649352 total : 40551K->2623K(46080K), 0.0022131 secs] 40551K->2623K(199680K), 0.0023103 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2021-05-03T10:53:18.797+0800: [GC (Allocation Failure) 2021-05-03T10:53:18.797+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 3 (max 3) - age 1: 168 bytes, 168 total - age 2: 2647416 bytes, 2647584 total : 43362K->2824K(46080K), 0.0025757 secs] 43362K->2824K(199680K), 0.0026316 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2021-05-03T10:53:21.805+0800: [GC (Allocation Failure) 2021-05-03T10:53:21.805+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 3 (max 3) - age 2: 168 bytes, 168 total - age 3: 2647416 bytes, 2647584 total : 43562K->2694K(46080K), 0.0009461 secs] 43562K->2694K(199680K), 0.0009973 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2021-05-03T10:53:24.808+0800: [GC (Allocation Failure) 2021-05-03T10:53:24.808+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 3 (max 3) - age 3: 168 bytes, 168 total : 43432K->104K(46080K), 0.0048805 secs] 43432K->2740K(199680K), 0.0049507 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2021-05-03T10:53:27.820+0800: [GC (Allocation Failure) 2021-05-03T10:53:27.821+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 1 (max 3) - age 1: 3145776 bytes, 3145776 total : 40842K->3072K(46080K), 0.0028666 secs] 43478K->5708K(199680K), 0.0030672 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2021-05-03T10:53:30.827+0800: [GC (Allocation Failure) 2021-05-03T10:53:30.827+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 3 (max 3) : 43811K->0K(46080K), 0.0033850 secs] 46447K->5708K(199680K), 0.0034430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] GC end! Heappar new generation total 46080K, used 13910K [0x00000000f3800000, 0x00000000f6a00000, 0x00000000f6a00000)eden space 40960K, 33% used [0x00000000f3800000, 0x00000000f45959a0, 0x00000000f6000000)from space 5120K, 0% used [0x00000000f6000000, 0x00000000f6000000, 0x00000000f6500000)to space 5120K, 0% used [0x00000000f6500000, 0x00000000f6500000, 0x00000000f6a00000)concurrent mark-sweep generation total 153600K, used 5708K [0x00000000f6a00000, 0x0000000100000000, 0x0000000100000000)Metaspace used 2595K, capacity 4486K, committed 4864K, reserved 1056768Kclass space used 288K, capacity 386K, committed 512K, reserved 1048576K============================================================================
另外的一篇文章的說(shuō)明:
-XX:MaxTenuringThreshold設(shè)置的是年齡閾值,默認(rèn)15(對(duì)象被復(fù)制的次數(shù))
JVM為每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器, 對(duì)象在Eden出生如果經(jīng)第一次Minor GC后仍然存活, 且能被Survivor容納的話, 將被移動(dòng)到Survivor空間中, 并將年齡設(shè)為1. 以后對(duì)象在Survivor區(qū)中每熬過(guò)一次Minor GC年齡就+1. 當(dāng)增加到設(shè)置的閥值時(shí)將會(huì)晉升到老年代。
但有一個(gè)疑惑,為什么我設(shè)置-XX:MaxTenuringThreshold足夠大了防止大量對(duì)象進(jìn)入老年區(qū),雖然進(jìn)入老年區(qū)的對(duì)象減少了,但還是有?
因?yàn)槿绻赟urvivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半, 年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代。
3.5.內(nèi)存分配–空間分配擔(dān)保
主要使用的JVM參數(shù)配置是:-XX:HandlePromotionFailure,使用空間分配擔(dān)保的時(shí)候使用-XX:+HandlePromotionFailure,不使用分配擔(dān)保的時(shí)候使用-XX:-HandlePromotionFailure。
3.5.1.堆空間參數(shù)
官網(wǎng)地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
?-XX:+PrintFlagsInitial : 查看所有的參數(shù)默認(rèn)初始值
?-XX:+PrintFlagsFinal: 查看所有的參數(shù)的最終值(可能會(huì)存在修改,不再是初始值)
?-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è)置新生代垃圾的最大年齡。
?-XX:+PrintGCDetails: 輸出詳細(xì)的GC處理日志
?打印gc簡(jiǎn)要信息:(1) -XX:+PrintGC (2) -verbose:gc
?-XX:HandlePromotionFailure: 是否設(shè)置空間分配擔(dān)保
3.5.2.-XX:HandlePromotionFailure
JDK7及以后這個(gè)參數(shù)就失效了。
只要老年代的連續(xù)空間大于新生代對(duì)象的總大小或者歷次晉升到老年代的對(duì)象的平均大小就進(jìn)行MinorGC,否則FullGC。
JDK7及以前這個(gè)參數(shù)的作用見(jiàn)下圖:
3.6.內(nèi)存分配–逃逸分析與棧上分配
3.6.1.逃逸分析
內(nèi)存逃逸主要是對(duì)象的動(dòng)態(tài)作用域的改變而引起的,故而內(nèi)存逃逸的分析就是分析對(duì)象的動(dòng)態(tài)作用域。
發(fā)生逃逸行為的情況分為兩種:方法逃逸和線程逃逸
逃逸是指在某個(gè)方法之內(nèi)創(chuàng)建的對(duì)象,除了在方法體之內(nèi)被引用之外,還在方法體之外被其它變量引用到;這樣帶來(lái)的后果是在該方法執(zhí)行完畢之后,該方法中創(chuàng)建的對(duì)象將無(wú)法將GC回收,由于其被其它變量引用。正常的方法調(diào)用中,方法體中創(chuàng)建的對(duì)象將在執(zhí)行完畢之后,將回收其中創(chuàng)建的對(duì)象;故由于無(wú)法回收,即成為逃逸。
如果對(duì)象發(fā)生逃逸,那會(huì)分配到堆中。(因?yàn)閷?duì)象發(fā)生了逃逸,就代表這個(gè)對(duì)象可以被外部訪問(wèn),換句話說(shuō),就是可以共享,能共享數(shù)據(jù)的,無(wú)非就是堆或方法區(qū),這就是堆。)
如果對(duì)象沒(méi)發(fā)生逃逸,那會(huì)分配到棧中。(因?yàn)閷?duì)象沒(méi)發(fā)生逃逸,那就代表這個(gè)對(duì)象不能外部訪問(wèn),換句話說(shuō),就是不可共享,這里就是棧。)
package com.toto.jvm.demo4;import jvm.test;public class Main {public static Object obj;public void globalVariableEscape() {// 給全局變量賦值,發(fā)生逃逸obj = new Object(); }public Object methodEscape() {// 方法返回值,發(fā)生逃逸return new Object(); }public void instanceEscape() {// 實(shí)例引用,發(fā)生逃逸 test(this);}public void getInstance() {//對(duì)象的作用域只在當(dāng)前方法中有效,沒(méi)有發(fā)生逃逸Object obj1 = new Object();}}運(yùn)行java時(shí)傳遞jvm參數(shù)-XX:+DoEscapeAnalysis
棧上分配與逃逸分析的關(guān)系
進(jìn)行逃逸分析之后,產(chǎn)生的后果是所有的對(duì)象都將由棧上分配,而非從JVM內(nèi)存模型中的堆來(lái)分配。
棧上分配可以提升代碼性能,降低在多線程情況下的鎖使用,但是會(huì)受限于其空間的大小。
分析找到未逃逸的變量,將變量類(lèi)的實(shí)例化內(nèi)存直接在棧里分配(無(wú)需進(jìn)入堆),分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對(duì)象也被回收。
能在方法內(nèi)創(chuàng)建對(duì)象,就不要再方法外創(chuàng)建對(duì)象。
1.什么是棧上分配?
棧上分配主要是指在java程序的執(zhí)行過(guò)程中,在方法體中聲明的變量以及創(chuàng)建的對(duì)象,將直接從該線程所使用的棧中分配空間。一般而言,創(chuàng)建對(duì)象都是從堆中來(lái)分配的,這里是指在棧上來(lái)分配空間給新創(chuàng)建的對(duì)象。
2.什么是逃逸?
逃逸是指在某個(gè)方法之內(nèi)創(chuàng)建的對(duì)象,除了在方法體之內(nèi)被引用之外,還在方法體之外被其它變量引用到;這樣帶來(lái)的后果是在該方法執(zhí)行完畢之后,該方法中創(chuàng)建的對(duì)象將無(wú)法被GC回收,由于其被其它變量引用。正常的方法調(diào)用中,方法體中創(chuàng)建的對(duì)象將在執(zhí)行完畢之后,將回收其中創(chuàng)建的對(duì)象;故由于無(wú)法回收,即成為逃逸。
3.6.1.1.方法逃逸
當(dāng)方法創(chuàng)建了一個(gè)對(duì)象之后,這個(gè)對(duì)象被外部方法所調(diào)用,這個(gè)時(shí)候方法運(yùn)行結(jié)束要進(jìn)行GC時(shí),本該方法的對(duì)象被回收,卻發(fā)現(xiàn)該對(duì)象還存活著,沒(méi)法回收,則稱(chēng)為"方法逃逸"
簡(jiǎn)單來(lái)說(shuō):就是當(dāng)前方法創(chuàng)建的對(duì)象,本該是當(dāng)前方法的棧幀所管理,卻被調(diào)用方所使用,可以稱(chēng)之為內(nèi)存逃逸。
3.6.1.2.線程分配
直接將對(duì)象進(jìn)行返回出去,該對(duì)象很可能被外部線程所訪問(wèn),如:賦值給變量等,則稱(chēng)為”線程逃逸”。
當(dāng)我們創(chuàng)建一個(gè)對(duì)象的時(shí)候,會(huì)立馬想到該對(duì)象是會(huì)存儲(chǔ)到堆空間中的,而垃圾回收機(jī)制會(huì)在堆空間中回收不再使用的對(duì)象,但是篩選可回收對(duì)象,還有整理對(duì)象都需要消耗時(shí)間,如果能夠通過(guò)逃逸分析確定某些對(duì)象不會(huì)逃出到方法外的話,那么就可以直接讓這個(gè)對(duì)象在??臻g分配內(nèi)存,這樣該對(duì)象會(huì)隨著方法的執(zhí)行完畢自動(dòng)進(jìn)行銷(xiāo)毀。
3.6.2.棧上分配
棧上分配主要是指在Java程序的執(zhí)行過(guò)程中,在方法體中聲明的變量以及創(chuàng)建的對(duì)象,將直接從該線程所使用的棧中分配空間。一般而言,創(chuàng)建對(duì)象都是從堆中來(lái)分配的,這里是指在棧上分配空間給新建的對(duì)象。
如果能夠證明一個(gè)對(duì)象,不會(huì)進(jìn)行逃逸到方法或線程外的話,則可以對(duì)該變量進(jìn)行優(yōu)化。
3.6.3.逃逸分析/棧上分配的優(yōu)勢(shì)分析
優(yōu)勢(shì)表現(xiàn)在以下兩個(gè)方面:
?消除同步:線程同步的代價(jià)是相當(dāng)高的,同步的后果是降低并發(fā)性和性能。逃逸分析可以判斷出某個(gè)對(duì)象是否始終只被一個(gè)線程訪問(wèn),如果只被一個(gè)線程訪問(wèn),那么對(duì)該對(duì)象的同步操作就可以轉(zhuǎn)化成沒(méi)有同步保護(hù)的操作,這樣就能大大提高并發(fā)程度和性能。
?矢量替代:逃逸分析方法如果發(fā)現(xiàn)對(duì)象的內(nèi)存存儲(chǔ)結(jié)構(gòu)不需要連續(xù)進(jìn)行的話,就可以將對(duì)象的部分甚至全部保存在CPU寄存器內(nèi),這樣能大大提高訪問(wèn)速度。
劣勢(shì):
?棧上分配受限于棧的空間大小,一般自我迭代類(lèi)的需求以及大的對(duì)象空間需求操作,將導(dǎo)致棧的內(nèi)存溢出;故只適用于一定范圍之內(nèi)的內(nèi)存范圍請(qǐng)求。
3.6.3.1.同步消除
線程同步本身比較耗時(shí),若確定了一個(gè)變量不會(huì)逃逸出線程,無(wú)法被其他線程訪問(wèn)到,那這個(gè)變量的讀寫(xiě)就不會(huì)存在競(jìng)爭(zhēng),則可以消除對(duì)該對(duì)象的同步鎖。
3.6.4.標(biāo)量替換
1、標(biāo)量是指不可分割的量,如java中基本數(shù)據(jù)類(lèi)型和引用類(lèi)型,都不能夠再進(jìn)一步分解,他們就可以成為稱(chēng)為標(biāo)量。
2、若一個(gè)數(shù)據(jù)可以繼續(xù)分解,那就稱(chēng)之為聚合量,而對(duì)象就是典型的聚合量。
3、若逃逸分析證明一個(gè)對(duì)象不會(huì)逃逸出方法,不會(huì)被外部訪問(wèn),并且這個(gè)對(duì)象是可以被分解的,那程序在真正執(zhí)行的時(shí)候可能不創(chuàng)建這個(gè)對(duì)象,而是直接創(chuàng)建這個(gè)對(duì)象分解后的標(biāo)量來(lái)代替。這樣就無(wú)需在對(duì)對(duì)象分配空間了,只在棧上為分解出的變量分配內(nèi)存即可。
注意:
逃逸分析是比較耗時(shí)的,所以性能未必提升很多,因?yàn)槠浜臅r(shí)性,采用的算法都是不那么準(zhǔn)確但是時(shí)間壓力相對(duì)較小的算法來(lái)完成的,這就可能導(dǎo)致效果不穩(wěn)定,要慎重。
由于HotSpot虛擬機(jī)目前的實(shí)現(xiàn)方法導(dǎo)致棧上分配實(shí)現(xiàn)起來(lái)比較復(fù)雜,所以HotSpot虛擬機(jī)中暫時(shí)還沒(méi)有這項(xiàng)優(yōu)化。
相關(guān)JVM參數(shù):
-XX:+DoEscapeAnalysis 開(kāi)啟逃逸分析、
-XX:+PrintEscapeAnalysis 開(kāi)啟逃逸分析后,可通過(guò)此參數(shù)查看分析結(jié)果。
-XX:+EliminateAllocations 開(kāi)啟標(biāo)量替換。
-XX:+EliminateLocks 開(kāi)啟同步消除。
-XX:+PrintEliminateAllocations 開(kāi)啟標(biāo)量替換后,查看標(biāo)量替換情況。
3.6.5.什么情況下會(huì)發(fā)生逃逸?
案例:
package com.toto.jvm.demo4;public class StackAllocation {public StackAllocation obj;/*** 方法返回StackAllocation對(duì)象,發(fā)生逃逸* @return*/public StackAllocation getInstance() {return obj == null ? new StackAllocation() : obj;}/*** 為成員屬性賦值,發(fā)生逃逸*/public void setObj() {this.obj = new StackAllocation();}/*** 對(duì)象的作用域僅在當(dāng)前方法中有效,沒(méi)有發(fā)生逃逸*/public void useStackAllocation() {StackAllocation s = new StackAllocation();}/*** 引用成員變量的值,發(fā)生逃逸*/public void useStackAllocation2() {StackAllocation s = getInstance();}}3.7.直接內(nèi)存
查看一下什么是直接內(nèi)存。
NIO中直接分配直接內(nèi)存。
3.8.Java內(nèi)存區(qū)域-直接內(nèi)存和運(yùn)行時(shí)常量池
3.8.1.運(yùn)行時(shí)常量池簡(jiǎn)介
運(yùn)行時(shí)常量池(Runtime Constant Pool),它是方法區(qū)的一部分。Class文件中除了有類(lèi)的版本、字段、方法、接口等描述等信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后存放到常量池中。
運(yùn)行時(shí)常量是相對(duì)于常量來(lái)說(shuō)的,它具備一個(gè)重要特征是:動(dòng)態(tài)性。當(dāng)然,值相同的動(dòng)態(tài)常量與我們通常說(shuō)的常量只是來(lái)源不同,但是都是儲(chǔ)存在池內(nèi)同一塊內(nèi)存區(qū)域。Java語(yǔ)言并不要求常量一定只能在編譯期產(chǎn)生,運(yùn)行期間也可能產(chǎn)生新的常量,這些常量被放在運(yùn)行時(shí)常量池中。這里所說(shuō)的常量包括:基本類(lèi)型包裝類(lèi)(包裝類(lèi)不管理浮點(diǎn)型,整型只會(huì)管理-128到127)和String(也可以通過(guò)**String.intern()**方法可以強(qiáng)制將String放入常量池)
3.8.2.Class文件中的信息常量池
在Class文件結(jié)構(gòu)中,最頭的4個(gè)字節(jié)用于存儲(chǔ)Megic Number,用于確定一個(gè)文件是否能被JVM接受,再接著4個(gè)字節(jié)用于存儲(chǔ)版本號(hào),前2個(gè)字節(jié)存儲(chǔ)次版本號(hào),后2個(gè)存儲(chǔ)主版本號(hào),再接著是用于存放常量的常量池,由于常量的數(shù)量是不固定的,所以常量池的入口放置一個(gè)U2類(lèi)型的數(shù)據(jù)(constant_pool_count)存儲(chǔ)常量池容量計(jì)數(shù)值。
常量池主要用于存放兩大類(lèi)常量:字面量(Literal)和符號(hào)引用量(Symbolic References),字面量相當(dāng)于Java語(yǔ)言層面常量的概念,如文本字符串,聲明為final的常量值等,符號(hào)引用則屬于編譯原理方面的概念,包括了如下三種類(lèi)型的常量:
?類(lèi)和接口的全限定名
?字段名稱(chēng)和描述符
?方法名稱(chēng)和描述符
3.8.3.常量池的好處
常量池是為了避免頻繁的創(chuàng)建和銷(xiāo)毀對(duì)象而影響系統(tǒng)性能,其實(shí)現(xiàn)了對(duì)象的共享。例如字符串常量池,在編譯階段就把所有的字符串文字放到一個(gè)常量池中。
?節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并,只占用一個(gè)空間。
?節(jié)省運(yùn)行時(shí)間:比較字符串時(shí),比equals()快。對(duì)于兩個(gè)引用變量,只用判斷引用是否相等,也就判斷實(shí)際值是否相等。
雙等號(hào)==的含義
?基本數(shù)據(jù)類(lèi)型之間應(yīng)用雙等號(hào),比較的是他們的數(shù)值。
?復(fù)合數(shù)據(jù)類(lèi)型(類(lèi))之間應(yīng)用雙等號(hào),比較的是他們?cè)趦?nèi)存中的存放地址。
3.8.4.基本類(lèi)型的包裝類(lèi)和常量池
java中基本類(lèi)型的包裝類(lèi)的大部分都實(shí)現(xiàn)了常量池技術(shù),即Byte,Short,Integer,Long,Character,Boolean。這5種包裝類(lèi)默認(rèn)創(chuàng)建了數(shù)值[-128, 127]的相應(yīng)類(lèi)型的緩存數(shù)據(jù),但是超出此范圍仍然會(huì)去創(chuàng)建新的對(duì)象。兩種浮點(diǎn)數(shù)類(lèi)型的包裝類(lèi)Float,Double并沒(méi)有實(shí)現(xiàn)常量池技術(shù)。
1)Integer與常量池
Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0);System.out.println("i1=i2 " + (i1 == i2)); System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); System.out.println("i1=i4 " + (i1 == i4)); System.out.println("i4=i5 " + (i4 == i5)); System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); System.out.println("40=i5+i6 " + (40 == i5 + i6));i1=i2 true i1=i2+i3 true i1=i4 false i4=i5 false i4=i5+i6 true 40=i5+i6 true解釋:
?Integer i1 = 40; java在編譯的時(shí)候會(huì)直接將代碼封裝成Integer i1 = Integer.valueOf(40); 從而使用常量池中的對(duì)象。
?Integer i4 = new Integer(40); 這種情況下會(huì)創(chuàng)建新的對(duì)象。
?語(yǔ)句i4 == i5 + i6,因此+這個(gè)操作符不適用于Integer對(duì)象,首先i5和i6進(jìn)行自動(dòng)拆箱操作,進(jìn)行數(shù)值相加,即i4 == 40。然后Integer對(duì)象無(wú)法與數(shù)值進(jìn)行直接比較,所以i4自動(dòng)拆箱轉(zhuǎn)為int值40,最終這條語(yǔ)句轉(zhuǎn)為40 == 40進(jìn)行數(shù)值比較。
2)String與常量池-普通方法賦值
String str1 = "abcd"; String str2 = new String("abcd"); System.out.println(str1==str2);//falseString str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing"; String str4 = str1 + str2; System.out.println("string" == "str" + "ing");// true System.out.println(str3 == str4);//falseString str5 = "string"; System.out.println(str3 == str5);//true解釋:
?“abcd”是在常量池中拿對(duì)象,new String(“abcd”)是直接在堆內(nèi)存空間創(chuàng)建一個(gè)新的對(duì)象。只要使用new方法,便需要?jiǎng)?chuàng)建的對(duì)象。
?連接表達(dá)式+,只有使用引號(hào)包含文本的方式創(chuàng)建的String對(duì)象之間使用”+”連接產(chǎn)生的新對(duì)象才會(huì)被加入常量池中。
?對(duì)于字符串變量的”+”連接表達(dá)式,它所產(chǎn)生的新對(duì)象都不會(huì)被加入字符串池中,其屬于在運(yùn)行時(shí)創(chuàng)建的字符串,具有獨(dú)立的內(nèi)存地址,所以不引用自同—String對(duì)象。
3)String與常量池-靜態(tài)方法賦值
package com.toto.jvm.demo5;public class Main {/** 常量A **/public static final String A;/** 常量B **/public static final String B;static {A = "ab";B = "cd";}public static void main(String[] args) {// 將兩個(gè)常量用 + 連接對(duì)s進(jìn)行初始化String s = A + B;String t = "abcd";if (s == t) {System.out.println("s等于t,它們是同一個(gè)對(duì)象");} else {System.out.println("s不等于t,它們不是同一個(gè)對(duì)象");}}} 輸出結(jié)果: s不等于t,它們不是同一個(gè)對(duì)象解釋:
s不等于t,它們不是同一個(gè)對(duì)象。A和B雖然被定義為常量,但是它們都沒(méi)有馬上被賦值。在運(yùn)算出s的值之前,他們何時(shí)被賦值,以及被賦予什么樣的值,都是個(gè)變量。因此A和B在賦值之前,性質(zhì)類(lèi)似于一個(gè)變量。那么s就不能在編譯期被確定,而只能運(yùn)行時(shí)被創(chuàng)建了。
4)String與常量池 - intern方法
package com.toto.jvm.demo6;public class Main {public static void main(String[] args) {String s1 = new String("計(jì)算機(jī)");String s2 = s1.intern();String s3 = "計(jì)算機(jī)";System.out.println("s1 == s2 ? " + (s1 == s2));System.out.println("s3 == s2 ? " + (s3 == s2));/*** 結(jié)果是:* s1 == s2 ? false* s3 == s2 ? true**/}}解釋:
String的intern()方法會(huì)查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒(méi)有則添加自己的字符串進(jìn)入常量。
5)String與常量池 - 延伸
String s1 = new String(“xyz”); //創(chuàng)建了幾個(gè)對(duì)象?解釋:
考慮類(lèi)加載階段和實(shí)際執(zhí)行時(shí)。
?類(lèi)加載對(duì)一個(gè)類(lèi)只會(huì)進(jìn)行一次。”xyz”在類(lèi)加載時(shí)就已經(jīng)創(chuàng)建并駐留了(如果該類(lèi)被加載之前已經(jīng)有”xyz”字符串被駐留過(guò)則不需要重新創(chuàng)建用于駐留的”xyz”實(shí)例)。駐留的字符串是放在全局共享的字符串常量池中的。
?在這段代碼后連續(xù)被運(yùn)行的時(shí)候,”xyz”字面量對(duì)應(yīng)的String實(shí)例已經(jīng)固定了,不會(huì)再被重新創(chuàng)建。所以這段代碼將常量池中的對(duì)象復(fù)制一份放在到heap中,并且把heap中的這個(gè)對(duì)象的引用交給s1持有。
這條語(yǔ)句創(chuàng)建了2個(gè)對(duì)象。
intern()會(huì)把值搬到運(yùn)行時(shí)常量池中。它是一個(gè)native方法。
如果無(wú)法申請(qǐng)內(nèi)存,報(bào):OutOfMemoryError
3.9.對(duì)象在內(nèi)存中的布局-對(duì)象的創(chuàng)建
對(duì)象創(chuàng)建 步驟
1、new類(lèi)名
2、根據(jù)new的參數(shù)在常量池中定位一個(gè)類(lèi)的符號(hào)引用。
3、如果沒(méi)有找到這個(gè)符號(hào)引用,說(shuō)明類(lèi)還沒(méi)加被加載,則進(jìn)行類(lèi)的加載、解析和初始化。
4、虛擬機(jī)為對(duì)象分配內(nèi)存(位于堆中)
5、將分配的內(nèi)存初始化為零值(不包括對(duì)象頭)
6、調(diào)用對(duì)象的方法。
3.10.探究對(duì)象的結(jié)構(gòu)
3.11.深度理解對(duì)象的訪問(wèn)定位
已經(jīng)創(chuàng)建對(duì)象,如何找到對(duì)象呢?就涉及到訪問(wèn)定位的問(wèn)題
有兩種方式(百度一下):
1、使用句柄
2、直接指針
使用句柄池的用途
Hotsport使用直接尋址(直接指針)的方式定位
- 到對(duì)象實(shí)例數(shù)據(jù)的指針
- 到對(duì)象類(lèi)型數(shù)據(jù)的指針
3.12.Java對(duì)象訪問(wèn)方式
一般來(lái)說(shuō),一個(gè)Java的引用訪問(wèn)涉及到3個(gè)內(nèi)存區(qū)域:JVM棧,堆,方法區(qū)。以最簡(jiǎn)單的本地變量引用:Object objRef = new Object()為例:
?Object objRef表示一個(gè)本地引用,存儲(chǔ)在JVM棧的本地變量表中,表示一個(gè)reference類(lèi)型數(shù)據(jù);
?new Object()作為實(shí)例對(duì)象數(shù)據(jù)存儲(chǔ)在堆中;
?堆中還記錄了能夠查詢(xún)到此Object對(duì)象的類(lèi)型數(shù)據(jù)(接口、方法、field、對(duì)象類(lèi)型等)的地址,實(shí)際的數(shù)據(jù)則存儲(chǔ)在方法區(qū)中;
在Java虛擬機(jī)規(guī)范中,只規(guī)定了指向?qū)ο蟮囊?#xff0c;對(duì)于通過(guò)reference類(lèi)型引用訪問(wèn)具體對(duì)象的方式并未做規(guī)定,不過(guò)目前主流的實(shí)現(xiàn)方式主要有兩種:
3.12.1.通過(guò)句柄訪問(wèn)
通過(guò)句柄訪問(wèn)的實(shí)現(xiàn)方式中,JVM堆中會(huì)劃分單獨(dú)一塊內(nèi)存區(qū)域作為句柄池,句柄池中存儲(chǔ)了對(duì)象實(shí)例數(shù)據(jù)(在堆中)和對(duì)象類(lèi)型數(shù)據(jù)(在方法區(qū)中)的指針。這種實(shí)現(xiàn)方法由于用句柄表示地址,因此十分穩(wěn)定。
3.12.2.通過(guò)直接指針訪問(wèn)
通過(guò)直接指針訪問(wèn)的方式中,reference中存儲(chǔ)的就是對(duì)象在堆中的實(shí)際地址,在堆中存儲(chǔ)的對(duì)象信息中包含了在方法區(qū)中的相應(yīng)類(lèi)型數(shù)據(jù)。這種方法最大的優(yōu)勢(shì)是速度快,在HotSpot虛擬機(jī)中用的就是這種方式。
3.13.對(duì)象分配內(nèi)存的策略
虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。
在類(lèi)加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類(lèi)加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來(lái)。假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱(chēng)為“指針碰撞”(Bump thePointer)。如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱(chēng)為“空閑列表”(FreeList)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時(shí),通常采用空閑列表。
給對(duì)象分配的方式:
方式一:指針碰撞
方式二:空間列表
下面兩張圖可以解釋指針碰撞和空閑列表:
指針碰撞:
空間列表:
3.13.1.線程安全問(wèn)題
1、實(shí)現(xiàn)線程同步,加鎖(但:執(zhí)行效率低)
2、本地線程分配緩沖TLAB: (每個(gè)線程分配一定一定的內(nèi)存)
3.13.1.1.本地線程分配緩沖----TLAB
TLAB是虛擬機(jī)在堆內(nèi)存的劃分出來(lái)的一塊專(zhuān)用空間,是線程專(zhuān)屬的。在TLAB啟動(dòng)的情況下,在線程初始化時(shí),虛擬機(jī)會(huì)為每個(gè)線程分配一塊TLAB空間,只給當(dāng)前線程使用,這樣每個(gè)線程都單獨(dú)擁有一個(gè)空間,如果需要分配內(nèi)存,就在自己的空間上分配,這樣就不存在競(jìng)爭(zhēng)的情況,可以大大提升分配效率。
ps:這里說(shuō)線程獨(dú)享的堆內(nèi)存,只是在“內(nèi)存分配”這個(gè)動(dòng)作上是線程獨(dú)享的,至于在讀取、垃圾回收等動(dòng)作上都是線程共享的。即是指其他線程可以在這個(gè)區(qū)域讀取、操作數(shù)據(jù),但是無(wú)法在這個(gè)區(qū)域中分配內(nèi)存。
3.13.1.2.TLAB生命周期
在分代收集的垃圾回收器中,TLAB是在eden區(qū)分配的。TLAB 是從堆上 Eden 區(qū)的分配的一塊線程本地私有內(nèi)存。線程初始化的時(shí)候,如果JVM 啟用了TLAB(默認(rèn)是啟用的, 可以通過(guò) -XX:-UseTLAB 關(guān)閉),則會(huì)創(chuàng)建并初始化TLAB。同時(shí),在GC 掃描對(duì)象發(fā)生之后,線程第一次嘗試分配對(duì)象的時(shí)候,也會(huì)創(chuàng)建并初始化TLAB。
在TLAB已經(jīng)滿了或者接近于滿了的時(shí)候,TLAB可能會(huì)被釋放回Eden。GC掃描對(duì)象發(fā)生時(shí),TLAB會(huì)被釋放回Eden。TLAB 的生命周期期望只存在于一個(gè)GC 掃描周期內(nèi)。在JVM中,一個(gè) GC 掃描周期,就是一個(gè)epoch。那么,可以知道,TLAB 內(nèi)分配內(nèi)存一定是線性分配的。
3.13.1.3.TLAB的大小
TLAB的初始大小可由參數(shù)-XX:TLABSize指定,若指定了TLAB的值,TLAB初始大小就是TLABSize。否則,TLAB大小為分配線程的平均值。
源碼地址:https://github.com/openjdk/jdk/blob/master/src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
TLAB 的大小的最小值:通過(guò)MinTLABSize指定
TLAB 的大小的最大值:不同GC中有不同的最大值。例如G1 GC中,TLAB的最大值為大對(duì)象的大小,即是Region的一半;ZGC中的最大值為1/8的Region,在大部分情況下Shenandoah GC也是每個(gè)Region 大小的 8 分之一。對(duì)于其他的GC,則是int 數(shù)組的最大大小。
TLAB空間大小的動(dòng)態(tài)調(diào)整:
默認(rèn)情況下:
resize開(kāi)關(guān)是默認(rèn)開(kāi)啟的,JVM可以對(duì)TLAB空間大小進(jìn)行調(diào)整。
對(duì)象的慢分配
當(dāng)TLAB內(nèi)存充足時(shí),分配新對(duì)象的方式稱(chēng)為快分配。當(dāng)TLAB內(nèi)存不足,分配新對(duì)象的方式稱(chēng)為“慢分配”。慢分配有兩種處理方式:
1、當(dāng)TLAB剩余內(nèi)存空間小于TLAB最大浪費(fèi)空間時(shí),丟棄當(dāng)前 TLAB 回歸 Eden,線程獲取新的 TLAB 分配對(duì)象。
2、當(dāng)TLAB剩余內(nèi)存空間大于TLAB最大浪費(fèi)空間時(shí),對(duì)象直接在Eden區(qū)分配內(nèi)存。
TLAB最大浪費(fèi)空間
最大浪費(fèi)空間是一個(gè)動(dòng)態(tài)值,TLAB最大浪費(fèi)空間初始值=TLAB大小/TLABRefillWasteFraction。TLABRefillWasteFraction默認(rèn)為64,所以TLAB最大浪費(fèi)空間初始值為T(mén)LAB大小的1/64。伴隨著每次慢分配,這個(gè)TLAB最大浪費(fèi)空間會(huì)每次遞增 TLABWasteIncrement 大小的空間。
3.13.1.4.總結(jié)
TLAB流程總結(jié):
3.13.1.5.參數(shù)總結(jié)
| UseTLAB | 是否啟用 TLAB,默認(rèn)是啟用的。 |
| ResizeTLAB | TLAB 是否是自適應(yīng)可變的,默認(rèn)為是 |
| TLABSize | 初始 TLAB 大小,單位是字節(jié) 。默認(rèn)為0,0 就是不主動(dòng)設(shè)置 TLAB 初始大小,而是通過(guò) JVM 自己計(jì)算每一個(gè)線程的初始大小。例如:-XX:TLABSize=65536 |
| MinTLABSize | 最小 TLAB 大小。單位是字節(jié),默認(rèn)2048。例如-XX:MinTLABSize=4096 |
| TLABRefillWasteFraction | 在一次 TLAB 再填充(refill)發(fā)生的時(shí)候,最大的 TLAB 浪費(fèi)。默認(rèn)為64,和TLAB最大浪費(fèi)空間有關(guān)。TLAB最大浪費(fèi)空間= TLAB大小/TLABRefillWasteFraction |
| TLABWasteIncrement | TLAB 慢分配時(shí)允許的 TLAB 浪費(fèi)增量. |
參考:
https://blog.csdn.net/a1076067274/article/details/112969208
3.14.垃圾回收-判斷對(duì)象是否存活算法-引用計(jì)數(shù)法詳解
在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,當(dāng)有地方引用這個(gè)對(duì)象的時(shí)候,引用計(jì)數(shù)器的值就+1,當(dāng)引用失效的時(shí)候,計(jì)數(shù)就減一
Java中一般不用:引用計(jì)數(shù)方法。
如何判斷垃圾如何回收。
查看gc信息的方式
案例:
創(chuàng)建循環(huán)引用方式:
斷掉右側(cè)的先:
Jdk8采用的并不是引用計(jì)數(shù)法,而是默認(rèn)是:parallel垃圾回收即。
3.15.垃圾回收-判斷對(duì)象是否存活算法-可達(dá)性分析法詳解
3.15.1.可達(dá)性分析算法
在Java中,是通過(guò)可達(dá)性分析(Reachability Analysis)來(lái)判定對(duì)象是否存活的。該算法的基本思路就是通過(guò)一些被稱(chēng)為引用鏈(GC Roots)的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索走過(guò)的路徑被稱(chēng)為(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)(即從GC Roots節(jié)點(diǎn)到該節(jié)點(diǎn)不可達(dá)),則證明該對(duì)象是不可用的。
如上圖所示,object1~object4對(duì)GC Root都是可達(dá)的,說(shuō)明不可被回收,object5和object6對(duì)GC Root節(jié)點(diǎn)不可達(dá),說(shuō)明其可以被回收。
在Java中,可作為GC Root的對(duì)象包括以下幾種:
?虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
?方法區(qū)中類(lèi)靜態(tài)屬性所引用的對(duì)象
?方法區(qū)中常量所引用的對(duì)象
?本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象
在堆里存放著幾乎多有的java對(duì)象實(shí)例,垃圾搜集器在對(duì)堆進(jìn)行回收之前,第一件事情就是確定這些對(duì)象之中哪些還“存活”著(即通過(guò)任何途徑都無(wú)法使用的對(duì)象)。
3.15.2.finalize()方法最終判定對(duì)象是否存活
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷再次標(biāo)記過(guò)程。
標(biāo)記的前提是對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)有與GC Roots相連接的引用鏈。
1. 第一次標(biāo)記并進(jìn)行一次篩選。
篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。
當(dāng)對(duì)象沒(méi)有覆蓋finalize方法,或者finzlize方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”,對(duì)象被回收。
2. 第二次標(biāo)記
如果這個(gè)對(duì)象被判定為有必要執(zhí)行finalize()方法,那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為:F-Queue的隊(duì)列之中,并在稍后由一條虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行。這里所謂的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法,但并不承諾會(huì)等待它運(yùn)行結(jié)束。這樣做的原因是,如果一個(gè)對(duì)象finalize()方法中執(zhí)行緩慢,或者發(fā)生死循環(huán)(更極端的情況),將很可能會(huì)導(dǎo)致F-Queue隊(duì)列中的其他對(duì)象永久處于等待狀態(tài),甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。
Finalize()方法是對(duì)象脫逃死亡命運(yùn)的最后一次機(jī)會(huì),稍后GC將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模標(biāo)記,如果對(duì)象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個(gè)對(duì)象建立關(guān)聯(lián)即可,譬如把自己賦值給某個(gè)類(lèi)變量或?qū)ο蟮某蓡T變量,那在第二次標(biāo)記時(shí)它將移除出“即將回收”的集合。如果對(duì)象這時(shí)候還沒(méi)逃脫,那基本上它就真的被回收了。
3.15.3.Java引用
從可達(dá)性算法中可以看出,判斷對(duì)象是否可達(dá)時(shí),與“引用”有關(guān)。那么什么情況下可以說(shuō)一個(gè)對(duì)象被引用,引用到底代表什么?
在JDK1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,可以將引用分為以下四類(lèi):
強(qiáng)引用(Strong Reference)
軟引用(Soft Reference)
弱引用(Weak Reference)
虛引用(Phantom Reference)
這四種引用從上到下,依次減弱
3.15.3.1.強(qiáng)引用
強(qiáng)引用就是指在程序代碼中普遍存在的,類(lèi)似Object obj = new Object()這類(lèi)似的引用,只要強(qiáng)引用在,垃圾搜集器永遠(yuǎn)不會(huì)搜集被引用的對(duì)象。也就是說(shuō),寧愿出現(xiàn)內(nèi)存溢出,也不會(huì)回收這些對(duì)象。
3.15.3.2.軟引用
軟引用是用來(lái)描述一些有用但并不是必需的對(duì)象,在Java中用java.lang.ref.SoftReference類(lèi)來(lái)表示。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,只有在內(nèi)存不足的時(shí)候JVM才會(huì)回收該對(duì)象。因此,這一點(diǎn)可以很好地用來(lái)解決OOM的問(wèn)題,并且這個(gè)特性很適合用來(lái)實(shí)現(xiàn)緩存:比如網(wǎng)頁(yè)緩存、圖片緩存等。
import java.lang.ref.SoftReference;public class Main {public static void main(String[] args) {SoftReference<String> sr = new SoftReference<String>(new String("hello"));System.out.println(sr.get());} }3.15.3.3.弱引用
弱引用也是用來(lái)描述非必需對(duì)象的,當(dāng)JVM進(jìn)行垃圾回收時(shí),無(wú)論內(nèi)存是否充足,都會(huì)回收被弱引用關(guān)聯(lián)的對(duì)象。在java中,用java.lang.ref.WeakReference類(lèi)來(lái)表示。下面是使用示例:
import java.lang.ref.WeakReference;public class Main {public static void main(String[] args) {WeakReference<String> sr = new WeakReference<String>(new String("hello"));System.out.println(sr.get());System.gc(); //通知JVM的gc進(jìn)行垃圾回收System.out.println(sr.get());} }3.15.3.4.虛引用
虛引用和前面的軟引用、弱引用不同,它并不影響對(duì)象的生命周期。在java中用java.lang.ref.PhantomReference類(lèi)表示。如果一個(gè)對(duì)象與虛引用關(guān)聯(lián),則跟沒(méi)有引用與之關(guān)聯(lián)一樣,在任何時(shí)候都可能被垃圾回收器回收。
要注意的是,虛引用必須和引用隊(duì)列關(guān)聯(lián)使用,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí),如果發(fā)現(xiàn)它還有虛引用,就會(huì)把這個(gè)虛引用加入到與之 關(guān)聯(lián)的引用隊(duì)列中。程序可以通過(guò)判斷引用隊(duì)列中是否已經(jīng)加入了虛引用,來(lái)了解被引用的對(duì)象是否將要被垃圾回收。如果程序發(fā)現(xiàn)某個(gè)虛引用已經(jīng)被加入到引用隊(duì)列,那么就可以在所引用的對(duì)象的內(nèi)存被回收之前采取必要的行動(dòng)。
3.15.3.5.軟引用和弱引用進(jìn)一步說(shuō)明
在SoftReference類(lèi)中,有三個(gè)方法,兩個(gè)構(gòu)造方法和一個(gè)get方法(WekReference類(lèi)似):
public class SoftReference<T> extends Reference<T> {/*** Timestamp clock, updated by the garbage collector*/static private long clock;/*** Timestamp updated by each invocation of the get method. The VM may use* this field when selecting soft references to be cleared, but it is not* required to do so.*/private long timestamp;/*** Creates a new soft reference that refers to the given object. The new* reference is not registered with any queue.** @param referent object the new soft reference will refer to*/public SoftReference(T referent) {super(referent);this.timestamp = clock;}/*** Creates a new soft reference that refers to the given object and is* registered with the given queue.** @param referent object the new soft reference will refer to* @param q the queue with which the reference is to be registered,* or <tt>null</tt> if registration is not required**/public SoftReference(T referent, ReferenceQueue<? super T> q) {super(referent, q);this.timestamp = clock;}/*** Returns this reference object's referent. If this reference object has* been cleared, either by the program or by the garbage collector, then* this method returns <code>null</code>.** @return The object to which this reference refers, or* <code>null</code> if this reference object has been cleared*/public T get() {T o = super.get();if (o != null && this.timestamp != clock)this.timestamp = clock;return o;}}get方法用來(lái)獲取與軟引用關(guān)聯(lián)的對(duì)象的引用,如果該對(duì)象被回收了,則返回null。
在使用軟引用和弱引用的時(shí)候,我們可以顯示地通過(guò)System.gc()來(lái)通知JVM進(jìn)行垃圾回收,但是要注意的是,雖然發(fā)出了通知,JVM不一定會(huì)立刻執(zhí)行,也就是說(shuō)這句是無(wú)法確保此時(shí)JVM一定會(huì)進(jìn)行垃圾回收的。
3.15.3.6.虛引用進(jìn)一步說(shuō)明:
虛引用中有一個(gè)構(gòu)造函數(shù),可以看出,其必須和一個(gè)引用隊(duì)列一起存在。get()方法永遠(yuǎn)返回null,因?yàn)樘撘糜肋h(yuǎn)不可達(dá)。
public class PhantomReference<T> extends Reference<T> {/*** Returns this reference object's referent. Because the referent of a* phantom reference is always inaccessible, this method always returns* <code>null</code>.** @return <code>null</code>*/public T get() {return null;}/*** Creates a new phantom reference that refers to the given object and* is registered with the given queue.** <p> It is possible to create a phantom reference with a <tt>null</tt>* queue, but such a reference is completely useless: Its <tt>get</tt>* method will always return null and, since it does not have a queue, it* will never be enqueued.** @param referent the object the new phantom reference will refer to* @param q the queue with which the reference is to be registered,* or <tt>null</tt> if registration is not required*/public PhantomReference(T referent, ReferenceQueue<? super T> q) {super(referent, q);} }總結(jié)
以上是生活随笔為你收集整理的3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 直销银行什么意思
- 下一篇: 4、JVM垃圾回收机制、新生代的GC、G