溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解Java jvm垃圾回收

發(fā)布時(shí)間:2021-10-26 14:34:24 來源:億速云 閱讀:100 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“如何理解Java jvm垃圾回收”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“如何理解Java jvm垃圾回收”吧!

    常見面試題

    • 如何判斷對(duì)象是否死亡

    • 簡單介紹一下強(qiáng)引用、軟引用、弱引用、虛引用

    • 如何判斷常量是一個(gè)廢棄常量

    • 如何判斷類是一個(gè)無用類

    • 垃圾收集有哪些算法、各自的特點(diǎn)?

    • 常見的垃圾回收器有哪些?

    • 介紹一下CMS,G1收集器?

    • minor gc和full gc有什么不同呢?

    如何理解Java jvm垃圾回收

    1.JVM內(nèi)存回收和分配

    1.1主要的區(qū)域?

    • 在伊甸區(qū)先產(chǎn)生對(duì)象

    • 然后發(fā)生一次gc之后去到幸存區(qū)幸存區(qū)

    • 如果年齡大于閾值那么就會(huì)升級(jí)到老年代

    閾值的計(jì)算

    如果某個(gè)年齡段的大小大于幸存區(qū)的一半,那么就取閾值或者是這個(gè)年齡最小的那個(gè)作為新的閾值升級(jí)到老年代

    • gc的時(shí)候是幸存區(qū)的from和伊甸區(qū)的存活對(duì)象復(fù)制到to,然后再清理其它的對(duì)象,接著from和to就會(huì)交換指針

    gc測(cè)試

    場(chǎng)景就是先給eden分配足量的空間,然后再申請(qǐng)大量空間,問題就是幸存區(qū)的空間不夠用

    • 那么這個(gè)時(shí)候就會(huì)觸發(fā)分配擔(dān)保機(jī)制,把多余的對(duì)象分配到老年代,而不會(huì)觸發(fā)full gc。仍然還是monor gc

    public class GCTest {
        public static void main(String[] args) {
            byte[] allocation1, allocation2;
            allocation1 = new byte[50900*1024];
            allocation2 = new byte[9500*1024];
        }
    }

    如何理解Java jvm垃圾回收

    1.2大對(duì)象進(jìn)入老年代

    • 防止在標(biāo)記復(fù)制的時(shí)候占用大量的時(shí)間,降低gc的效率

    1.3長期存活的對(duì)象進(jìn)入老年代

    • 每次gc都會(huì)把eden和from的存活對(duì)象放到to,每次gc存活年齡就會(huì)+1,如果超過閾值那么就能夠升級(jí)到老年代,設(shè)置的參數(shù)是-XX:MaxTenuringThreshold

    • 下面是計(jì)算的方式,每個(gè)年齡的人數(shù)累加,累加一個(gè)就+1,如果對(duì)象數(shù)量大于幸存區(qū)的一半的時(shí)候就需要更新閾值(新計(jì)算的age和MaxTenuringThreshold)

    • 通常晉升閾值是15,但是CMS是6

    uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
        //survivor_capacity是survivor空間的大小
        size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
        size_t total = 0;
        uint age = 1;
        while (age < table_size) {
            //sizes數(shù)組是每個(gè)年齡段對(duì)象大小
            total += sizes[age];
            if (total > desired_survivor_size) {
                break;
            }
            age++;
        }
        uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
        ...
    }

    1.4主要進(jìn)行g(shù)c的區(qū)域

    gc的類型
    • Partial Gc

    Young Gc:收集新生代的

    Old Gc:只收集老年代的

    Mixed Gc:新生代和部分老年代

    • Full Gc:新生代,老年代都會(huì)收集

    Young Gc
    • 每次都是收集新生代的,并且晉升那些存活久的

    Full Gc
    • 如果發(fā)現(xiàn)幸存區(qū)要晉升的對(duì)象內(nèi)存空間比老年代內(nèi)存空間更大那么就進(jìn)行full Gc。有的虛擬機(jī)會(huì)先進(jìn)行young gc來清理掉一些,減少full gc的時(shí)間消耗

    1.5空間分配擔(dān)保?

    • jdk1.6之前需要判斷老年代剩余的空間是不是完全大于新生代的空間,如果是那么才能進(jìn)行minorgc保證不會(huì)出現(xiàn)問題。如果是不行就會(huì)去檢查-XX:handlePromotionFailure也就是晉升的對(duì)象平均大小是不是小于老年代剩余空間,如果是那么就直接minor gc否則就full gc

    • jdk1.6之后直接檢查新生代晉升平均大小如果小于老年代那么就會(huì)直接晉升

    2.對(duì)象已經(jīng)死亡?

    如何理解Java jvm垃圾回收

    2.1引用計(jì)數(shù)法

    • 其實(shí)就是每次被引用那么計(jì)數(shù)+1,如果計(jì)數(shù)不是0那么就不會(huì)被回收

    • 但是不使用的原因就是循環(huán)引用依賴,如果兩個(gè)對(duì)象互相引用就會(huì)導(dǎo)致計(jì)數(shù)永遠(yuǎn)不會(huì)為0

    2.2可達(dá)性分析

    • Gc roots作為起點(diǎn)一直往下面的一條引用鏈

    gc Roots的對(duì)象

    • 虛擬機(jī)棧引用的對(duì)象(棧的本地局部變量表)

    • 本地方法棧引用的對(duì)象

    • 方法區(qū)常量引用的對(duì)象(常量池引用的對(duì)象)

    • 方法區(qū)靜態(tài)屬性引用的對(duì)象

    • 被同步鎖持有的對(duì)象

    • java虛擬機(jī)內(nèi)部引用,比如Integer這些基本類型的

    如何理解Java jvm垃圾回收

    2.3再談引用

    • 強(qiáng)引用:垃圾回收器不會(huì)對(duì)他進(jìn)行回收

    • 軟引用:內(nèi)存空間不足會(huì)回收

    • 弱引用:gc就回收

    • 虛引用:隨時(shí)會(huì)被回收而且需要引用隊(duì)列

    虛引用、軟引用、弱引用的區(qū)別?
    • 虛引用的對(duì)象在gc之前會(huì)被送到引用隊(duì)列,并且程序在對(duì)象回收之前做相應(yīng)的活動(dòng)(臨死之前的處理)

    • 軟引用是用的最多的,可以提高gc的效率,維護(hù)系統(tǒng)安全,防止內(nèi)存溢出

    2.4不可達(dá)對(duì)象不一定回收

    • 在回收之前會(huì)對(duì)對(duì)象進(jìn)行一次標(biāo)記,看是否會(huì)執(zhí)行finalize方法。如果沒有那么這些對(duì)象將會(huì)先被回收

    • 如果有那么進(jìn)行第二次標(biāo)記,讓對(duì)象執(zhí)行finalize之后再進(jìn)行回收

    2.5如何判斷一個(gè)常量是廢棄常量?

    • 如果常量池對(duì)象沒有被任何對(duì)象引用就會(huì)被回收

    • jdk1.7之前運(yùn)行時(shí)常量池包含字符串常量池,需要進(jìn)行復(fù)制來返回新的引用(堆有一個(gè),常量池有一個(gè))

    • jdk1.7的時(shí)候字符串池已經(jīng)不在運(yùn)行時(shí)常量池,如果調(diào)用intern就會(huì)把當(dāng)前對(duì)象放入常量池并且返回引用(只有常量池有一個(gè))。如果本來就存在就會(huì)返回對(duì)象實(shí)例的地址。

    • jdk1.8之后運(yùn)行時(shí)常量池已經(jīng)轉(zhuǎn)移到了元空間

    2.6如果判斷一個(gè)類沒有用?

    • 類的實(shí)例都回收了

    • 類的類加載器回收了

    • 類信息沒有被引用

    • 大量的反射和動(dòng)態(tài)代理生成類信息會(huì)對(duì)方法區(qū)產(chǎn)生很大的壓力

    3.垃圾回收算法

    hotspot為什么要區(qū)分老年代和新生代?

    原因就是不同的存活對(duì)象需要不同的垃圾回收算法

    • 如果新生代用的是標(biāo)記整理,問題就是每次清除大量的對(duì)象,移動(dòng)時(shí)間很長,整理消耗很大。但是標(biāo)記復(fù)制就很快,因?yàn)榇婊顚?duì)象少

    • 但是老年代如果使用標(biāo)記整理就很好,因?yàn)榇婊疃嘁苿?dòng)少,復(fù)制就相反

    • 不能夠統(tǒng)一設(shè)計(jì)為弱分代假說和強(qiáng)分代假說

    跨代收集假說?

    如果老年代和新生代互相引用,新生代的年齡就會(huì)被拉長。但是為了知道新生代什么時(shí)候被gc,這個(gè)時(shí)候可以給新生代加上一個(gè)記憶集(把老年代劃分為很多個(gè)格子,代表誰引用了我),避免掃描整個(gè)老年代

    4.垃圾回收器

    4.1Serial收集器

    • 單線程收集器,每次都要阻塞其它線程(STW),一個(gè)垃圾線程單獨(dú)回收

    • 新生代是標(biāo)記復(fù)制,老年代是標(biāo)記整理

    • 它簡單高效,沒有和其它線程交換不會(huì)產(chǎn)生并發(fā)問題

    • 但是STW會(huì)導(dǎo)致響應(yīng)很慢

    4.2ParNew收集器

    • Serial的多線程版本,但是還是會(huì)STW

    • 新生代是標(biāo)記復(fù)制,老年代是標(biāo)記整理

    4.3Parallel Scavenge收集器

    • 新生代是標(biāo)記復(fù)制,老年代是標(biāo)記整理

    • 和ParNew不同的地方就是它完全關(guān)注cpu的利用率,也就是處理任務(wù)的吞吐量,而不會(huì)管STW到底停多久

    4.4SerialOld

    • Serial的老年代版本,1.5以前和Parallel Scavenge一起使用,還有別的用途就是CMS的后備方案

    4.5Parallel Old收集器

    • Parallel Scavenge收集器的老年代也是注重吞吐量

    4.6CMS收集器

    • 注重最小響應(yīng)時(shí)間

    • 垃圾收集器和用戶線程同時(shí)工作

    • 初始標(biāo)記記錄gc root直接相連的對(duì)象

    • 并發(fā)標(biāo)記遍歷整個(gè)鏈,但是可以和用戶線程并發(fā)運(yùn)行

    • 重新標(biāo)記修正那些更新的對(duì)象的引用鏈,比并發(fā)標(biāo)記短

    • 并發(fā)清除

    問題?

    內(nèi)存碎片多對(duì)cpu資源敏感

    如何理解Java jvm垃圾回收

    4.7G1收集器

    同時(shí)滿足響應(yīng)快處理多的問題

    特點(diǎn)

    • 并行和并發(fā),使用多個(gè)cpu執(zhí)行g(shù)c線程來縮短stw,而且還能與java線程并發(fā)執(zhí)行

    • 分代收集

    • 空間整合:大部分時(shí)候使用標(biāo)記復(fù)制

    • 可預(yù)測(cè)停頓:響應(yīng)時(shí)間快,可以設(shè)置stw時(shí)間

    • 分區(qū)之間的跨代引用,young這里使用了rset(非收集區(qū)指向收集區(qū))記錄,老年代那個(gè)區(qū)域指向了我,老年代使用了卡表劃分了很多個(gè)區(qū)域,那么minor gc的時(shí)候就不需要遍歷整個(gè)其它所有區(qū)域去看看當(dāng)前的區(qū)域的對(duì)象到底有沒有被引用。

    如何理解Java jvm垃圾回收

    補(bǔ)充字符串池的本質(zhì)

    第一個(gè)問題是String a="a"的時(shí)候做了什么?
    • 先去找常量池是否存在a如果存在那么就直接返回常量池的引用地址返回,如果不存在那么就創(chuàng)建一個(gè)在常量池然后再返回引用地址

    第二個(gè)問題new String(“a”)發(fā)生了什么?
    • 先看看常量池是否存在a,如果不存在創(chuàng)建一個(gè)在常量池,而且在堆單獨(dú)創(chuàng)建一個(gè)a對(duì)象返回引用(而不是返回常量池的),相當(dāng)于就是創(chuàng)建了兩次。

    • 如果第二次創(chuàng)建發(fā)現(xiàn)已經(jīng)存在就直接在堆中創(chuàng)建對(duì)象。

    第三個(gè)問題intern的原理?
    • 看看常量池有沒有這個(gè)字符串,沒有就創(chuàng)建并返回常量池對(duì)象的地址引用

    • 如果有那么直接返回常量池對(duì)象的地址引用

    String s1=new String(“a”)

    String s2=s1.intern();

    很明顯s1不等于s2如果上面的問題都清晰知道。s1引用的是堆,而s2引用的是常量池的

    第四個(gè)問題

    String s3=new String(“1”)+new String(“1”);

    String s5=s3.intern();

    String s4=“11”

    那么地方他們相等嗎?當(dāng)然是相等的,s3會(huì)把1存入常量池,但是不會(huì)吧11存入常量池因?yàn)?,還沒編譯出來。調(diào)用了intern之后才會(huì)把對(duì)象存入常量池,而這個(gè)時(shí)候存入的對(duì)象就是s3指向的那個(gè)。所以s4指向的也是s3的。如果是s0="11"的話那就不一樣了,s3.intern只會(huì)返回常量池的對(duì)象引用地址,而不是s3的,因?yàn)閟3是不能重復(fù)intern 11進(jìn)去的。jdk1.6的話那么無論怎么樣都是錯(cuò)的,intern是復(fù)制一份,而不是把對(duì)象存入常量池(因?yàn)樽址A砍卦诜椒▍^(qū),而jdk1.7它在堆所以可以很好的保存s3的引用)

    下面的代碼正確分析應(yīng)該是三個(gè)true,但是在test里面就會(huì)先緩存了11導(dǎo)致false, true,false的問題。

    @Test
    public void test4(){
        String s3 = new String("1") + new String("1");
        String s5 = s3.intern();
        String s4 = "11";
        System.out.println(s5 == s3);
        System.out.println(s5 == s4);
        System.out.println(s3 == s4);
        System.out.println("======================");
        String s6 = new String("go") +new String("od");
        String s7 = s6.intern();
        String s8 = "good";
        System.out.println(s6 == s7);
        System.out.println(s7 == s8);
        System.out.println(s6 == s8);
    }

    finalize的原理

    • 其實(shí)就是對(duì)象重寫了finalize,那么第一次gc的時(shí)候如果發(fā)現(xiàn)有finalize,就會(huì)把對(duì)象帶到F-Queue上面等待,執(zhí)行finalize方法進(jìn)行自救,下面就是一個(gè)自救過程,new了一個(gè)GCTest對(duì)象,這個(gè)時(shí)候test不引用了,那么正常來說這個(gè)GCTest就會(huì)被回收,但是它觸發(fā)了finalize的方法,最后再次在finalize中使用test引用它所以對(duì)象沒有被消除

    • 但是finalize是一個(gè)守護(hù)線程,防止有的finalize是個(gè)循環(huán)等待方法阻塞整個(gè)隊(duì)列,影響回收效率

    • 最后一次標(biāo)記就是在F-queue里面標(biāo)記這個(gè)對(duì)象(如果沒有引用)然后釋放

    • finalize實(shí)際上是放到了Finalizer線程上實(shí)現(xiàn)。然后然引用隊(duì)列指向這個(gè)雙向鏈表,一旦遇到gc,那么就會(huì)調(diào)用ReferenceHandler來處理這些節(jié)點(diǎn)的finalize調(diào)用,調(diào)用之后斷開節(jié)點(diǎn),節(jié)點(diǎn)就會(huì)被回收了

    • finalize上鎖導(dǎo)致執(zhí)行很慢

    public class GCTest {
        static GCTest test;
        public void isAlive(){
            System.out.println("我還活著");
        }
        @Override
        protected void finalize() throws Throwable {
            System.out.println("我要死了");
            test=this;
        }
        public static void main(String[] args) throws InterruptedException {
           test = new GCTest();
            test=null;
            System.gc();
            Thread.sleep(500);
            if(test!=null){
                test.isAlive();
            }else{
                System.out.println("死了");
            }
            test=null;
            System.gc();
            if(test!=null){
                test.isAlive();
            }else{
                System.out.println("死了");
            }
        }
    }

    感謝各位的閱讀,以上就是“如何理解Java jvm垃圾回收”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)如何理解Java jvm垃圾回收這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI