3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)
3.JVM內存分配
3.1.內存分配概述
3.2.內存分配–Eden區域
3.3.內存分配–大對象直接進老年代
3.3.1.背景
3.3.2.解析
3.4.內存分配–長期存活的對象進去老年代
3.5.內存分配–空間分配擔保
3.5.1.堆空間參數
3.5.2.-XX:HandlePromotionFailure
3.6.內存分配–逃逸分析與棧上分配
3.6.1.逃逸分析
3.6.1.1.方法逃逸
3.6.1.2.線程分配
3.6.2.棧上分配
3.6.3.逃逸分析/棧上分配的優勢分析
3.6.3.1.同步消除
3.6.4.標量替換
3.6.5.什么情況下會發生逃逸?
3.7.直接內存
3.8.Java內存區域-直接內存和運行時常量池
3.8.1.運行時常量池簡介
3.8.2.Class文件中的信息常量池
3.8.3.常量池的好處
3.8.4.基本類型的包裝類和常量池
3.9.對象在內存中的布局-對象的創建
3.10.探究對象的結構
3.11.深度理解對象的訪問定位
3.12.Java對象訪問方式
3.12.1.通過句柄訪問
3.12.2.通過直接指針訪問
3.13.對象分配內存的策略
3.13.1.線程安全問題
3.13.1.1.本地線程分配緩沖----TLAB
3.13.1.2.TLAB生命周期
3.13.1.3.TLAB的大小
3.13.1.4.總結
3.13.1.5.參數總結
3.14.垃圾回收-判斷對象是否存活算法-引用計數法詳解
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內存分配
3.1.內存分配概述
3.1.1.優先分配到eden
3.1.2.大對象直接分配到老年代
3.1.3.長期存活的對象分配到老年代
3.1.4.空間分配擔保
3.1.5.動態對象年齡判斷
Java對象所占用的內存主要在堆上實現,因為堆是線程共享的,因此在堆上分配內存時需要進行加鎖,這就導致了創建對象的開銷比較大。當堆上空間不足時,會觸發GC,如果GC后空間仍然不足,則會拋出OutOfMemory異常。
為了提升內存分配效率,在年輕代的Eden區HotSpot虛擬機使用了兩種技術來加快內存分配 ,分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers)。由于Eden區是連續的,因此bump-the-pointer技術的核心就是跟蹤最后創建的一個對象,在對象創建時,只需要檢查最后一個對象后面是否足夠的內存即可,從而大大加快內存分配速度;而對于TLAB技術是對于多線程而言的,它會為每個新創建的線程在新生代的Eden Space上分配一塊獨立的空間,這塊空間成為TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行情況計算而得。通過XX:TLABWasteTargetPercent來設置其可占用的Eden Space的百分比,默認是1%。在TLAB上分配內存不需要加鎖,一般JVM會優先在TLAB上分配內存,如果對象過大或者TLAB空間已經用完,則仍然在堆上進行分配。因此,在編寫程序時,多個小對象比大的對象分配起來效率更高。可在啟動參數上增加-XX:+PrintTLAB來查看TLAB空間的使用情況。
對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Minor GC后存活了下來),則會被復制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC。
可以使用**-XX:+UseAdaptiveSizePolicy**開關來控制是否采用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。
如果對象比較大(比如長字符串或大數組),年輕代空間不足,則大對象會直接分配到老年代上(大對象可能觸發提前GC,應少用,更應避免使用短命的大對象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大于這個值的對象會直接分配在老年代上。
3.2.內存分配–Eden區域
Java執行的時候,默認使用parallel收集器。對象優先到Eden中:
案例:
創建Main類:
在Eclipse中配置VM arguments參數(-verbose:gc -XX:+PrintGCDetails):
Eden是新生代上的一部分區域,當運行上面的代碼的時候,GC日志輸出中可以看到優先到Eden,輸出結果如下:
上面輸出ParOldGen,說明使用的parallel收集器。
使用serialGC的時候,打印的gc日志(-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC):
運行后輸出結果:
由于上面的b1分配的內存是4 * 1024 * 1024 即4M
而上圖可以看到只有eden space 138816K,占比8%。可以得出結論:創建的對象優先進入eden區域。
當把b1變成200M時(即大對象):
說明:大對象直接分配到老年代。
再如案例:
修改VM參數:
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8輸出結果:
-XX:SurvivorRatio=8表示Survivor是Eden的1/8。
3.3.內存分配–大對象直接進老年代
3.3.1.背景
講到大對象主要指字符串和數組,虛擬機提供了一個-XX:PretenureSizeThreshold參數,大于這個值的參數直接在老年代分配。
這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存復制(新生代采用復制算法)。
3.3.2.解析
有兩種情況,對象會直接分配到老年代:
?如果在新生代分配失敗且對象是一個不含任何對象引用的大數組,可被直接分配到老年代。通過在老年代的分配避免新生代的一次垃圾回收。
?XX:PretenureSizeThreshold=<字節大小>可以設分配到新生代分配內存。任何比這個大的對象都不會嘗試在新生代分配,將在老年代分配內存。
?PretenureSizeThreshold默認值是0,意味著任何對象都會現在新生代分配內存。
案例:
設置虛擬機參數:
-Xms表示初始化堆內存
-Xmx表示最大堆內存
-Xmn表示新生代的內存
-XX:SurvivorRatio=8表示新生代的Eden占8/10,S1和S2各占1/10
因此Eden的內存大小為:0.8 * 1024 * 1024 * 1024字節 約為819 * 1024 * 1024
上代碼:
輸出結果:
當把代碼改成:
3.4.內存分配–長期存活的對象進去老年代
用法: -XX:MaxTenuringThreshold=15
該參數主要是控制新生代需要經歷多少次GC晉升到老年代中的最大閾值。在JVM中用4個bit存儲(放在對象頭中),(1111)所以其最大值是15。
但并非意味著,對象必須要經歷15次YGC才會晉升到老年代中。例如,當Survivor區空間不夠時,便會提前進入到老年代中,但這個次數一定不大于設置的最大閾值。
那么JVM到底是如何來計算S區對象晉升到Old區的呢?
首先介紹另一個重要的JVM參數:
-XX:TargetSurvivorRatio:一個計算期望S區存活大小(Desired survivor size)的參數。默認值為50,即50%。
當一個S區中所有的age對象的大小如果大于等于Desired survivor size,則重新計算threshold,以age和MaxTenuringThreshold兩者的最小值為準。
以一個Demo為例。設置VM參數值:
-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區為40M,S0和S1為5M* * 可以在JVM啟動參數中加上-XX:+PrintTenuringDistribution,該參數可以輸出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的年齡經過3次ygc后已經達到3(-XX:MaxTenuringThreshold=3),// 所以會晉升到oldYGC(40);// ygc后, s0(from)/s1(to)的空間為0Thread.sleep(3000);// 達到TargetSurvivorRatio這個比例指定的值,即5M(S區)*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區已經占用達到了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區,局部變量會被回收,作為觸發GC的小工具private static void YGC(int edenSize) {for (int i = 0; i < edenSize; i++) {byte[] byte1m = new byte[1 * 1024 * 1024];}}}輸出結果:
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設置的是年齡閾值,默認15(對象被復制的次數)
JVM為每個對象定義了一個對象年齡(Age)計數器, 對象在Eden出生如果經第一次Minor GC后仍然存活, 且能被Survivor容納的話, 將被移動到Survivor空間中, 并將年齡設為1. 以后對象在Survivor區中每熬過一次Minor GC年齡就+1. 當增加到設置的閥值時將會晉升到老年代。
但有一個疑惑,為什么我設置-XX:MaxTenuringThreshold足夠大了防止大量對象進入老年區,雖然進入老年區的對象減少了,但還是有?
因為如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半, 年齡大于或等于該年齡的對象就可以直接進入老年代。
3.5.內存分配–空間分配擔保
主要使用的JVM參數配置是:-XX:HandlePromotionFailure,使用空間分配擔保的時候使用-XX:+HandlePromotionFailure,不使用分配擔保的時候使用-XX:-HandlePromotionFailure。
3.5.1.堆空間參數
官網地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
?-XX:+PrintFlagsInitial : 查看所有的參數默認初始值
?-XX:+PrintFlagsFinal: 查看所有的參數的最終值(可能會存在修改,不再是初始值)
?-Xms: 初始堆空間內存(默認為物理內存的1/64)
?-Xmx: 最大堆空間內存(默認為物理內存的1/4)
?-Xmn: 設置新生代的大小(初始值及最大值)。
?-XX:NewRatio: 配置新生代與老年代在堆結構的占比。
?-XX:SurvivorRatio: 設置新生代中Eden和S0/S1空間的比例。
?-XX:MaxTenuringThreshold: 設置新生代垃圾的最大年齡。
?-XX:+PrintGCDetails: 輸出詳細的GC處理日志
?打印gc簡要信息:(1) -XX:+PrintGC (2) -verbose:gc
?-XX:HandlePromotionFailure: 是否設置空間分配擔保
3.5.2.-XX:HandlePromotionFailure
JDK7及以后這個參數就失效了。
只要老年代的連續空間大于新生代對象的總大小或者歷次晉升到老年代的對象的平均大小就進行MinorGC,否則FullGC。
JDK7及以前這個參數的作用見下圖:
3.6.內存分配–逃逸分析與棧上分配
3.6.1.逃逸分析
內存逃逸主要是對象的動態作用域的改變而引起的,故而內存逃逸的分析就是分析對象的動態作用域。
發生逃逸行為的情況分為兩種:方法逃逸和線程逃逸
逃逸是指在某個方法之內創建的對象,除了在方法體之內被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執行完畢之后,該方法中創建的對象將無法將GC回收,由于其被其它變量引用。正常的方法調用中,方法體中創建的對象將在執行完畢之后,將回收其中創建的對象;故由于無法回收,即成為逃逸。
如果對象發生逃逸,那會分配到堆中。(因為對象發生了逃逸,就代表這個對象可以被外部訪問,換句話說,就是可以共享,能共享數據的,無非就是堆或方法區,這就是堆。)
如果對象沒發生逃逸,那會分配到棧中。(因為對象沒發生逃逸,那就代表這個對象不能外部訪問,換句話說,就是不可共享,這里就是棧。)
package com.toto.jvm.demo4;import jvm.test;public class Main {public static Object obj;public void globalVariableEscape() {// 給全局變量賦值,發生逃逸obj = new Object(); }public Object methodEscape() {// 方法返回值,發生逃逸return new Object(); }public void instanceEscape() {// 實例引用,發生逃逸 test(this);}public void getInstance() {//對象的作用域只在當前方法中有效,沒有發生逃逸Object obj1 = new Object();}}運行java時傳遞jvm參數-XX:+DoEscapeAnalysis
棧上分配與逃逸分析的關系
進行逃逸分析之后,產生的后果是所有的對象都將由棧上分配,而非從JVM內存模型中的堆來分配。
棧上分配可以提升代碼性能,降低在多線程情況下的鎖使用,但是會受限于其空間的大小。
分析找到未逃逸的變量,將變量類的實例化內存直接在棧里分配(無需進入堆),分配完成后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。
能在方法內創建對象,就不要再方法外創建對象。
1.什么是棧上分配?
棧上分配主要是指在java程序的執行過程中,在方法體中聲明的變量以及創建的對象,將直接從該線程所使用的棧中分配空間。一般而言,創建對象都是從堆中來分配的,這里是指在棧上來分配空間給新創建的對象。
2.什么是逃逸?
逃逸是指在某個方法之內創建的對象,除了在方法體之內被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執行完畢之后,該方法中創建的對象將無法被GC回收,由于其被其它變量引用。正常的方法調用中,方法體中創建的對象將在執行完畢之后,將回收其中創建的對象;故由于無法回收,即成為逃逸。
3.6.1.1.方法逃逸
當方法創建了一個對象之后,這個對象被外部方法所調用,這個時候方法運行結束要進行GC時,本該方法的對象被回收,卻發現該對象還存活著,沒法回收,則稱為"方法逃逸"
簡單來說:就是當前方法創建的對象,本該是當前方法的棧幀所管理,卻被調用方所使用,可以稱之為內存逃逸。
3.6.1.2.線程分配
直接將對象進行返回出去,該對象很可能被外部線程所訪問,如:賦值給變量等,則稱為”線程逃逸”。
當我們創建一個對象的時候,會立馬想到該對象是會存儲到堆空間中的,而垃圾回收機制會在堆空間中回收不再使用的對象,但是篩選可回收對象,還有整理對象都需要消耗時間,如果能夠通過逃逸分析確定某些對象不會逃出到方法外的話,那么就可以直接讓這個對象在棧空間分配內存,這樣該對象會隨著方法的執行完畢自動進行銷毀。
3.6.2.棧上分配
棧上分配主要是指在Java程序的執行過程中,在方法體中聲明的變量以及創建的對象,將直接從該線程所使用的棧中分配空間。一般而言,創建對象都是從堆中來分配的,這里是指在棧上分配空間給新建的對象。
如果能夠證明一個對象,不會進行逃逸到方法或線程外的話,則可以對該變量進行優化。
3.6.3.逃逸分析/棧上分配的優勢分析
優勢表現在以下兩個方面:
?消除同步:線程同步的代價是相當高的,同步的后果是降低并發性和性能。逃逸分析可以判斷出某個對象是否始終只被一個線程訪問,如果只被一個線程訪問,那么對該對象的同步操作就可以轉化成沒有同步保護的操作,這樣就能大大提高并發程度和性能。
?矢量替代:逃逸分析方法如果發現對象的內存存儲結構不需要連續進行的話,就可以將對象的部分甚至全部保存在CPU寄存器內,這樣能大大提高訪問速度。
劣勢:
?棧上分配受限于棧的空間大小,一般自我迭代類的需求以及大的對象空間需求操作,將導致棧的內存溢出;故只適用于一定范圍之內的內存范圍請求。
3.6.3.1.同步消除
線程同步本身比較耗時,若確定了一個變量不會逃逸出線程,無法被其他線程訪問到,那這個變量的讀寫就不會存在競爭,則可以消除對該對象的同步鎖。
3.6.4.標量替換
1、標量是指不可分割的量,如java中基本數據類型和引用類型,都不能夠再進一步分解,他們就可以成為稱為標量。
2、若一個數據可以繼續分解,那就稱之為聚合量,而對象就是典型的聚合量。
3、若逃逸分析證明一個對象不會逃逸出方法,不會被外部訪問,并且這個對象是可以被分解的,那程序在真正執行的時候可能不創建這個對象,而是直接創建這個對象分解后的標量來代替。這樣就無需在對對象分配空間了,只在棧上為分解出的變量分配內存即可。
注意:
逃逸分析是比較耗時的,所以性能未必提升很多,因為其耗時性,采用的算法都是不那么準確但是時間壓力相對較小的算法來完成的,這就可能導致效果不穩定,要慎重。
由于HotSpot虛擬機目前的實現方法導致棧上分配實現起來比較復雜,所以HotSpot虛擬機中暫時還沒有這項優化。
相關JVM參數:
-XX:+DoEscapeAnalysis 開啟逃逸分析、
-XX:+PrintEscapeAnalysis 開啟逃逸分析后,可通過此參數查看分析結果。
-XX:+EliminateAllocations 開啟標量替換。
-XX:+EliminateLocks 開啟同步消除。
-XX:+PrintEliminateAllocations 開啟標量替換后,查看標量替換情況。
3.6.5.什么情況下會發生逃逸?
案例:
package com.toto.jvm.demo4;public class StackAllocation {public StackAllocation obj;/*** 方法返回StackAllocation對象,發生逃逸* @return*/public StackAllocation getInstance() {return obj == null ? new StackAllocation() : obj;}/*** 為成員屬性賦值,發生逃逸*/public void setObj() {this.obj = new StackAllocation();}/*** 對象的作用域僅在當前方法中有效,沒有發生逃逸*/public void useStackAllocation() {StackAllocation s = new StackAllocation();}/*** 引用成員變量的值,發生逃逸*/public void useStackAllocation2() {StackAllocation s = getInstance();}}3.7.直接內存
查看一下什么是直接內存。
NIO中直接分配直接內存。
3.8.Java內存區域-直接內存和運行時常量池
3.8.1.運行時常量池簡介
運行時常量池(Runtime Constant Pool),它是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后存放到常量池中。
運行時常量是相對于常量來說的,它具備一個重要特征是:動態性。當然,值相同的動態常量與我們通常說的常量只是來源不同,但是都是儲存在池內同一塊內存區域。Java語言并不要求常量一定只能在編譯期產生,運行期間也可能產生新的常量,這些常量被放在運行時常量池中。這里所說的常量包括:基本類型包裝類(包裝類不管理浮點型,整型只會管理-128到127)和String(也可以通過**String.intern()**方法可以強制將String放入常量池)
3.8.2.Class文件中的信息常量池
在Class文件結構中,最頭的4個字節用于存儲Megic Number,用于確定一個文件是否能被JVM接受,再接著4個字節用于存儲版本號,前2個字節存儲次版本號,后2個存儲主版本號,再接著是用于存放常量的常量池,由于常量的數量是不固定的,所以常量池的入口放置一個U2類型的數據(constant_pool_count)存儲常量池容量計數值。
常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當于Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:
?類和接口的全限定名
?字段名稱和描述符
?方法名稱和描述符
3.8.3.常量池的好處
常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共享。例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。
?節省內存空間:常量池中所有相同的字符串常量被合并,只占用一個空間。
?節省運行時間:比較字符串時,比equals()快。對于兩個引用變量,只用判斷引用是否相等,也就判斷實際值是否相等。
雙等號==的含義
?基本數據類型之間應用雙等號,比較的是他們的數值。
?復合數據類型(類)之間應用雙等號,比較的是他們在內存中的存放地址。
3.8.4.基本類型的包裝類和常量池
java中基本類型的包裝類的大部分都實現了常量池技術,即Byte,Short,Integer,Long,Character,Boolean。這5種包裝類默認創建了數值[-128, 127]的相應類型的緩存數據,但是超出此范圍仍然會去創建新的對象。兩種浮點數類型的包裝類Float,Double并沒有實現常量池技術。
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); 這種情況下會創建新的對象。
?語句i4 == i5 + i6,因此+這個操作符不適用于Integer對象,首先i5和i6進行自動拆箱操作,進行數值相加,即i4 == 40。然后Integer對象無法與數值進行直接比較,所以i4自動拆箱轉為int值40,最終這條語句轉為40 == 40進行數值比較。
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”)是直接在堆內存空間創建一個新的對象。只要使用new方法,便需要創建的對象。
?連接表達式+,只有使用引號包含文本的方式創建的String對象之間使用”+”連接產生的新對象才會被加入常量池中。
?對于字符串變量的”+”連接表達式,它所產生的新對象都不會被加入字符串池中,其屬于在運行時創建的字符串,具有獨立的內存地址,所以不引用自同—String對象。
3)String與常量池-靜態方法賦值
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,它們不是同一個對象");}}} 輸出結果: s不等于t,它們不是同一個對象解釋:
s不等于t,它們不是同一個對象。A和B雖然被定義為常量,但是它們都沒有馬上被賦值。在運算出s的值之前,他們何時被賦值,以及被賦予什么樣的值,都是個變量。因此A和B在賦值之前,性質類似于一個變量。那么s就不能在編譯期被確定,而只能運行時被創建了。
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));/*** 結果是:* s1 == s2 ? false* s3 == s2 ? true**/}}解釋:
String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進入常量。
5)String與常量池 - 延伸
String s1 = new String(“xyz”); //創建了幾個對象?解釋:
考慮類加載階段和實際執行時。
?類加載對一個類只會進行一次。”xyz”在類加載時就已經創建并駐留了(如果該類被加載之前已經有”xyz”字符串被駐留過則不需要重新創建用于駐留的”xyz”實例)。駐留的字符串是放在全局共享的字符串常量池中的。
?在這段代碼后連續被運行的時候,”xyz”字面量對應的String實例已經固定了,不會再被重新創建。所以這段代碼將常量池中的對象復制一份放在到heap中,并且把heap中的這個對象的引用交給s1持有。
這條語句創建了2個對象。
intern()會把值搬到運行時常量池中。它是一個native方法。
如果無法申請內存,報:OutOfMemoryError
3.9.對象在內存中的布局-對象的創建
對象創建 步驟
1、new類名
2、根據new的參數在常量池中定位一個類的符號引用。
3、如果沒有找到這個符號引用,說明類還沒加被加載,則進行類的加載、解析和初始化。
4、虛擬機為對象分配內存(位于堆中)
5、將分配的內存初始化為零值(不包括對象頭)
6、調用對象的方法。
3.10.探究對象的結構
3.11.深度理解對象的訪問定位
已經創建對象,如何找到對象呢?就涉及到訪問定位的問題
有兩種方式(百度一下):
1、使用句柄
2、直接指針
使用句柄池的用途
Hotsport使用直接尋址(直接指針)的方式定位
- 到對象實例數據的指針
- 到對象類型數據的指針
3.12.Java對象訪問方式
一般來說,一個Java的引用訪問涉及到3個內存區域:JVM棧,堆,方法區。以最簡單的本地變量引用:Object objRef = new Object()為例:
?Object objRef表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數據;
?new Object()作為實例對象數據存儲在堆中;
?堆中還記錄了能夠查詢到此Object對象的類型數據(接口、方法、field、對象類型等)的地址,實際的數據則存儲在方法區中;
在Java虛擬機規范中,只規定了指向對象的引用,對于通過reference類型引用訪問具體對象的方式并未做規定,不過目前主流的實現方式主要有兩種:
3.12.1.通過句柄訪問
通過句柄訪問的實現方式中,JVM堆中會劃分單獨一塊內存區域作為句柄池,句柄池中存儲了對象實例數據(在堆中)和對象類型數據(在方法區中)的指針。這種實現方法由于用句柄表示地址,因此十分穩定。
3.12.2.通過直接指針訪問
通過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區中的相應類型數據。這種方法最大的優勢是速度快,在HotSpot虛擬機中用的就是這種方式。
3.13.對象分配內存的策略
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成后便可完全確定,為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump thePointer)。如果Java堆中的內存并不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(FreeList)。選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用空閑列表。
給對象分配的方式:
方式一:指針碰撞
方式二:空間列表
下面兩張圖可以解釋指針碰撞和空閑列表:
指針碰撞:
空間列表:
3.13.1.線程安全問題
1、實現線程同步,加鎖(但:執行效率低)
2、本地線程分配緩沖TLAB: (每個線程分配一定一定的內存)
3.13.1.1.本地線程分配緩沖----TLAB
TLAB是虛擬機在堆內存的劃分出來的一塊專用空間,是線程專屬的。在TLAB啟動的情況下,在線程初始化時,虛擬機會為每個線程分配一塊TLAB空間,只給當前線程使用,這樣每個線程都單獨擁有一個空間,如果需要分配內存,就在自己的空間上分配,這樣就不存在競爭的情況,可以大大提升分配效率。
ps:這里說線程獨享的堆內存,只是在“內存分配”這個動作上是線程獨享的,至于在讀取、垃圾回收等動作上都是線程共享的。即是指其他線程可以在這個區域讀取、操作數據,但是無法在這個區域中分配內存。
3.13.1.2.TLAB生命周期
在分代收集的垃圾回收器中,TLAB是在eden區分配的。TLAB 是從堆上 Eden 區的分配的一塊線程本地私有內存。線程初始化的時候,如果JVM 啟用了TLAB(默認是啟用的, 可以通過 -XX:-UseTLAB 關閉),則會創建并初始化TLAB。同時,在GC 掃描對象發生之后,線程第一次嘗試分配對象的時候,也會創建并初始化TLAB。
在TLAB已經滿了或者接近于滿了的時候,TLAB可能會被釋放回Eden。GC掃描對象發生時,TLAB會被釋放回Eden。TLAB 的生命周期期望只存在于一個GC 掃描周期內。在JVM中,一個 GC 掃描周期,就是一個epoch。那么,可以知道,TLAB 內分配內存一定是線性分配的。
3.13.1.3.TLAB的大小
TLAB的初始大小可由參數-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 數組的最大大小。
TLAB空間大小的動態調整:
默認情況下:
resize開關是默認開啟的,JVM可以對TLAB空間大小進行調整。
對象的慢分配
當TLAB內存充足時,分配新對象的方式稱為快分配。當TLAB內存不足,分配新對象的方式稱為“慢分配”。慢分配有兩種處理方式:
1、當TLAB剩余內存空間小于TLAB最大浪費空間時,丟棄當前 TLAB 回歸 Eden,線程獲取新的 TLAB 分配對象。
2、當TLAB剩余內存空間大于TLAB最大浪費空間時,對象直接在Eden區分配內存。
TLAB最大浪費空間
最大浪費空間是一個動態值,TLAB最大浪費空間初始值=TLAB大小/TLABRefillWasteFraction。TLABRefillWasteFraction默認為64,所以TLAB最大浪費空間初始值為TLAB大小的1/64。伴隨著每次慢分配,這個TLAB最大浪費空間會每次遞增 TLABWasteIncrement 大小的空間。
3.13.1.4.總結
TLAB流程總結:
3.13.1.5.參數總結
| UseTLAB | 是否啟用 TLAB,默認是啟用的。 |
| ResizeTLAB | TLAB 是否是自適應可變的,默認為是 |
| TLABSize | 初始 TLAB 大小,單位是字節 。默認為0,0 就是不主動設置 TLAB 初始大小,而是通過 JVM 自己計算每一個線程的初始大小。例如:-XX:TLABSize=65536 |
| MinTLABSize | 最小 TLAB 大小。單位是字節,默認2048。例如-XX:MinTLABSize=4096 |
| TLABRefillWasteFraction | 在一次 TLAB 再填充(refill)發生的時候,最大的 TLAB 浪費。默認為64,和TLAB最大浪費空間有關。TLAB最大浪費空間= TLAB大小/TLABRefillWasteFraction |
| TLABWasteIncrement | TLAB 慢分配時允許的 TLAB 浪費增量. |
參考:
https://blog.csdn.net/a1076067274/article/details/112969208
3.14.垃圾回收-判斷對象是否存活算法-引用計數法詳解
在對象中添加一個引用計數器,當有地方引用這個對象的時候,引用計數器的值就+1,當引用失效的時候,計數就減一
Java中一般不用:引用計數方法。
如何判斷垃圾如何回收。
查看gc信息的方式
案例:
創建循環引用方式:
斷掉右側的先:
Jdk8采用的并不是引用計數法,而是默認是:parallel垃圾回收即。
3.15.垃圾回收-判斷對象是否存活算法-可達性分析法詳解
3.15.1.可達性分析算法
在Java中,是通過可達性分析(Reachability Analysis)來判定對象是否存活的。該算法的基本思路就是通過一些被稱為引用鏈(GC Roots)的對象作為起點,從這些節點開始向下搜索,搜索走過的路徑被稱為(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。
如上圖所示,object1~object4對GC Root都是可達的,說明不可被回收,object5和object6對GC Root節點不可達,說明其可以被回收。
在Java中,可作為GC Root的對象包括以下幾種:
?虛擬機棧(棧幀中的本地變量表)中引用的對象
?方法區中類靜態屬性所引用的對象
?方法區中常量所引用的對象
?本地方法棧中JNI(即一般說的Native方法)引用的對象
在堆里存放著幾乎多有的java對象實例,垃圾搜集器在對堆進行回收之前,第一件事情就是確定這些對象之中哪些還“存活”著(即通過任何途徑都無法使用的對象)。
3.15.2.finalize()方法最終判定對象是否存活
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。
標記的前提是對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈。
1. 第一次標記并進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法。
當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”,對象被回收。
2. 第二次標記
如果這個對象被判定為有必要執行finalize()方法,那么這個對象將會被放置在一個名為:F-Queue的隊列之中,并在稍后由一條虛擬機自動建立的、低優先級的Finalizer線程去執行。這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),將很可能會導致F-Queue隊列中的其他對象永久處于等待狀態,甚至導致整個內存回收系統崩潰。
Finalize()方法是對象脫逃死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。
3.15.3.Java引用
從可達性算法中可以看出,判斷對象是否可達時,與“引用”有關。那么什么情況下可以說一個對象被引用,引用到底代表什么?
在JDK1.2之后,Java對引用的概念進行了擴充,可以將引用分為以下四類:
強引用(Strong Reference)
軟引用(Soft Reference)
弱引用(Weak Reference)
虛引用(Phantom Reference)
這四種引用從上到下,依次減弱
3.15.3.1.強引用
強引用就是指在程序代碼中普遍存在的,類似Object obj = new Object()這類似的引用,只要強引用在,垃圾搜集器永遠不會搜集被引用的對象。也就是說,寧愿出現內存溢出,也不會回收這些對象。
3.15.3.2.軟引用
軟引用是用來描述一些有用但并不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。對于軟引用關聯著的對象,只有在內存不足的時候JVM才會回收該對象。因此,這一點可以很好地用來解決OOM的問題,并且這個特性很適合用來實現緩存:比如網頁緩存、圖片緩存等。
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.弱引用
弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在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類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。
要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
3.15.3.5.軟引用和弱引用進一步說明
在SoftReference類中,有三個方法,兩個構造方法和一個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方法用來獲取與軟引用關聯的對象的引用,如果該對象被回收了,則返回null。
在使用軟引用和弱引用的時候,我們可以顯示地通過System.gc()來通知JVM進行垃圾回收,但是要注意的是,雖然發出了通知,JVM不一定會立刻執行,也就是說這句是無法確保此時JVM一定會進行垃圾回收的。
3.15.3.6.虛引用進一步說明:
虛引用中有一個構造函數,可以看出,其必須和一個引用隊列一起存在。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);} }總結
以上是生活随笔為你收集整理的3.内存分配、逃逸分析与栈上分配、直接内存和运行时常量池、基本类型的包装类和常量池、TLAB、可达性分析算法(学习笔记)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 直销银行什么意思
- 下一篇: 创投板块有哪些上市公司 一笔投资可能就改