對象創建的主要流程
- 類加載檢查
在創建對象之前,JVM 首先會檢查該類是否已經加載、解析并初始化:
如果沒有,則會通過類加載機制加載類元信息(Class Metadata)到方法區。
這個過程包括:加載(load)、驗證(verify)、準備(prepare)、解析(resolve)、初始化(init)。
new關鍵詞、對象克隆、對象序列化等
- 分配內存
對象的內存分配通常發生在堆內存中(Heap):
JVM 使用了兩種主要的內存分配方式
分配方式 | 說明 |
---|---|
指針碰撞(Bump-the-pointer)(默認用指針碰撞) | 適用于堆空間規整;分配快,只需移動指針。 |
空閑列表(Free List) | 適用于堆空間不規整(存在碎片);需要維護空閑塊鏈表,分配效率低一些。(通常在垃圾回收后出現堆空間不規整) |
以上兩種方法都存在并發問題,解決并發問題的方法:
- CAS(compare and swap)
虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性來對分配內存空間的動作進行同步處理。 - 本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)
把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存。通過-XX:+/-UseTLAB參數來設定虛擬機是否使用TLAB(JVM會默認開啟-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。
- 內存初始化(零值初始化)
分配的內存空間會被零值初始化(不包括對象頭),也就是將對象的實例字段全部設為默認值(0、null、false 等)。如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。
注意:這只是 JVM 級別的初始化,字段的顯式初始值(如 int a = 10)還未處理。
- 設置對象頭
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、 實例數據(Instance Data)和對齊填充(Padding)。 HotSpot虛擬機的對象頭包含兩個主要部分:
- Mark Word:記錄哈希碼、GC 分代信息、鎖信息等。
- Klass Pointer:指向對象的類元信息(方法區存的類元數據)(即該對象是哪個類的實例)。
64 位 JVM 默認啟用了指針壓縮(CompressedOops),否則指針是 64 bit,Klass Pointer 就會是 8 byte。
- 32位對象頭
- 64位對象頭
數組對象在 JVM 中的對象頭結構 相比普通對象稍有不同,因為它除了包含普通對象頭,還需要4字節存儲數組長度。
分代年齡4字節也驗證了只能小于等于15
- 執行< init >方法
執行< init >方法,即對象按照程序員的意愿進行初始化。對應到語言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值),和執行構造方法。
對象大小與指針壓縮
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;/*** 計算對象大小*/
public class JOLSample {public static void main(String[] args) {ClassLayout layout = ClassLayout.parseInstance(new Object());System.out.println(layout.toPrintable());System.out.println();ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});System.out.println(layout1.toPrintable());System.out.println();ClassLayout layout2 = ClassLayout.parseInstance(new A());System.out.println(layout2.toPrintable());}// -XX:+UseCompressedOops 默認開啟的壓縮所有指針// -XX:+UseCompressedClassPointers 默認開啟的壓縮對象頭里的類型指針Klass Pointer// Oops : Ordinary Object Pointerspublic static class A {//8B mark word//4B Klass Pointer 如果關閉壓縮-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,則占用8Bint id; //4BString name; //4B 如果關閉壓縮-XX:-UseCompressedOops,則占用8Bbyte b; //1B Object o; //4B 如果關閉壓縮-XX:-UseCompressedOops,則占用8B}
}運行結果:
java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) //mark word4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) //mark word 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) //Klass Pointer12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total[I object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes totalcom.tuling.jvm.JOLSample$A object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407)12 4 int A.id 016 1 byte A.b 017 3 (alignment/padding gap) 20 4 java.lang.String A.name null24 4 java.lang.Object A.o null28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
- 什么是java對象的指針壓縮?
- jdk1.6 update14開始,在64bit操作系統中,JVM支持指針壓縮
- jvm配置參數:
-XX:+UseCompressedOops 默認開啟的壓縮所有指針(關閉同時會關閉UseCompressedClassPointers ),
-XX:+UseCompressedClassPointers 默認開啟的壓縮對象頭里的類型指針Klass Pointer
(compressed–壓縮、oop(ordinary object pointer)–對象指針) - 啟用指針壓縮:-XX:+UseCompressedOops(默認開啟),禁止指針壓縮:-XX:-UseCompressedOops
指針壓縮(Compressed Oops)主要是為了解決 64 位 JVM 指針過大帶來的內存和性能問題。
64 位指針占用 8 字節,是 32 位指針的兩倍(4 字節)。Java 堆中對象眾多,對象之間通過指針相互引用,指針占內存總量很大。指針變大導致:堆內存占用增加,同樣大小的堆實際能存放的對象數量減少。CPU 緩存壓力變大,緩存命中率下降,性能下降。內存帶寬需求增加,訪問延遲提高。指針壓縮將 64 位指針壓縮成 32 位偏移量,減少內存占用。
- 堆內存小于4G時,不需要啟用指針壓縮,jvm會直接去除高32位地址,即使用低虛擬地址空間
- 堆內存大于32G時,壓縮指針會失效,會強制使用64位(即8字節)來對java對象尋址,這就會出現堆內存占用增加等問題,所以堆內存不要大于32G為好(最大堆大小 ≈ 壓縮指針可表示的偏移范圍 × 縮放因子(scale)最大堆地址范圍 = 2^32 × 8 = 34,359,738,368 字節 ≈ 32GB)
關于對齊填充:對于大部分處理器,對象以8字節整數倍來對齊填充都是最高效的存取方式。
對象內存分配
對象內存分配流程
對象棧上分配(逃逸分析)
JVM通過逃逸分析判斷對象是否只在方法內部使用,若沒有逃逸,可以將對象分配在棧上(棧上分配)或通過標量替換消除對象,減少堆分配壓力。
- 對象逃逸分析:就是分析對象動態作用域,當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他地方中。 JVM對于這種情況可以通過開啟逃逸分析參數(-XX:+DoEscapeAnalysis)來優化對象內存分配位置,使其通過標量替換優先分配在棧上(棧上分配),JDK7之后默認開啟逃逸分析,如果要關閉使用參數(-XX:-DoEscapeAnalysis)
- 標量替換(標量替換聚合量):通過逃逸分析確定該對象不會被外部訪問,并且對象可以被進一步分解時,JVM不會創建該對象,而是將該對象成員變量分解若干個被這個方法使用的成員變量所代替,這些代替的成員變量在棧幀或寄存器上分配空間,這樣就不會因為沒有一大塊連續空間導致對象內存不夠分配。開啟標量替換參數(-XX:+EliminateAllocations),JDK7之后默認開啟。
- 標量與聚合量:標量即不可被進一步分解的量,而JAVA的基本數據類型就是標量(如:int,long等基本數據類型以及reference類型等),標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量。而在JAVA中對象就是可以被進一步分解的聚合量。
對象在Eden區分配
新生代分為 Eden區 和兩個 Survivor區(S0、S1)Eden與Survivor區默認8:1:1,新生代對象絕大多數先在 Eden 區分配。大多數對象生命周期短暫,及時GC回收效率高,Eden區空間一般較大,分配速度快。
當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
Minor GC和Full GC
- Minor GC/Young GC:指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
觸發minor gc就會會把eden和from清空,并且把兩個區域還存活的對象移動到to - Major GC/Full GC:一般會回收老年代 ,年輕代,方法區的垃圾,Major GC的速度一般會比Minor GC的慢10倍以上。
就是執行一次Minor GC+老年代的回收+方法區的回收
JVM默認有這個參數-XX:+UseAdaptiveSizePolicy(默認開啟),會導致這個8:1:1比例自動變化,如果不想這個比例有變化可以設置參數-XX:-UseAdaptiveSizePolicy
-XX:+PrintGCDetails 是 JVM 的一個參數,用于打印垃圾回收(GC)時的詳細日志信息。它是調試和性能分析GC行為的常用工具。
大對象直接進入老年代
當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,GC期間虛擬機又發現無法存入Survior空間,所以只好把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放,所以不會出現Full GC。
大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。JVM參數 -XX:PretenureSizeThreshold(單位字節) 可以設置大對象的大小,如果對象超過設置大小會直接進入老年代,不會進入年輕代,這個參數只在 Serial 和ParNew兩個收集器下有效。這樣可以避免為大對象分配內存時的復制操作而降低效率。
長期存活的對象將進入老年代
對象在 Eden 出生并經過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設為1。對象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,CMS收集器默認6歲,不同的垃圾收集器會略微有點不同)就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 **-XX:MaxTenuringThreshold(最大值硬編碼為 15,解釋對象頭有提到) **來設置。
對象動態年齡判斷
JVM 會根據 Survivor 區當前對象年齡的分布動態決定對象是否需要提前進入老年代,即使它們還沒有達到 MaxTenuringThreshold。
當前放對象的Survivor區域里(to space),一批對象的總大小大于這塊Survivor區域內存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此時大于等于這批對象年齡最大值的對象直接進入老年代。
例如Survivor區域里現在有一批對象,年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代。這個規則其實是希望那些可能是長期存活的對象,盡早進入老年代。
動態年齡判斷觸發對象直接進入老年代,發生在每次 Minor GC 執行時的對象復制過程中。
當 Minor GC 把 Eden 和 From 區的存活對象復制到 To Space 時,如果即將進入 To Space 的對象總大小超過 To Space 容量的一半(即 Survivor 區目標大小的一半),會導致這些對象直接進入老年代。
老年代空間分配擔保機制
老年代空間分配擔保機制(也稱為“晉升空間擔保”)是 JVM 中一項重要的安全機制,可以實現提前進行full gc。并且確保在 minor GC 時,幸存對象能夠安全晉升到老年代,不會因為老年代空間不足導致堆內存溢出或對象丟失。
擔保機制的流程:
- minor GC 準備階段:JVM 會計算老年代剩余可用空間是否小于年輕代里現有的所有對象大小之和(包括垃圾對象)
- 如果空間足夠,minor GC 繼續執行,晉升操作順利完成。
- 如果空間不足,就會看一個
-XX:-HandlePromotionFailure
(jdk1.8默認就設置了)的參數是否設置了,如果有這個參數,就會看看老年代的可用內存大小,是否大于之前每一次minor gc后進入老年代的對象的平均大小。老年代的可用內存大小依然不足之前進入老年代的平均大小就full gc,足夠則minor gc - 如果老年代剩余空間不足并且參數沒有設置,就會觸發一次full gc,對老年代和年輕代一起回收一次垃圾,如果回收完還是沒有足夠空間存放新的對象就會發生"OOM"
當然,如果minor gc之后剩余存活的需要挪動到老年代的對象大小還是大于老年代可用空間,那么也會觸發full gc,full gc完之后如果還是沒有空間放minor gc之后的存活對象,則也會發生“OOM”
對象內存回收
引用計數法(jvm一般不用)
每個對象維護一個引用計數器(refCount),每當有一個地方引用它,計數器加 1;引用失效時,計數器減 1。當引用計數為 0,就說明該對象不再被使用,可以被回收。致命缺點:循環引用問題。引用計數法無法處理兩個對象互相引用但整體不可達的情況。
public class ReferenceCountingGc {Object instance = null;public static void main(String[] args) {ReferenceCountingGc objA = new ReferenceCountingGc();ReferenceCountingGc objB = new ReferenceCountingGc();objA.instance = objB;objB.instance = objA;objA = null;objB = null;}
}
除了對象objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導致它們的引用計數器都不為0,于是引用計數算法無法通知 GC 回收器回收他們。
可達性分析算法(jvm一般使用)
將“GC Roots” 對象作為起點,從這些節點開始向下搜索引用的對象,找到的對象都標記為非垃圾對象,其余未標記的對象都是垃圾對象
常見的 GC Roots 包括:
GC Roots 類型 | 示例 |
---|---|
虛擬機棧中的引用變量(棧幀中的局部變量) | 方法中定義的局部變量、參數等 |
方法區中靜態字段引用的對象 | static 引用的對象 |
方法區中常量引用的對象 | 字面量池中的字符串,如 "abc" |
本地方法棧中的 JNI 引用 | Native 方法中用到的對象 |
運行時的活動線程 | 啟動的線程本身不會被 GC 回收 |
類加載器 | 系統/應用類加載器 |
因為stw了,還在活躍但是被暫停的棧幀的對象不該被回收
三色標記(并發垃圾回收器(如 CMS、G1))
三色標記算法(Tri-color Marking)是現代垃圾回收(GC)中常用的可達性分析方法,特別用于并發和增量標記階段,它有效避免了標記過程中出現對象“丟失”的問題。
三色 | 含義 |
---|---|
白色(White) | 表示未被訪問的對象集合。初始時,所有對象都是白色。標記完成后,仍為白色的對象被判定為不可達對象,需要回收。 |
灰色(Gray) | 示已經被發現,但其引用的對象尚未完全掃描的對象集合。需要掃描灰色對象的引用,進一步遞歸標記。 |
黑色(Black) | 表示該對象以及它引用的所有對象都已經被掃描完成。 |
算法流程:
- 初始化:所有對象為白色。從 GC Roots 開始,將這些根對象標記為灰色。
- 掃描灰色對象:從灰色對象集合中取出一個對象,掃描它引用的所有白色對象。發現的白色對象變成灰色,加入待掃描集合。掃描完成后,當前對象變黑色。
- 重復掃描灰色對象,直到灰色集合為空。此時所有可達對象都變成了黑色。剩下的白色對象即不可達,準備回收。
三色標記通過保持“黑色對象不引用白色對象”的不變式,保證標記的正確性。
當應用線程修改引用時,如果黑色對象引用變成了白色對象,GC必須檢測到這個“新引用的白色對象”。
GC使用寫屏障(Write Barrier)機制,捕獲這類修改,并將被新引用的白色對象標記為灰色,加入掃描隊列,保證不會遺漏。
常見引用類型
Java 四種引用類型(按強弱排序):
引用類型 | 類名 | 是否會阻止 GC | 回收時機描述 |
---|---|---|---|
強引用 | 普通變量引用(默認) | 會阻止回收 | 永遠不會被 GC 回收,除非手動斷開引用 |
軟引用 | java.lang.ref.SoftReference | 一定條件下回收 | 內存不足時才會被回收,適合緩存 |
弱引用 | java.lang.ref.WeakReference | 不阻止回收 | 下一次 GC 就會被回收 |
虛引用 | java.lang.ref.PhantomReference | 完全不保留 | 被 GC 回收前就進入 ReferenceQueue,僅作通知 |
- 強引用:普通的變量引用
public static User user = new User();//靜態變量
public Object foo() {Object obj = new Object();//方法內的變量引用return obj;
}
foo();//返回的對象不可達,可能會被回收
Object result = foo();//返回的對象賦值了屬于強引用
- 軟引用:將對象用SoftReference軟引用類型的對象包裹,正常情況不會被回收,但是GC做完后發現釋放不出空間存放新的對象,則會把這些軟引用的對象回收掉。軟引用可用來實現內存敏感的高速緩存。
public static SoftReference<User> user = new SoftReference<User>(new User());
例如瀏覽器的后退按鈕。按后退時,這個后退時顯示的網頁內容是重新進行請求還是從緩存中取出呢?
- 如果一個網頁在瀏覽結束時就進行內容的回收,則按后退查看前面瀏覽過的頁面時,需要重新構建
- 如果將瀏覽過的網頁存儲到內存中會造成內存的大量浪費,甚至會造成內存溢出
這時候軟引用就可以實現,在內存充足的時候緩存,不足的時候gc,讓程序重新加載頁面。
- 弱引用:將對象用WeakReference軟引用類型的對象包裹,弱引用跟沒引用差不多,GC會直接回收掉,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
- 虛引用:虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系,幾乎不用
finalize()方法最終判定對象是否存活
finalize() 是 Java 中 java.lang.Object 類定義的一個方法,它在垃圾回收器準備回收對象之前被調用,可以讓對象有機會進行清理操作。
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。
標記的前提是對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈。
- 第一次標記并進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize方法,對象將直接被回收。 - 第二次標記
如果這個對象覆蓋了finalize方法,會進行第二次標記,如果對象還是應該被回收就真的被回收了。
缺點與問題:
- 不確定性:finalize() 什么時候被調用不確定,依賴 GC 時間,可能很晚或根本不調用。
- 性能開銷大:有 finalize() 方法的對象回收會更慢,垃圾回收效率降低。
- 復活問題:在 finalize() 中可以讓對象“復活”(重新被引用),導致難以預測的行為。
- 可能導致內存泄漏:如果對象在 finalize() 中復活,但不被及時清理。
finalize()方法的運行代價高昂, 不確定性大, 無法保證各個對象的調用順序, 如今已被官方明確聲明為不推薦使用的語法。finalize()方法的運行代價高昂, 不確定性大, 無法保證各個對象的調用順序, 如今已被官方明確聲明為不推薦使用的語法。
如何判斷一個類是無用的類
方法區主要回收的是無用的類,那么如何判斷一個類是無用的類呢?
類需要同時滿足下面4個條件才能算是 “無用的類” :
- 該類所有的對象實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
- 加載該類的 ClassLoader 已經被回收。
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒有在任何地方通過反射訪問該類的方法。
- 該類的靜態變量/常量沒有被外部引用,靜態變量/常量持有的強引用會阻止類的卸載,因此靜態變量/常量應當不被引用或已置空。