柚子快報(bào)邀請(qǐng)碼778899分享:JVM 根可達(dá)算法
柚子快報(bào)邀請(qǐng)碼778899分享:JVM 根可達(dá)算法
Java中的垃圾
Java中"垃圾"通常指的是不再被程序使用和引用的對(duì)象,具體表現(xiàn)在沒(méi)有被棧、JNI指針和永久代對(duì)象所引用的對(duì)象。Java作為一種面向?qū)ο蟮木幊陶Z(yǔ)言,它使用自動(dòng)內(nèi)存管理機(jī)制,其中垃圾收集器負(fù)責(zé)檢測(cè)和回收不再被程序引用的對(duì)象,以釋放它們占用的內(nèi)存空間。以下是一些導(dǎo)致對(duì)象成為垃圾的常見(jiàn)情況:
無(wú)引用對(duì)象: 當(dāng)一個(gè)對(duì)象沒(méi)有任何引用指向它時(shí),它就變得不可達(dá),成為垃圾,Java的垃圾收集器會(huì)識(shí)別這樣的對(duì)象,并將它們回收。 引用循環(huán): 如果一組對(duì)象彼此引用形成一個(gè)循環(huán),而這個(gè)循環(huán)與程序的其他部分沒(méi)有引用相連,那么這個(gè)循環(huán)中的對(duì)象就會(huì)成為垃圾。Java的垃圾收集器通過(guò)識(shí)別引用循環(huán)并處理它們來(lái)防止內(nèi)存泄漏。 顯式置空引用: 如果程序員顯式地將一個(gè)引用置空(null),而沒(méi)有其他引用指向相同的對(duì)象,那么該對(duì)象就變成了垃圾。
垃圾收集器周期性地運(yùn)行,并識(shí)別和回收這些垃圾對(duì)象,釋放其內(nèi)存中對(duì)應(yīng)的區(qū)域以確保內(nèi)存能夠得到有效利用,這種自動(dòng)的內(nèi)存管理機(jī)制就叫做垃圾回收。
如何尋找垃圾?
引用計(jì)數(shù)(Reference Count) 引用計(jì)數(shù)算法是一種垃圾標(biāo)記,其核心思想是通過(guò)維護(hù)對(duì)象的引用計(jì)數(shù)來(lái)判斷對(duì)象是否可以被回收。每個(gè)對(duì)象都有一個(gè)關(guān)聯(lián)的引用計(jì)數(shù),表示當(dāng)前有多少個(gè)指針引用它。當(dāng)引用計(jì)數(shù)為零時(shí),意味著沒(méi)有指針再引用該對(duì)象,因此可以安全地回收該對(duì)象的內(nèi)存。
其實(shí)引用計(jì)數(shù)算法的核心思想就是,只要有對(duì)象引用我,那么就說(shuō)明我是有用的,我還不需要被回收,反正,我就是沒(méi)有用的對(duì)象,那么我和我的子對(duì)象都應(yīng)該被回收掉。這里我們說(shuō)的對(duì)象都是堆上的對(duì)象,一般是堆上的內(nèi)存空間需要程序員手動(dòng)回收,而棧上的內(nèi)存空間則由操作系統(tǒng)自行回收。由于棧上的對(duì)象是操作系統(tǒng)自行管理和回收的,因此棧上的對(duì)象以及一些靜態(tài)對(duì)象始終都是出于存活的狀態(tài),因此,堆中存活的對(duì)象至少會(huì)有一個(gè)引用(指針)指向它。
但是這樣會(huì)存在著一個(gè)問(wèn)題,就是對(duì)象中的引用關(guān)系形成了環(huán)狀——循環(huán)引用,這種情況下環(huán)內(nèi)所有對(duì)象的引用都是>1的,這樣一來(lái)環(huán)內(nèi)的所有都無(wú)法被回收從而造成“內(nèi)存泄漏”。這是引用算法最主要的局限性,也是為什么JVM不采用循環(huán)計(jì)數(shù)的方法來(lái)標(biāo)記垃圾的原因。
根可達(dá)算法(Root Search)
由于引用計(jì)數(shù)算法無(wú)法解決“循環(huán)引用”的問(wèn)題,無(wú)可避免的會(huì)造成內(nèi)存泄露,因此,Java沒(méi)有采用引用計(jì)數(shù)算法來(lái)尋找垃圾。而是采用了一種從GC Roots開(kāi)始搜索存活對(duì)象的垃圾標(biāo)記算法——根可達(dá)算法。
哪些是GC Root?
線程棧 (Thread Stacks) :活動(dòng)線程的棧幀中的本地變量引用的對(duì)象。每個(gè)線程都有一個(gè)棧,棧中的引用對(duì)象是潛在的存活對(duì)象。靜態(tài)變量 (Static Variables) :類(lèi)的靜態(tài)成員變量引用的對(duì)象。靜態(tài)變量隨著類(lèi)的加載而初始化,它們的引用可能使對(duì)象保持存活。常量池 (Constant Pool) :常量池中的引用,包括字符串常量等。這些常量在類(lèi)加載時(shí)被創(chuàng)建,它們的引用也可能使對(duì)象保持存活。JNI 引用 (JNI References) :通過(guò) JNI 在本地代碼中創(chuàng)建的對(duì)象引用。如果 Java 代碼通過(guò) JNI 調(diào)用了本地方法,并在本地方法中創(chuàng)建了對(duì)象,這些對(duì)象的引用也是 GC Roots。監(jiān)控與管理 MBeans (JMX) :活動(dòng)的監(jiān)控、管理 MBeans 等通過(guò) JMX 等管理工具注冊(cè)的對(duì)象。這些對(duì)象的引用也被視為 GC Roots。
線程棧 (Thread Stacks) 在Java中,當(dāng)程序運(yùn)行的時(shí)候線程會(huì)將一個(gè)個(gè)方法放到棧上來(lái)執(zhí)行,并且對(duì)于方法局部的一些小的對(duì)象和變量也會(huì)被分配在??臻g上,而??臻g是由操作系統(tǒng)本身來(lái)控制什么時(shí)候進(jìn)行釋放和分配的。因此,基于這個(gè)邏輯我們可以認(rèn)為對(duì)于當(dāng)前線程來(lái)說(shuō),存在于??臻g上的變量都是存活的,而且??臻g一般比較小只有幾MB的大小,里面存活的變量和對(duì)象都是有限的作為GC Roots來(lái)說(shuō)搜索起來(lái)也是非常高效的。
靜態(tài)變量 (Static Variables) 在Java中靜態(tài)變量一般是隨著類(lèi)加載的時(shí)候被創(chuàng)建和初始化的,和Java字節(jié)碼一樣,靜態(tài)變量也會(huì)被加載到元空間(Meta Space,Java 8之前叫做方法區(qū)(Method Area)或叫做永久代(Permanent Generation),Java 8之后叫做元空間)。
元空間的對(duì)象是不會(huì)輕易被釋放的,而靜態(tài)變量會(huì)隨著整個(gè)類(lèi)被釋放的時(shí)候才會(huì)被釋放,因此靜態(tài)變量可以作為GC Root來(lái)尋找垃圾。
常量池 (Constant Pool) 常量池(Constant Pool)是Java中一種存放常量的數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)編譯期生成的字面量和符號(hào)引用。常量池屬于元空間(Meta Space,Java 8之前叫做方法區(qū)(Method Area)或叫做永久代(Permanent Generation),Java 8之后叫做元空間),具體說(shuō)是類(lèi)加載后存放在元空間的一部分內(nèi)存。
在Java程序的編譯階段,常量池會(huì)保存各種字面量和符號(hào)引用,包括字符串、類(lèi)和接口的全限定名、字段和方法的名稱(chēng)和描述符等,這些信息在編譯后會(huì)被存放在class文件的常量池中,在運(yùn)行期間這些常量池依舊會(huì)存在并且Java根據(jù)常量池來(lái)映射參數(shù)。
所以,處于常量池中的變量也可以作為GC Roots來(lái)尋找垃圾
JNI 引用 (JNI References) JNI(Java Native Interface)引用是指在Java程序中通過(guò)JNI創(chuàng)建的與本地代碼(C++代碼,調(diào)用平臺(tái)相關(guān)函數(shù))相互調(diào)用的引用。JNI引用包括本地引用(Local Reference)、全局引用(Global Reference)和弱全局引用(Weak Global Reference)。
本地引用(Local Reference): 本地引用是一種短期的引用,用于限定其生命周期。當(dāng)Java方法調(diào)用本地方法時(shí),本地引用會(huì)被創(chuàng)建,但在本地方法返回后,這些引用將被自動(dòng)釋放。本地引用不能作為GC Roots。
全局引用(Global Reference): 全局引用是一種長(zhǎng)期有效的引用,可以在整個(gè)程序的生命周期內(nèi)使用。全局引用可以防止被引用對(duì)象被垃圾回收,因此它可以作為GC Roots。
弱全局引用(Weak Global Reference): 弱全局引用也是一種全局引用,但它對(duì)被引用對(duì)象的生命周期沒(méi)有強(qiáng)制影響。如果一個(gè)對(duì)象只被弱全局引用引用,那么它在垃圾回收時(shí)可能被回收。弱全局引用不能作為GC Roots。
JNI引用之所以能作為GC Roots,是因?yàn)樗鼈兛梢栽诒镜胤椒ǎ–++方法,調(diào)用平臺(tái)相關(guān)函數(shù))中持有Java對(duì)象的引用,防止這些對(duì)象在本地方法執(zhí)行期間被垃圾回收。全局引用在整個(gè)程序的生命周期內(nèi)有效,因此它們有可能成為根引用,即GC Roots。
根可達(dá)算法原理
知道了什么是GC Roots那么根可達(dá)算法理解起來(lái)就相對(duì)來(lái)說(shuō)會(huì)簡(jiǎn)單一些。GC Roots我們可以簡(jiǎn)單理解為和Java程序的生命周期強(qiáng)關(guān)聯(lián)、和JVM生命周期強(qiáng)關(guān)聯(lián)或者和當(dāng)前線程強(qiáng)關(guān)聯(lián)的一些對(duì)象。這些對(duì)象至少說(shuō)在發(fā)生GC這一時(shí)刻是不應(yīng)該被當(dāng)成垃圾回收掉的,否則會(huì)影響程序的正常使用,因此,我們標(biāo)記存活對(duì)象的時(shí)候從GC Roots開(kāi)始,認(rèn)為被GC Roots 引用或者間接引用的對(duì)象就是存活對(duì)象。因此,根可達(dá)算法的基本原理和流程如下:
初始根集合(Initial Roots): 根可達(dá)算法從程序的初始根集合開(kāi)始,這些根是一組特殊的引用,如線程棧中的引用、靜態(tài)變量、JNI(Java Native Interface)引用等。 標(biāo)記階段(Mark Phase): 算法通過(guò)追蹤根引用,遞歸遍歷對(duì)象圖,標(biāo)記所有可以從根引用訪問(wèn)到的對(duì)象。在這個(gè)過(guò)程中,被標(biāo)記的對(duì)象被認(rèn)為是可達(dá)的,而未被標(biāo)記的對(duì)象被認(rèn)為是不可達(dá)的。 標(biāo)記-清除階段(Mark-Sweep Phase): 在標(biāo)記完成后,算法執(zhí)行清除操作,即移除未被標(biāo)記的對(duì)象。這些未被標(biāo)記的對(duì)象被認(rèn)為是不可達(dá)的,可以被垃圾回收器回收。這個(gè)階段的目標(biāo)是回收不再被程序使用的內(nèi)存空間。 壓縮(Compaction)或整理(Compaction): 在某些情況下,為了優(yōu)化內(nèi)存布局,可能會(huì)執(zhí)行進(jìn)一步的操作,如將存活對(duì)象整理到一起,以減少內(nèi)存碎片。這個(gè)步驟通常與標(biāo)記-清除階段結(jié)合使用。 可選的再標(biāo)記階段(Optional Re-Mark Phase): 有些算法可能會(huì)在標(biāo)記-清除后執(zhí)行可選的再標(biāo)記階段,以處理在清除階段可能發(fā)生的并發(fā)引用更新。這一步確保在垃圾回收過(guò)程中引用關(guān)系的一致性。 結(jié)束(Finish): 垃圾回收算法完成后,內(nèi)存中只留下了可達(dá)對(duì)象,而不可達(dá)的對(duì)象已被清理。程序可以繼續(xù)執(zhí)行。
實(shí)際上來(lái)說(shuō),如CMS和G1之類(lèi)比較流行的垃圾回收器都是采用的“三色標(biāo)記”算法,而非直接采用的根可達(dá)算法來(lái)對(duì)垃圾進(jìn)行標(biāo)記的.
柚子快報(bào)邀請(qǐng)碼778899分享:JVM 根可達(dá)算法
推薦鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。