1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        有沒(méi)有搞錯(cuò)?Java 對(duì)象不使用時(shí),要賦值為 null?

        共 7205字,需瀏覽 15分鐘

         ·

        2021-05-06 09:58

        來(lái)源 | olarxiong.com/category/java/

        前言

        最近,聽(tīng)粉絲討論說(shuō),“不使用的對(duì)象應(yīng)手動(dòng)賦值為null“ 這句話(huà),而且好多開(kāi)發(fā)者一直信奉著這句話(huà);問(wèn)其原因,大都是回答“有利于GC更早回收內(nèi)存,減少內(nèi)存占用”,但再往深入問(wèn)就回答不出來(lái)了。

        鑒于網(wǎng)上有太多關(guān)于此問(wèn)題的誤導(dǎo),本文將通過(guò)實(shí)例,深入JVM剖析“對(duì)象不再使用時(shí)賦值為null”這一操作存在的意義,供君參考。本文盡量不使用專(zhuān)業(yè)術(shù)語(yǔ),但仍需要你對(duì)JVM有一些概念。

        示例代碼

        我們來(lái)看看一段非常簡(jiǎn)單的代碼:

        public static void main(String[] args) {
                if (true) {
                byte[] placeHolder = new byte[64 * 1024 * 1024];
                System.out.println(placeHolder.length / 1024);
                }
                System.gc();
                }

        我們?cè)趇f中實(shí)例化了一個(gè)數(shù)組placeHolder,然后在if的作用域外通過(guò)System.gc();手動(dòng)觸發(fā)了GC,其用意是回收placeHolder,因?yàn)?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;border-radius: 4px;color: rgb(53, 179, 120);background-color: rgba(27, 31, 35, 0.047);word-break: break-all;">placeHolder已經(jīng)無(wú)法訪(fǎng)問(wèn)到了。來(lái)看看輸出:

        65536
        [GC 68239K->65952K(125952K), 0.0014820 secs]
        [Full GC 65952K->65881K(125952K), 0.0093860 secs]

        Full GC 65952K->65881K(125952K)代表的意思是:本次GC后,內(nèi)存占用從65952K降到了65881K。意思其實(shí)是說(shuō)GC沒(méi)有將placeHolder回收掉,是不是不可思議?

        下面來(lái)看看遵循“不使用的對(duì)象應(yīng)手動(dòng)賦值為null“的情況:

        public static void main(String[] args) {
                if (true) {
                byte[] placeHolder = new byte[64 * 1024 * 1024];
                System.out.println(placeHolder.length / 1024);
                placeHolder = null;
                }
                System.gc();
                }

        其輸出為:

        65536
        [GC 68239K->65952K(125952K), 0.0014910 secs]
        [Full GC 65952K->345K(125952K), 0.0099610 secs]

        這次GC后內(nèi)存占用下降到了345K,即placeHolder被成功回收了!對(duì)比兩段代碼,僅僅將placeHolder賦值為null就解決了GC的問(wèn)題,真應(yīng)該感謝“不使用的對(duì)象應(yīng)手動(dòng)賦值為null“。

        等等,為什么例子里placeHolder不賦值為null,GC就“發(fā)現(xiàn)不了”placeHolder該回收呢?這才是問(wèn)題的關(guān)鍵所在。

        運(yùn)行時(shí)棧

        典型的運(yùn)行時(shí)棧

        如果你了解過(guò)編譯原理,或者程序執(zhí)行的底層機(jī)制,你會(huì)知道方法在執(zhí)行的時(shí)候,方法里的變量(局部變量)都是分配在棧上的;當(dāng)然,對(duì)于Java來(lái)說(shuō),new出來(lái)的對(duì)象是在堆中,但棧中也會(huì)有這個(gè)對(duì)象的指針,和int一樣。

        比如對(duì)于下面這段代碼:

        public static void main(String[] args) {
                int a = 1;
                int b = 2;
                int c = a + b;
                }

        其運(yùn)行時(shí)棧的狀態(tài)可以理解成:

        索引變量
        1a
        2b
        3c

        “索引”表示變量在棧中的序號(hào),根據(jù)方法內(nèi)代碼執(zhí)行的先后順序,變量被按順序放在棧中。

        再比如:

        public static void main(String[] args) {
                if (true) {
                int a = 1;
                int b = 2;
                int c = a + b;
                }
                int d = 4;
                }

        這時(shí)運(yùn)行時(shí)棧就是:

        索引變量
        1a
        2b
        3c
        4d

        容易理解吧?其實(shí)仔細(xì)想想上面這個(gè)例子的運(yùn)行時(shí)棧是有優(yōu)化空間的。

        Java的棧優(yōu)化

        上面的例子,main()方法運(yùn)行時(shí)占用了4個(gè)棧索引空間,但實(shí)際上不需要占用這么多。當(dāng)if執(zhí)行完后,變量a、bc都不可能再訪(fǎng)問(wèn)到了,所以它們占用的1~3的棧索引是可以“回收”掉的,比如像這樣:

        索引變量
        1a
        2b
        3c
        1d

        變量d重用了變量a的棧索引,這樣就節(jié)約了內(nèi)存空間。

        提醒

        上面的“運(yùn)行時(shí)?!焙汀八饕笔菫榉奖阋攵室獍l(fā)明的詞,實(shí)際上在JVM中,它們的名字分別叫做“局部變量表”和“Slot”。而且局部變量表在編譯時(shí)即已確定,不需要等到“運(yùn)行時(shí)”。還請(qǐng)注意

        GC一瞥

        這里來(lái)簡(jiǎn)單講講主流GC里非常簡(jiǎn)單的一小塊:如何確定對(duì)象可以被回收。另一種表達(dá)是,如何確定對(duì)象是存活的。

        仔細(xì)想想,Java的世界中,對(duì)象與對(duì)象之間是存在關(guān)聯(lián)的,我們可以從一個(gè)對(duì)象訪(fǎng)問(wèn)到另一個(gè)對(duì)象。如圖所示。

        GC Roots

        再仔細(xì)想想,這些對(duì)象與對(duì)象之間構(gòu)成的引用關(guān)系,就像是一張大大的圖;更清楚一點(diǎn),是眾多的樹(shù)。

        如果我們找到了所有的樹(shù)根,那么從樹(shù)根走下去就能找到所有存活的對(duì)象,那么那些沒(méi)有找到的對(duì)象,就是已經(jīng)死亡的了!這樣GC就可以把那些對(duì)象回收掉了。

        現(xiàn)在的問(wèn)題是,怎么找到樹(shù)根呢?JVM早有規(guī)定,其中一個(gè)就是:棧中引用的對(duì)象。也就是說(shuō),只要堆中的這個(gè)對(duì)象,在棧中還存在引用,就會(huì)被認(rèn)定是存活的。

        提醒

        上面介紹的確定對(duì)象可以被回收的算法,其名字是“可達(dá)性分析算法”。

        JVM的“bug”

        我們?cè)賮?lái)回頭看看最開(kāi)始的例子:

        public static void main(String[] args) {
                if (true) {
                byte[] placeHolder = new byte[64 * 1024 * 1024];
                System.out.println(placeHolder.length / 1024);
                }
                System.gc();
                }

        看看其運(yùn)行時(shí)棧:

        LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  args   [Ljava/lang/String;
            5      12     1 placeHolder   [B

        棧中第一個(gè)索引是方法傳入?yún)?shù)args,其類(lèi)型為String[];第二個(gè)索引是placeHolder,其類(lèi)型為byte[]。

        聯(lián)系前面的內(nèi)容,我們推斷placeHolder沒(méi)有被回收的原因:System.gc();觸發(fā)GC時(shí),main()方法的運(yùn)行時(shí)棧中,還存在有對(duì)argsplaceHolder的引用,GC判斷這兩個(gè)對(duì)象都是存活的,不進(jìn)行回收。 也就是說(shuō),代碼在離開(kāi)if后,雖然已經(jīng)離開(kāi)了placeHolder的作用域,但在此之后,沒(méi)有任何對(duì)運(yùn)行時(shí)棧的讀寫(xiě),placeHolder所在的索引還沒(méi)有被其他變量重用,所以GC判斷其為存活。

        為了驗(yàn)證這一推斷,我們?cè)?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;border-radius: 4px;color: rgb(53, 179, 120);background-color: rgba(27, 31, 35, 0.047);word-break: break-all;">System.gc();之前再聲明一個(gè)變量,按照之前提到的“Java的棧優(yōu)化”,這個(gè)變量會(huì)重用placeHolder的索引。

        public static void main(String[] args) {
                if (true) {
                byte[] placeHolder = new byte[64 * 1024 * 1024];
                System.out.println(placeHolder.length / 1024);
                }
                int replacer = 1;
                System.gc();
                }

        看看其運(yùn)行時(shí)棧:

        LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            5      12     1 placeHolder   [B
           19       4     1 replacer   I

        不出所料,replacer重用了placeHolder的索引。來(lái)看看GC情況:

        65536
        [GC 68239K->65984K(125952K), 0.0011620 secs]
        [Full GC 65984K->345K(125952K), 0.0095220 secs]

        placeHolder被成功回收了!我們的推斷也被驗(yàn)證了。

        再?gòu)倪\(yùn)行時(shí)棧來(lái)看,加上int replacer = 1;和將placeHolder賦值為null起到了同樣的作用:斷開(kāi)堆中placeHolder和棧的聯(lián)系,讓GC判斷placeHolder已經(jīng)死亡。

        現(xiàn)在算是理清了“不使用的對(duì)象應(yīng)手動(dòng)賦值為null“的原理了,一切根源都是來(lái)自于JVM的一個(gè)“bug”:代碼離開(kāi)變量作用域時(shí),并不會(huì)自動(dòng)切斷其與堆的聯(lián)系。為什么這個(gè)“bug”一直存在?你不覺(jué)得出現(xiàn)這種情況的概率太小了么?算是一個(gè)tradeoff了。

        總結(jié)

        希望看到這里你已經(jīng)明白了“不使用的對(duì)象應(yīng)手動(dòng)賦值為null“這句話(huà)背后的奧義。我比較贊同《深入理解Java虛擬機(jī)》作者的觀(guān)點(diǎn):在需要“不使用的對(duì)象應(yīng)手動(dòng)賦值為null“時(shí)大膽去用,但不應(yīng)當(dāng)對(duì)其有過(guò)多依賴(lài),更不能當(dāng)作是一個(gè)普遍規(guī)則來(lái)推廣。


        點(diǎn)擊閱讀全文前往微服務(wù)電商教程
        瀏覽 72
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日逼图| 国产XXXX| 欧美裸体视频| 97无码精品人妻一区二区三区| 成人免费版欧美州| 国产成人内射| 精品欧美一区二区三区久久久| 波多野结衣黄色| 天天躁狠狠躁av| 亚洲www视频|