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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)

發(fā)布時間:2024/9/27 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

3.JVM內(nèi)存分配
3.1.內(nèi)存分配概述
3.2.內(nèi)存分配–Eden區(qū)域
3.3.內(nèi)存分配–大對象直接進老年代
3.3.1.背景
3.3.2.解析
3.4.內(nèi)存分配–長期存活的對象進去老年代
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)勢分析
3.6.3.1.同步消除
3.6.4.標量替換
3.6.5.什么情況下會發(fā)生逃逸?
3.7.直接內(nèi)存
3.8.Java內(nèi)存區(qū)域-直接內(nèi)存和運行時常量池
3.8.1.運行時常量池簡介
3.8.2.Class文件中的信息常量池
3.8.3.常量池的好處
3.8.4.基本類型的包裝類和常量池
3.9.對象在內(nèi)存中的布局-對象的創(chuàng)建
3.10.探究對象的結(jié)構(gòu)
3.11.深度理解對象的訪問定位
3.12.Java對象訪問方式
3.12.1.通過句柄訪問
3.12.2.通過直接指針訪問
3.13.對象分配內(nèi)存的策略
3.13.1.線程安全問題
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.垃圾回收-判斷對象是否存活算法-引用計數(shù)法詳解
3.15.垃圾回收-判斷對象是否存活算法-可達性分析法詳解
3.15.1.可達性分析算法
3.15.2.finalize()方法最終判定對象是否存活
3.15.3.Java引用
3.15.3.1.強引用
3.15.3.2.軟引用
3.15.3.3.弱引用
3.15.3.4.虛引用
3.15.3.5.軟引用和弱引用進一步說明
3.15.3.6.虛引用進一步說明:

3.JVM內(nèi)存分配

3.1.內(nèi)存分配概述

3.1.1.優(yōu)先分配到eden

3.1.2.大對象直接分配到老年代

3.1.3.長期存活的對象分配到老年代

3.1.4.空間分配擔(dān)保

3.1.5.動態(tài)對象年齡判斷

Java對象所占用的內(nèi)存主要在堆上實現(xiàn),因為堆是線程共享的,因此在堆上分配內(nèi)存時需要進行加鎖,這就導(dǎo)致了創(chuàng)建對象的開銷比較大。當(dāng)堆上空間不足時,會觸發(fā)GC,如果GC后空間仍然不足,則會拋出OutOfMemory異常。

為了提升內(nèi)存分配效率,在年輕代的Eden區(qū)HotSpot虛擬機使用了兩種技術(shù)來加快內(nèi)存分配 ,分別是bump-the-pointerTLAB(Thread-Local Allocation Buffers)。由于Eden區(qū)是連續(xù)的,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個對象,在對象創(chuàng)建時,只需要檢查最后一個對象后面是否足夠的內(nèi)存即可,從而大大加快內(nèi)存分配速度;而對于TLAB技術(shù)是對于多線程而言的,它會為每個新創(chuàng)建的線程在新生代的Eden Space上分配一塊獨立的空間,這塊空間成為TLAB(Thread Local Allocation Buffer),其大小由JVM根據(jù)運行情況計算而得。通過XX:TLABWasteTargetPercent來設(shè)置其可占用的Eden Space的百分比,默認是1%。在TLAB上分配內(nèi)存不需要加鎖,一般JVM會優(yōu)先在TLAB上分配內(nèi)存,如果對象過大或者TLAB空間已經(jīng)用完,則仍然在堆上進行分配。因此,在編寫程序時,多個小對象比大的對象分配起來效率更高。可在啟動參數(shù)上增加-XX:+PrintTLAB來查看TLAB空間的使用情況。

對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Minor GC后存活了下來),則會被復(fù)制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發(fā)生的GC次數(shù)也比年輕代少。當(dāng)年老代內(nèi)存不足時,將執(zhí)行Major GC,也叫 Full GC。

可以使用**-XX:+UseAdaptiveSizePolicy**開關(guān)來控制是否采用動態(tài)控制策略,如果動態(tài)控制,則動態(tài)調(diào)整Java堆中各個區(qū)域的大小以及進入老年代的年齡。

如果對象比較大(比如長字符串或大數(shù)組),年輕代空間不足,則大對象會直接分配到老年代上(大對象可能觸發(fā)提前GC,應(yīng)少用,更應(yīng)避免使用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大于這個值的對象會直接分配在老年代上。

3.2.內(nèi)存分配–Eden區(qū)域

Java執(zhí)行的時候,默認使用parallel收集器。對象優(yōu)先到Eden中:
案例:
創(chuàng)建Main類:

package com.toto.jvm.demo;public class Main {public static void main(String[] args) {byte[] b1 = new byte[4 * 1024 * 1024];}}

在Eclipse中配置VM arguments參數(shù)(-verbose:gc -XX:+PrintGCDetails):

Eden是新生代上的一部分區(qū)域,當(dāng)運行上面的代碼的時候,GC日志輸出中可以看到優(yōu)先到Eden,輸出結(jié)果如下:

上面輸出ParOldGen,說明使用的parallel收集器。

使用serialGC的時候,打印的gc日志(-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC):

運行后輸出結(jié)果:

由于上面的b1分配的內(nèi)存是4 * 1024 * 1024 即4M
而上圖可以看到只有eden space 138816K,占比8%。可以得出結(jié)論:創(chuàng)建的對象優(yōu)先進入eden區(qū)域。

當(dāng)把b1變成200M時(即大對象):

說明:大對象直接分配到老年代。

再如案例:

package com.toto.jvm.demo;public class Main {public static void main(String[] args) {byte[] b1 = new byte[5 * 1024 * 1024];byte[] b2 = new byte[4 * 1024 * 1024];byte[] b3 = new byte[4 * 1024 * 1024];byte[] b4 = new byte[4 * 1024 * 1024];}}

修改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)存分配–大對象直接進老年代

3.3.1.背景

講到大對象主要指字符串和數(shù)組,虛擬機提供了一個-XX:PretenureSizeThreshold參數(shù),大于這個值的參數(shù)直接在老年代分配。

這樣做的目的是避免在Eden區(qū)和兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代采用復(fù)制算法)。

3.3.2.解析

有兩種情況,對象會直接分配到老年代:
?如果在新生代分配失敗且對象是一個不含任何對象引用的大數(shù)組,可被直接分配到老年代。通過在老年代的分配避免新生代的一次垃圾回收。
?XX:PretenureSizeThreshold=<字節(jié)大小>可以設(shè)分配到新生代分配內(nèi)存。任何比這個大的對象都不會嘗試在新生代分配,將在老年代分配內(nèi)存。
?PretenureSizeThreshold默認值是0,意味著任何對象都會現(xiàn)在新生代分配內(nèi)存。

案例:
設(shè)置虛擬機參數(shù):

-verbose:gc -XX:+PrintGCDetails -Xms2048M -Xmx2048M -Xmn1024M -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC

-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
上代碼:

package com.toto.jvm.demo2;import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean;public class Main {public static void main(String[] args) {//734003216byte[] array = new byte[700 * 1024 * 1024];for (MemoryPoolMXBean memoryPoolMXBean : ManagementFactory.getMemoryPoolMXBeans()) {System.out.println(memoryPoolMXBean.getName() + " 總量:" + memoryPoolMXBean.getUsage().getCommitted() + " 使用的內(nèi)存:" + memoryPoolMXBean.getUsage().getUsed());}}}

輸出結(jié)果:

當(dāng)把代碼改成:

package com.toto.jvm.demo2;import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean;public class Main {public static void main(String[] args) {//734003216byte[] array = new byte[900 * 1024 * 1024];for (MemoryPoolMXBean memoryPoolMXBean : ManagementFactory.getMemoryPoolMXBeans()) {System.out.println(memoryPoolMXBean.getName() + " 總量:" + memoryPoolMXBean.getUsage().getCommitted() + " 使用的內(nèi)存:" + memoryPoolMXBean.getUsage().getUsed());}}}

3.4.內(nèi)存分配–長期存活的對象進去老年代

用法: -XX:MaxTenuringThreshold=15
該參數(shù)主要是控制新生代需要經(jīng)歷多少次GC晉升到老年代中的最大閾值。在JVM中用4個bit存儲(放在對象頭中),(1111)所以其最大值是15。

但并非意味著,對象必須要經(jīng)歷15次YGC才會晉升到老年代中。例如,當(dāng)Survivor區(qū)空間不夠時,便會提前進入到老年代中,但這個次數(shù)一定不大于設(shè)置的最大閾值。

那么JVM到底是如何來計算S區(qū)對象晉升到Old區(qū)的呢?
首先介紹另一個重要的JVM參數(shù):
-XX:TargetSurvivorRatio:一個計算期望S區(qū)存活大小(Desired survivor size)的參數(shù)。默認值為50,即50%。
當(dāng)一個S區(qū)中所有的age對象的大小如果大于等于Desired survivor size,則重新計算threshold,以age和MaxTenuringThreshold兩者的最小值為準。

以一個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,默認SurvivorRatio為8,那么可以知道Eden區(qū)為40M,S0和S1為5M* * 可以在JVM啟動參數(shù)中加上-XX:+PrintTenuringDistribution,該參數(shù)可以輸出age的額外信息。*/ public class App {public static void main(String[] args) throws InterruptedException {// main方法作為主線程,變量不會被回收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時, 由于byte1和byte2的年齡經(jīng)過3次ygc后已經(jīng)達到3(-XX:MaxTenuringThreshold=3),// 所以會晉升到oldYGC(40);// ygc后, s0(from)/s1(to)的空間為0Thread.sleep(3000);// 達到TargetSurvivorRatio這個比例指定的值,即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時, 由于s區(qū)已經(jīng)占用達到了60%(-XX:TargetSurvivorRatio=60),// 所以會重新計算對象晉升的min(age, MaxTenuringThreshold) = 1YGC(40);Thread.sleep(3000);// 由于前一次ygc時算出age=1, 所以這一次再ygc時, byte4, byte5, byte6就要晉升到Old,// 而不需要等MaxTenuringThreshold這么多次, 此次ygc后, s0(from)/s1(to)的空間再次為0,// 對象全部晉升到oldYGC(40);Thread.sleep(3000);System.out.println("GC end!");}// 塞滿Eden區(qū),局部變量會被回收,作為觸發(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

============================================================================

另外的一篇文章的說明:
-XX:MaxTenuringThreshold設(shè)置的是年齡閾值,默認15(對象被復(fù)制的次數(shù))
JVM為每個對象定義了一個對象年齡(Age)計數(shù)器, 對象在Eden出生如果經(jīng)第一次Minor GC后仍然存活, 且能被Survivor容納的話, 將被移動到Survivor空間中, 并將年齡設(shè)為1. 以后對象在Survivor區(qū)中每熬過一次Minor GC年齡就+1. 當(dāng)增加到設(shè)置的閥值時將會晉升到老年代。

但有一個疑惑,為什么我設(shè)置-XX:MaxTenuringThreshold足夠大了防止大量對象進入老年區(qū),雖然進入老年區(qū)的對象減少了,但還是有?
因為如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半, 年齡大于或等于該年齡的對象就可以直接進入老年代。

3.5.內(nèi)存分配–空間分配擔(dān)保

主要使用的JVM參數(shù)配置是:-XX:HandlePromotionFailure,使用空間分配擔(dān)保的時候使用-XX:+HandlePromotionFailure,不使用分配擔(dān)保的時候使用-XX:-HandlePromotionFailure。

3.5.1.堆空間參數(shù)

官網(wǎng)地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
?-XX:+PrintFlagsInitial : 查看所有的參數(shù)默認初始值
?-XX:+PrintFlagsFinal: 查看所有的參數(shù)的最終值(可能會存在修改,不再是初始值)
?-Xms: 初始堆空間內(nèi)存(默認為物理內(nèi)存的1/64)
?-Xmx: 最大堆空間內(nèi)存(默認為物理內(nèi)存的1/4)
?-Xmn: 設(shè)置新生代的大小(初始值及最大值)。
?-XX:NewRatio: 配置新生代與老年代在堆結(jié)構(gòu)的占比。
?-XX:SurvivorRatio: 設(shè)置新生代中Eden和S0/S1空間的比例。
?-XX:MaxTenuringThreshold: 設(shè)置新生代垃圾的最大年齡。
?-XX:+PrintGCDetails: 輸出詳細的GC處理日志
?打印gc簡要信息:(1) -XX:+PrintGC (2) -verbose:gc
?-XX:HandlePromotionFailure: 是否設(shè)置空間分配擔(dān)保

3.5.2.-XX:HandlePromotionFailure

JDK7及以后這個參數(shù)就失效了。
只要老年代的連續(xù)空間大于新生代對象的總大小或者歷次晉升到老年代的對象的平均大小就進行MinorGC,否則FullGC

JDK7及以前這個參數(shù)的作用見下圖:

3.6.內(nèi)存分配–逃逸分析與棧上分配

3.6.1.逃逸分析

內(nèi)存逃逸主要是對象的動態(tài)作用域的改變而引起的,故而內(nèi)存逃逸的分析就是分析對象的動態(tài)作用域。
發(fā)生逃逸行為的情況分為兩種:方法逃逸和線程逃逸


逃逸是指在某個方法之內(nèi)創(chuàng)建的對象,除了在方法體之內(nèi)被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執(zhí)行完畢之后,該方法中創(chuàng)建的對象將無法將GC回收,由于其被其它變量引用。正常的方法調(diào)用中,方法體中創(chuàng)建的對象將在執(zhí)行完畢之后,將回收其中創(chuàng)建的對象;故由于無法回收,即成為逃逸。

如果對象發(fā)生逃逸,那會分配到堆中。(因為對象發(fā)生了逃逸,就代表這個對象可以被外部訪問,換句話說,就是可以共享,能共享數(shù)據(jù)的,無非就是堆或方法區(qū),這就是堆。)

如果對象沒發(fā)生逃逸,那會分配到棧中。(因為對象沒發(fā)生逃逸,那就代表這個對象不能外部訪問,換句話說,就是不可共享,這里就是棧。)

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() {// 實例引用,發(fā)生逃逸 test(this);}public void getInstance() {//對象的作用域只在當(dāng)前方法中有效,沒有發(fā)生逃逸Object obj1 = new Object();}}

運行java時傳遞jvm參數(shù)-XX:+DoEscapeAnalysis

棧上分配與逃逸分析的關(guān)系
進行逃逸分析之后,產(chǎn)生的后果是所有的對象都將由棧上分配,而非從JVM內(nèi)存模型中的堆來分配。

棧上分配可以提升代碼性能,降低在多線程情況下的鎖使用,但是會受限于其空間的大小。

分析找到未逃逸的變量,將變量類的實例化內(nèi)存直接在棧里分配(無需進入堆),分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對象也被回收。

能在方法內(nèi)創(chuàng)建對象,就不要再方法外創(chuàng)建對象。


1.什么是棧上分配?
棧上分配主要是指在java程序的執(zhí)行過程中,在方法體中聲明的變量以及創(chuàng)建的對象,將直接從該線程所使用的棧中分配空間。一般而言,創(chuàng)建對象都是從堆中來分配的,這里是指在棧上來分配空間給新創(chuàng)建的對象。

2.什么是逃逸?
逃逸是指在某個方法之內(nèi)創(chuàng)建的對象,除了在方法體之內(nèi)被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執(zhí)行完畢之后,該方法中創(chuàng)建的對象將無法被GC回收,由于其被其它變量引用。正常的方法調(diào)用中,方法體中創(chuàng)建的對象將在執(zhí)行完畢之后,將回收其中創(chuàng)建的對象;故由于無法回收,即成為逃逸。

3.6.1.1.方法逃逸

當(dāng)方法創(chuàng)建了一個對象之后,這個對象被外部方法所調(diào)用,這個時候方法運行結(jié)束要進行GC時,本該方法的對象被回收,卻發(fā)現(xiàn)該對象還存活著,沒法回收,則稱為"方法逃逸"
簡單來說:就是當(dāng)前方法創(chuàng)建的對象,本該是當(dāng)前方法的棧幀所管理,卻被調(diào)用方所使用,可以稱之為內(nèi)存逃逸。

3.6.1.2.線程分配

直接將對象進行返回出去,該對象很可能被外部線程所訪問,如:賦值給變量等,則稱為”線程逃逸”。

當(dāng)我們創(chuàng)建一個對象的時候,會立馬想到該對象是會存儲到堆空間中的,而垃圾回收機制會在堆空間中回收不再使用的對象,但是篩選可回收對象,還有整理對象都需要消耗時間,如果能夠通過逃逸分析確定某些對象不會逃出到方法外的話,那么就可以直接讓這個對象在棧空間分配內(nèi)存,這樣該對象會隨著方法的執(zhí)行完畢自動進行銷毀。

3.6.2.棧上分配

棧上分配主要是指在Java程序的執(zhí)行過程中,在方法體中聲明的變量以及創(chuàng)建的對象,將直接從該線程所使用的棧中分配空間。一般而言,創(chuàng)建對象都是從堆中來分配的,這里是指在棧上分配空間給新建的對象。

如果能夠證明一個對象,不會進行逃逸到方法或線程外的話,則可以對該變量進行優(yōu)化。

3.6.3.逃逸分析/棧上分配的優(yōu)勢分析

優(yōu)勢表現(xiàn)在以下兩個方面:
?消除同步:線程同步的代價是相當(dāng)高的,同步的后果是降低并發(fā)性和性能。逃逸分析可以判斷出某個對象是否始終只被一個線程訪問,如果只被一個線程訪問,那么對該對象的同步操作就可以轉(zhuǎn)化成沒有同步保護的操作,這樣就能大大提高并發(fā)程度和性能。
?矢量替代:逃逸分析方法如果發(fā)現(xiàn)對象的內(nèi)存存儲結(jié)構(gòu)不需要連續(xù)進行的話,就可以將對象的部分甚至全部保存在CPU寄存器內(nèi),這樣能大大提高訪問速度。
劣勢:
?棧上分配受限于棧的空間大小,一般自我迭代類的需求以及大的對象空間需求操作,將導(dǎo)致棧的內(nèi)存溢出;故只適用于一定范圍之內(nèi)的內(nèi)存范圍請求。

3.6.3.1.同步消除

線程同步本身比較耗時,若確定了一個變量不會逃逸出線程,無法被其他線程訪問到,那這個變量的讀寫就不會存在競爭,則可以消除對該對象的同步鎖。

3.6.4.標量替換

1、標量是指不可分割的量,如java中基本數(shù)據(jù)類型和引用類型,都不能夠再進一步分解,他們就可以成為稱為標量。
2、若一個數(shù)據(jù)可以繼續(xù)分解,那就稱之為聚合量,而對象就是典型的聚合量。
3、若逃逸分析證明一個對象不會逃逸出方法,不會被外部訪問,并且這個對象是可以被分解的,那程序在真正執(zhí)行的時候可能不創(chuàng)建這個對象,而是直接創(chuàng)建這個對象分解后的標量來代替。這樣就無需在對對象分配空間了,只在棧上為分解出的變量分配內(nèi)存即可。

注意:
逃逸分析是比較耗時的,所以性能未必提升很多,因為其耗時性,采用的算法都是不那么準確但是時間壓力相對較小的算法來完成的,這就可能導(dǎo)致效果不穩(wěn)定,要慎重。
由于HotSpot虛擬機目前的實現(xiàn)方法導(dǎo)致棧上分配實現(xiàn)起來比較復(fù)雜,所以HotSpot虛擬機中暫時還沒有這項優(yōu)化。

相關(guān)JVM參數(shù):
-XX:+DoEscapeAnalysis 開啟逃逸分析、
-XX:+PrintEscapeAnalysis 開啟逃逸分析后,可通過此參數(shù)查看分析結(jié)果。
-XX:+EliminateAllocations 開啟標量替換。
-XX:+EliminateLocks 開啟同步消除。
-XX:+PrintEliminateAllocations 開啟標量替換后,查看標量替換情況。

3.6.5.什么情況下會發(fā)生逃逸?

案例:

package com.toto.jvm.demo4;public class StackAllocation {public StackAllocation obj;/*** 方法返回StackAllocation對象,發(fā)生逃逸* @return*/public StackAllocation getInstance() {return obj == null ? new StackAllocation() : obj;}/*** 為成員屬性賦值,發(fā)生逃逸*/public void setObj() {this.obj = new StackAllocation();}/*** 對象的作用域僅在當(dāng)前方法中有效,沒有發(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)存和運行時常量池

3.8.1.運行時常量池簡介

運行時常量池(Runtime Constant Pool),它是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到常量池中

運行時常量是相對于常量來說的,它具備一個重要特征是:動態(tài)性。當(dāng)然,值相同的動態(tài)常量與我們通常說的常量只是來源不同,但是都是儲存在池內(nèi)同一塊內(nèi)存區(qū)域。Java語言并不要求常量一定只能在編譯期產(chǎn)生,運行期間也可能產(chǎn)生新的常量,這些常量被放在運行時常量池中。這里所說的常量包括:基本類型包裝類(包裝類不管理浮點型,整型只會管理-128到127)和String(也可以通過**String.intern()**方法可以強制將String放入常量池)

3.8.2.Class文件中的信息常量池

在Class文件結(jié)構(gòu)中,最頭的4個字節(jié)用于存儲Megic Number,用于確定一個文件是否能被JVM接受,再接著4個字節(jié)用于存儲版本號,前2個字節(jié)存儲次版本號,后2個存儲主版本號,再接著是用于存放常量的常量池,由于常量的數(shù)量是不固定的,所以常量池的入口放置一個U2類型的數(shù)據(jù)(constant_pool_count)存儲常量池容量計數(shù)值。

常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當(dāng)于Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:

?類和接口的全限定名
?字段名稱和描述符
?方法名稱和描述符

3.8.3.常量池的好處

常量池是為了避免頻繁的創(chuàng)建和銷毀對象而影響系統(tǒng)性能,其實現(xiàn)了對象的共享。例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。
?節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并,只占用一個空間。
?節(jié)省運行時間:比較字符串時,比equals()快。對于兩個引用變量,只用判斷引用是否相等,也就判斷實際值是否相等。

雙等號==的含義
?基本數(shù)據(jù)類型之間應(yīng)用雙等號,比較的是他們的數(shù)值。
?復(fù)合數(shù)據(jù)類型(類)之間應(yīng)用雙等號,比較的是他們在內(nèi)存中的存放地址。

3.8.4.基本類型的包裝類和常量池

java中基本類型的包裝類的大部分都實現(xiàn)了常量池技術(shù),即Byte,Short,Integer,Long,Character,Boolean。這5種包裝類默認創(chuàng)建了數(shù)值[-128, 127]的相應(yīng)類型的緩存數(shù)據(jù),但是超出此范圍仍然會去創(chuàng)建新的對象。兩種浮點數(shù)類型的包裝類Float,Double并沒有實現(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在編譯的時候會直接將代碼封裝成Integer i1 = Integer.valueOf(40); 從而使用常量池中的對象。
?Integer i4 = new Integer(40); 這種情況下會創(chuàng)建新的對象。
?語句i4 == i5 + i6,因此+這個操作符不適用于Integer對象,首先i5和i6進行自動拆箱操作,進行數(shù)值相加,即i4 == 40。然后Integer對象無法與數(shù)值進行直接比較,所以i4自動拆箱轉(zhuǎn)為int值40,最終這條語句轉(zhuǎn)為40 == 40進行數(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”是在常量池中拿對象,new String(“abcd”)是直接在堆內(nèi)存空間創(chuàng)建一個新的對象。只要使用new方法,便需要創(chuàng)建的對象
?連接表達式+,只有使用引號包含文本的方式創(chuàng)建的String對象之間使用”+”連接產(chǎn)生的新對象才會被加入常量池中
?對于字符串變量的”+”連接表達式,它所產(chǎn)生的新對象都不會被加入字符串池中,其屬于在運行時創(chuàng)建的字符串,具有獨立的內(nèi)存地址,所以不引用自同—String對象。

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) {// 將兩個常量用 + 連接對s進行初始化String s = A + B;String t = "abcd";if (s == t) {System.out.println("s等于t,它們是同一個對象");} else {System.out.println("s不等于t,它們不是同一個對象");}}} 輸出結(jié)果: s不等于t,它們不是同一個對象

解釋:
s不等于t,它們不是同一個對象。A和B雖然被定義為常量,但是它們都沒有馬上被賦值。在運算出s的值之前,他們何時被賦值,以及被賦予什么樣的值,都是個變量。因此A和B在賦值之前,性質(zhì)類似于一個變量。那么s就不能在編譯期被確定,而只能運行時被創(chuàng)建了。

4)String與常量池 - intern方法

package com.toto.jvm.demo6;public class Main {public static void main(String[] args) {String s1 = new String("計算機");String s2 = s1.intern();String s3 = "計算機";System.out.println("s1 == s2 ? " + (s1 == s2));System.out.println("s3 == s2 ? " + (s3 == s2));/*** 結(jié)果是:* s1 == s2 ? false* s3 == s2 ? true**/}}

解釋:
String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進入常量。

5)String與常量池 - 延伸

String s1 = new String(“xyz”); //創(chuàng)建了幾個對象?

解釋:
考慮類加載階段和實際執(zhí)行時。
?類加載對一個類只會進行一次。”xyz”在類加載時就已經(jīng)創(chuàng)建并駐留了(如果該類被加載之前已經(jīng)有”xyz”字符串被駐留過則不需要重新創(chuàng)建用于駐留的”xyz”實例)。駐留的字符串是放在全局共享的字符串常量池中的。
?在這段代碼后連續(xù)被運行的時候,”xyz”字面量對應(yīng)的String實例已經(jīng)固定了,不會再被重新創(chuàng)建。所以這段代碼將常量池中的對象復(fù)制一份放在到heap中,并且把heap中的這個對象的引用交給s1持有

這條語句創(chuàng)建了2個對象。

intern()會把值搬到運行時常量池中。它是一個native方法。
如果無法申請內(nèi)存,報:OutOfMemoryError

3.9.對象在內(nèi)存中的布局-對象的創(chuàng)建

對象創(chuàng)建 步驟
1、new類名
2、根據(jù)new的參數(shù)在常量池中定位一個類的符號引用。
3、如果沒有找到這個符號引用,說明類還沒加被加載,則進行類的加載、解析和初始化。
4、虛擬機為對象分配內(nèi)存(位于堆中)
5、將分配的內(nèi)存初始化為零值(不包括對象頭)
6、調(diào)用對象的方法。

3.10.探究對象的結(jié)構(gòu)


3.11.深度理解對象的訪問定位

已經(jīng)創(chuàng)建對象,如何找到對象呢?就涉及到訪問定位的問題

有兩種方式(百度一下):
1、使用句柄
2、直接指針

使用句柄池的用途

Hotsport使用直接尋址(直接指針)的方式定位

  • 到對象實例數(shù)據(jù)的指針
  • 到對象類型數(shù)據(jù)的指針

3.12.Java對象訪問方式

一般來說,一個Java的引用訪問涉及到3個內(nèi)存區(qū)域:JVM棧,堆,方法區(qū)。以最簡單的本地變量引用:Object objRef = new Object()為例:
?Object objRef表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數(shù)據(jù);
?new Object()作為實例對象數(shù)據(jù)存儲在堆中;
?堆中還記錄了能夠查詢到此Object對象的類型數(shù)據(jù)(接口、方法、field、對象類型等)的地址,實際的數(shù)據(jù)則存儲在方法區(qū)中;

在Java虛擬機規(guī)范中,只規(guī)定了指向?qū)ο蟮囊?#xff0c;對于通過reference類型引用訪問具體對象的方式并未做規(guī)定,不過目前主流的實現(xiàn)方式主要有兩種:

3.12.1.通過句柄訪問

通過句柄訪問的實現(xiàn)方式中,JVM堆中會劃分單獨一塊內(nèi)存區(qū)域作為句柄池,句柄池中存儲了對象實例數(shù)據(jù)(在堆中)和對象類型數(shù)據(jù)(在方法區(qū)中)的指針。這種實現(xiàn)方法由于用句柄表示地址,因此十分穩(wěn)定。

3.12.2.通過直接指針訪問

通過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區(qū)中的相應(yīng)類型數(shù)據(jù)。這種方法最大的優(yōu)勢是速度快,在HotSpot虛擬機中用的就是這種方式。

3.13.對象分配內(nèi)存的策略

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。

在類加載檢查通過后,接下來虛擬機將為新生對象分配內(nèi)存。對象所需內(nèi)存的大小在類加載完成后便可完全確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump thePointer)。如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(FreeList)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用空閑列表。

給對象分配的方式:
方式一:指針碰撞
方式二:空間列表

下面兩張圖可以解釋指針碰撞和空閑列表:
指針碰撞:

空間列表:

3.13.1.線程安全問題

1、實現(xiàn)線程同步,加鎖(但:執(zhí)行效率低)
2、本地線程分配緩沖TLAB: (每個線程分配一定一定的內(nèi)存)

3.13.1.1.本地線程分配緩沖----TLAB

TLAB是虛擬機在堆內(nèi)存的劃分出來的一塊專用空間,是線程專屬的。在TLAB啟動的情況下,在線程初始化時,虛擬機會為每個線程分配一塊TLAB空間,只給當(dāng)前線程使用,這樣每個線程都單獨擁有一個空間,如果需要分配內(nèi)存,就在自己的空間上分配,這樣就不存在競爭的情況,可以大大提升分配效率。

ps:這里說線程獨享的堆內(nèi)存,只是在“內(nèi)存分配”這個動作上是線程獨享的,至于在讀取、垃圾回收等動作上都是線程共享的。即是指其他線程可以在這個區(qū)域讀取、操作數(shù)據(jù),但是無法在這個區(qū)域中分配內(nèi)存。

3.13.1.2.TLAB生命周期

在分代收集的垃圾回收器中,TLAB是在eden區(qū)分配的。TLAB 是從堆上 Eden 區(qū)的分配的一塊線程本地私有內(nèi)存。線程初始化的時候,如果JVM 啟用了TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB 關(guān)閉),則會創(chuàng)建并初始化TLAB。同時,在GC 掃描對象發(fā)生之后,線程第一次嘗試分配對象的時候,也會創(chuàng)建并初始化TLAB

在TLAB已經(jīng)滿了或者接近于滿了的時候,TLAB可能會被釋放回Eden。GC掃描對象發(fā)生時,TLAB會被釋放回Eden。TLAB 的生命周期期望只存在于一個GC 掃描周期內(nèi)。在JVM中,一個 GC 掃描周期,就是一個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 的大小的最小值:通過MinTLABSize指定
TLAB 的大小的最大值:不同GC中有不同的最大值。例如G1 GC中,TLAB的最大值為大對象的大小,即是Region的一半;ZGC中的最大值為1/8的Region,在大部分情況下Shenandoah GC也是每個Region 大小的 8 分之一。對于其他的GC,則是int 數(shù)組的最大大小

TLAB空間大小的動態(tài)調(diào)整:
默認情況下:

-XX:ResizeTLAB

resize開關(guān)是默認開啟的,JVM可以對TLAB空間大小進行調(diào)整。

對象的慢分配
當(dāng)TLAB內(nèi)存充足時,分配新對象的方式稱為快分配。當(dāng)TLAB內(nèi)存不足,分配新對象的方式稱為“慢分配”。慢分配有兩種處理方式:
1、當(dāng)TLAB剩余內(nèi)存空間小于TLAB最大浪費空間時,丟棄當(dāng)前 TLAB 回歸 Eden,線程獲取新的 TLAB 分配對象。
2、當(dāng)TLAB剩余內(nèi)存空間大于TLAB最大浪費空間時,對象直接在Eden區(qū)分配內(nèi)存。

TLAB最大浪費空間
最大浪費空間是一個動態(tài)值,TLAB最大浪費空間初始值=TLAB大小/TLABRefillWasteFraction。TLABRefillWasteFraction默認為64,所以TLAB最大浪費空間初始值為TLAB大小的1/64。伴隨著每次慢分配,這個TLAB最大浪費空間會每次遞增 TLABWasteIncrement 大小的空間。

3.13.1.4.總結(jié)

TLAB流程總結(jié):



3.13.1.5.參數(shù)總結(jié)

參數(shù)名稱參數(shù)作用
UseTLAB是否啟用 TLAB,默認是啟用的。
ResizeTLABTLAB 是否是自適應(yīng)可變的,默認為是
TLABSize初始 TLAB 大小,單位是字節(jié) 。默認為0,0 就是不主動設(shè)置 TLAB 初始大小,而是通過 JVM 自己計算每一個線程的初始大小。例如:-XX:TLABSize=65536
MinTLABSize最小 TLAB 大小。單位是字節(jié),默認2048。例如-XX:MinTLABSize=4096
TLABRefillWasteFraction在一次 TLAB 再填充(refill)發(fā)生的時候,最大的 TLAB 浪費。默認為64,和TLAB最大浪費空間有關(guān)。TLAB最大浪費空間= TLAB大小/TLABRefillWasteFraction
TLABWasteIncrementTLAB 慢分配時允許的 TLAB 浪費增量.

參考:
https://blog.csdn.net/a1076067274/article/details/112969208

3.14.垃圾回收-判斷對象是否存活算法-引用計數(shù)法詳解

在對象中添加一個引用計數(shù)器,當(dāng)有地方引用這個對象的時候,引用計數(shù)器的值就+1,當(dāng)引用失效的時候,計數(shù)就減一

Java中一般不用:引用計數(shù)方法。

如何判斷垃圾如何回收。

查看gc信息的方式

案例:

創(chuàng)建循環(huán)引用方式:

斷掉右側(cè)的先:




Jdk8采用的并不是引用計數(shù)法,而是默認是:parallel垃圾回收即。

3.15.垃圾回收-判斷對象是否存活算法-可達性分析法詳解

3.15.1.可達性分析算法

在Java中,是通過可達性分析(Reachability Analysis)來判定對象是否存活的。該算法的基本思路就是通過一些被稱為引用鏈(GC Roots)的對象作為起點,從這些節(jié)點開始向下搜索,搜索走過的路徑被稱為(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節(jié)點到該節(jié)點不可達),則證明該對象是不可用的。

如上圖所示,object1~object4對GC Root都是可達的,說明不可被回收,object5和object6對GC Root節(jié)點不可達,說明其可以被回收。
在Java中,可作為GC Root的對象包括以下幾種:
?虛擬機棧(棧幀中的本地變量表)中引用的對象
?方法區(qū)中類靜態(tài)屬性所引用的對象
?方法區(qū)中常量所引用的對象
?本地方法棧中JNI(即一般說的Native方法)引用的對象

在堆里存放著幾乎多有的java對象實例,垃圾搜集器在對堆進行回收之前,第一件事情就是確定這些對象之中哪些還“存活”著(即通過任何途徑都無法使用的對象)。

3.15.2.finalize()方法最終判定對象是否存活

即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷再次標記過程。
標記的前提是對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈。
1. 第一次標記并進行一次篩選。
篩選的條件是此對象是否有必要執(zhí)行finalize()方法。
當(dāng)對象沒有覆蓋finalize方法,或者finzlize方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”,對象被回收。
2. 第二次標記
如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會被放置在一個名為:F-Queue的隊列之中,并在稍后由一條虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行。這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束。這樣做的原因是,如果一個對象finalize()方法中執(zhí)行緩慢,或者發(fā)生死循環(huán)(更極端的情況),將很可能會導(dǎo)致F-Queue隊列中的其他對象永久處于等待狀態(tài),甚至導(dǎo)致整個內(nèi)存回收系統(tǒng)崩潰。
Finalize()方法是對象脫逃死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規(guī)模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關(guān)聯(lián)即可,譬如把自己賦值給某個類變量或?qū)ο蟮某蓡T變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。

3.15.3.Java引用

從可達性算法中可以看出,判斷對象是否可達時,與“引用”有關(guān)。那么什么情況下可以說一個對象被引用,引用到底代表什么?
在JDK1.2之后,Java對引用的概念進行了擴充,可以將引用分為以下四類:

強引用(Strong Reference)
軟引用(Soft Reference)
弱引用(Weak Reference)
虛引用(Phantom Reference)

這四種引用從上到下,依次減弱

3.15.3.1.強引用

強引用就是指在程序代碼中普遍存在的,類似Object obj = new Object()這類似的引用,只要強引用在,垃圾搜集器永遠不會搜集被引用的對象。也就是說,寧愿出現(xiàn)內(nèi)存溢出,也不會回收這些對象。

3.15.3.2.軟引用

軟引用是用來描述一些有用但并不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。對于軟引用關(guān)聯(lián)著的對象,只有在內(nèi)存不足的時候JVM才會回收該對象。因此,這一點可以很好地用來解決OOM的問題,并且這個特性很適合用來實現(xiàn)緩存:比如網(wǎng)頁緩存、圖片緩存等。

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.弱引用

弱引用也是用來描述非必需對象的,當(dāng)JVM進行垃圾回收時,無論內(nèi)存是否充足,都會回收被弱引用關(guān)聯(lián)的對象。在java中,用java.lang.ref.WeakReference類來表示。下面是使用示例:

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進行垃圾回收System.out.println(sr.get());} }

3.15.3.4.虛引用

虛引用和前面的軟引用、弱引用不同,它并不影響對象的生命周期。在java中用java.lang.ref.PhantomReference類表示。如果一個對象與虛引用關(guān)聯(lián),則跟沒有引用與之關(guān)聯(lián)一樣,在任何時候都可能被垃圾回收器回收。
要注意的是,虛引用必須和引用隊列關(guān)聯(lián)使用,當(dāng)垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會把這個虛引用加入到與之 關(guān)聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是否已經(jīng)加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動。

import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue;public class Main {public static void main(String[] args) {ReferenceQueue<String> queue = new ReferenceQueue<String>();PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);System.out.println(pr.get());} }

3.15.3.5.軟引用和弱引用進一步說明

在SoftReference類中,有三個方法,兩個構(gòu)造方法和一個get方法(WekReference類似):

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方法用來獲取與軟引用關(guān)聯(lián)的對象的引用,如果該對象被回收了,則返回null。

在使用軟引用和弱引用的時候,我們可以顯示地通過System.gc()來通知JVM進行垃圾回收,但是要注意的是,雖然發(fā)出了通知,JVM不一定會立刻執(zhí)行,也就是說這句是無法確保此時JVM一定會進行垃圾回收的。

3.15.3.6.虛引用進一步說明:

虛引用中有一個構(gòu)造函數(shù),可以看出,其必須和一個引用隊列一起存在。get()方法永遠返回null,因為虛引用永遠不可達。

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、可达性分析算法(学习笔记)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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