這是why技術的第37篇原創文章
老規矩,先聊聊生活,上面這張圖片是我周一拍的。
周一晚上下班后發現公司樓下推着三輪車賣花的阿姨又開始買花了。整個路口只有她一個人在做生意,整條路上也沒有幾個人,大家都低着頭匆匆走着,繁花中帶着點憂傷。
於是,我去買了一把白玫瑰。
上周日把《霍亂時期的愛情》看完了,就剛好當道具拍了上面的照片。總體來說我不喜歡這種縱情聲色的故事,更不喜歡那個看起來冠冕堂皇的理由∶“我一生有622個情人,但是我只愛過你”。雖然它真的是窮極了愛情的所有可能性,但是它不夠真實。
相比之下我覺得錢鍾書先生寫的《圍城》∶“我說的讓她三分,不是三分流水七分塵的三分,而是天下明月只有三分的三分。”這樣打打鬧鬧的愛情更加真實。
再看楊絳先生的《我們仨》,書的最后她說∶“世間好物不堅牢,彩雲易散琉璃脆”。這才是愛情,這才是真實的生活。
好了,說回文章。
對不起,我錯了。
前面發的這兩篇文章:
里面有一些沒有說清楚的地方,又有很多讀者來問,所以我覺得需要補充說明一下。
更重要的是,經過高手指點,其中還有一些描述錯誤的地方,我也需要進行勘誤。
如果真的是面試題,可能面試官就會對我說:好了,我們今天就先到這里。你回去等通知吧。
如果你沒看過我剛剛說的兩篇文章,我建議你不要看這篇,因為一看就得看三篇,如果里面的衍生知識點你還想徹底弄明白,一個下午就過去了......(當然,你看了后收獲肯定還是有的。)
如果你看了我之前的兩篇文章,我求求你一定看看這篇,補充、更正一下答案,等面試官真的問起細節來,也不怕......
好了,在閱讀本文之前,我假設你已經讀過我前面說的兩篇優質、幽默、有料的文章了。
並發的可達性分析-勘誤
之前發布了這篇文章《面試官:你說你熟悉jvm?那你講一下並發的可達性分析》,對於文中這一部分內容中的動圖,有很多朋友給我說看不懂:
我把這個動圖拿出來:
首先,需要說明的是,我現在也看不懂這個動圖了。(畫錯了就是畫錯了,還強行找個理由)。
接下來,忘記這個動圖,我們重新分析一波原始快照方案(以下簡稱SATB,Snapshot At The Beginning)。
首先,我們看初始標記階段(即根節點枚舉)完成后,剛剛進入並發標記階段,GC 線程開始掃描時的對象圖:
在上面這張圖里,當GC Roots確定后,對象圖就已經確定了。SATB掃描的時候基於已經確定的對象圖(快照版的對象圖)掃描,也就是說掃描過程中上面的快照圖的引用關系是不會發生變化的,但是真實的對象圖是會發生變化的。
舉個例子:就類似於你在操場上拍了一張照片,你數照片里面的人數,照片是不會發生變化,人數一直都是這么多,但是真實的操場上的人是在時刻變化的。
所以,在對象圖確定的一刻,正常掃描完成后,對象圖變成了下面這樣:
好了,面前的鋪墊完成了。
我們這里需要演示的是“對象消失”情況。
首先,我們先確定一下上面展示的對象圖,在並發標記階段必然有一個時刻的對象圖是這樣的:
我們基於這個時刻的這個對象圖去討論“對象消失”的問題。
還得記得"對象消失"必須同時滿足的兩個條件嗎?(這兩個條件是摘抄自《深入理解Java虛擬機(第3版)》P.89)
條件一:賦值器插入了一條或者多條從黑色對象到白色對象的新引用。
條件二:賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。
我們再仔細的讀一遍第二個條件,你會發現,它說的是**“該白色對象”。這個“該白色對象”指的是條件一里面的白色對象。**
所以,我們有理由相信:條件一和條件二是有先后順序的,即必須是賦值器插入了一條或者多條從黑色對象到白色對象的新引用,然后賦值器又刪除了全部從灰色對象到該白色對象的直接或間接引用。在這樣的情況下,才會出現“對象消失”的情況。
經過高人指點,我們還可以進行反證法,如下:
我們假設灰色對象到白色對象的引用先刪除了,即先觸發了條件二。那么對應的這個時刻真實的對象圖將變成下面的樣子:
(注意我這里強調的是真實的對象圖,而不是快照的對象圖。再次重申:快照的對象圖在掃描開始的時候就確定了,掃描過程中是不會變化的。)
那么,白色對象9是處於游離態的,從根節點沒有任何引用鏈相連,用圖論的話來說就是從 GC Root 到對象9不可達,則證明此對象是不可能再被使用的。因此用戶線程不可能把黑色對象5指向游離態的白色對象9,你寫不出這樣的代碼來。
如果說上面的圖你一眼沒看出來,那么請看下面這圖,是不是恍然大悟:
黑色對象5不能指向白色對象9,那么第一條規則就滿足不了了。
所以,綜上我們可以得出:條件一和條件二是有先后順序的。
那么我們根據條件一繼續做圖如下:
條件一是賦值器插入了一條或者多條從黑色對象到白色對象的新引用。
在上面這個圖的場景中,就是 GC 線程在工作的同時,賦值器插入了一條黑色對象5到白色對象9之間的新引用。(用紅色線條以示區分)
在這個時刻,由於灰色對象6指向白色對象9,所以黑色對象5可以指向白色對象9,想一想我們前面的證明,只要有引用鏈,黑色對象就可以到達白色對象。
這個時候僅僅滿足了條件一,對象還沒消失。
接下來就是條件二的圖,STAB破壞的就是條件二:
條件二是賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。
在上面這個圖的場景中,就是賦值器刪除了灰色對象6到白色對象9的直接引用。
這個時候白色對象9就是“消失的對象”了,因為黑色的對象5是不會被再次掃描的。
需要注意的是,賦值器可以理解為用戶線程,由於在並發標記階段,用戶線程和 GC 線程在同時運行,所以需要出現上面的圖,還有一個前置條件就是:
用戶線程刪除對象6到對象9之間的引用,要先於 GC 線程掃描到對象6,把對象6變成灰色的操作。因為只有這樣,GC 線程處理到對象6的時候,才有對應的寫屏障記錄。
如果在 GC 線程已經掃描過對象6,即對象6已經是黑色的情況下(這個時候對象9,不是黑色就是灰色,不可能是白色),用戶線程再去刪除對象6到對象9之間的引用,GC 線程是不需要處理的,因為對象9已經是非白了,它在本輪中必定會活下來。
這里我引用R大的描述:
https://hllvm-group.iteye.com/group/topic/44381?page=2
因為刪除操作會觸發 pre-write barrier,把每次引用關系變化時舊的引用值記下來,只有這樣,等 GC 線程到達某一個對象時,這個對象的所有引用類型字段的變化全都有記錄在案,就不會漏掉任何在快照圖里活的對象。當然,很可能有對象在快照中是活的,但隨着並發 GC 的進行它可能本來已經死了,但 SATB 還是會讓它活過這次 GC,變成了浮動垃圾。
SATB 在寫屏障里,把舊的引用所指向的對象都變成非白的(已經黑灰就不用管,還是白的就變成灰的)。
這樣做的實際效果是:如果一個灰對象的字段原本指向一個白對象,但在concurrent marker能掃描到這個字段之前,這個字段被賦上了別的值(例如說null),那么這個字段跟白對象之間的關聯就被切斷了。SATB write barrier保證在這種切斷發生之前就把字段原本引用的對象變灰,從而杜絕了上述條件二的發生。
其中:“把舊的引用所指向的對象都變成非白的。”在我們這個場景下含義如下:
舊的引用指的是:灰色對象6到白色對象9之間的引用。
所指向的對象指的是:白色對象9。
都變成非白的:指的是白色對象9變成了灰色。
所以,在兩個條件順序觸發、對象圖掃描完成后會變成下面的樣子:
並發掃描結束之后,再以灰色對象9為根(把它作為根,自然會變成黑色),重新掃描一次,所以最終的對象圖變成了這樣:
有的小伙伴就會問了:如果在標記過程中,用戶線程並沒有把對象5指向對象9的操作,僅僅是發生了刪除對象6到對象9之間引用的操作,那么這個對象圖是什么樣子呢?
就是下面這個樣子,你應該可以想象出來:
對象9還是黑色,只是它變成了浮動垃圾,逃過了本次回收而已。並不影響程序運行。
接下來,讓上面的圖動起來,並且我把圖片之間的切換順序放慢。你再自己細品品:
所以,上面的全部描述,才是一次我認為正確的,展示SATB方案是如何解決“對象消失”問題的過程。
之前《面試官:你說你熟悉jvm?那你講一下並發的可達性分析》中對於這一部分的描述過於簡單,且存在錯誤,給大家道歉,並特以此文進行修正。
你是什么時候的垃圾-勘誤
在《G1回收器:我怎么知道你是什么時候的垃圾?》這篇文章中有一句描述是這樣的:
“GC Roots 能直接關聯到的對象:就是一個 Region 已經使用過的部分,所以在 bottom 與 top 之間。”這句話是錯誤的。
實際上,通過文章后面的描述你也能發現。GC Roots 能直接關聯到的對象集合應該“小於” Region 已經使用過的部分,對象圖遞歸完之后,所有對象總和,才等於Region已經使用過的部分。
通過文章中后半部分的這個圖片也可以直觀的發現, bottom 到 top 之間是一個 Region 已經使用的部分。但是這一部分中,只有 bottom 到 NextTAMS 之間的對象才是 GC Roots 能直接關聯到的對象,這部分對象並不是一個 Region 已經使用過的部分。
你是什么時候的垃圾-補充說明
關於《G1回收器:我怎么知道你是什么時候的垃圾?》這篇文章,還有兩個需要補充說明的地方。
有的讀者問說:文章中沒有討論回收的內容,每次清理不會真正回收,那是不是多輪標記后才發生一次回收呢?
一。
首先,文章中確實沒有討論回收相關的內容。我在前面部分也寫了,把G1回收切分為兩大部分:
1.Global Concurrent Marking:全局並發標記。
2.Evacuation Pauses:該階段是負責把一部分Region里的活對象拷貝到空Region里面去,然后回收原本的Region空間。
只要清楚了全局並發標記階段,就可以解答文中拋出的這個問題:
所以我只說明了全局並發標記階段。
如果想要了解回收階段的事,可以去看看R大的回答,強烈建議你看完本文,點個贊后,打開下面的鏈接,反復閱讀幾遍:
https://hllvm-group.iteye.com/group/topic/44381
其次,“每次清理不會真正回收,那是不是多輪標記后才發生一次回收呢?”
這句話,可能是我在文章強調了清理階段不拷貝任何對象,再加上沒有描述回收階段,導致讀者有點懵了吧。
一次全局並發標記完成后,緊接着一次回收的過程。
只是G1收集器之所以能建立可預測的停頓時間模型(-XX:MaxGCPauseMillis指定,默認值為200毫秒),是因為它將 Region 作為單次回收的最小單元,即每次收集到的內存空間都是 Region 大小的整數倍,這樣就可以有計划地避免在整個Java堆中進行全區域的垃圾回收。
更具體一點的做法就是每個 Region 里面堆積的垃圾都有一個“價值”(價值即回收所獲得的空間大小以及回收所需要的時間的經驗值)。而這些“價值”,是維護在一個優先級列表中的,G1收集器都是知道的。
所以回收階段會優先處理回收價值最大的那些 Region。因此,一次回收的過程並不會回收所有的 Region。
二。
這里也就解釋了讀者提出的另外一個問題:如果每次標記完都會回收整理,那為什么紅框所在的區間與上一次標記之后相同,好像沒有被整理一樣,整理之后不是應該不留下內存空隙嗎?
我覺得一個合理的解釋,就是我上面說的:這個 Region 的價值不夠,所以它本次沒有被回收。隨着時間的推移,它里面堆積的垃圾越來越多,“價值”就越來越高,總是會被回收的。
還有讀者問:看了並發標記的過程,有個疑問 prevBitmap 的作用是什么? 因為感覺每次都是從頭開始掃描,沒看到它的作用。
這個問題,可以從這張圖片入手解答:
這個 E 是 Remark 階段,可以看到,在這個階段,其實 PrevBitmap 是派上用場了。
前面剛剛說了,這個 Region 由於“價值”不夠,它逃過了上次垃圾回收,所以待到下次垃圾回收的時候,就是 prevBitmap 的用武之地了,它里面記錄的地址對應的區間就不需要再次標記了,因為這些地址對應的對象就已經是垃圾了。
我們可以假設 E 代表的是第 n 輪回收的過程的Remark階段。那么 PrevBitmap 就是第 n-1 輪的標記結果。
之前的文章說了:一個 previous Bitmap 記錄的是上一輪 Concurrent Marking 后的對象標記狀態,因為上一輪已經完成(上一輪就是第n-1輪),所以這個bitmap的信息可以直接使用。
可以直接使用的意思就是前面說的:它里面記錄的地址對應的區間就不需要再次標記了,因為這些地址對應的對象就已經是垃圾了。
到 F 圖里面,可以看到,當前的 F 圖是清理階段已經完成的狀態了:
判斷標准有二:
1.和 E 圖相比PrevBitmap 和 NextBitmap 已經交換了位置。
2.PrevBitmap 里面對應的地址的空間已經被標記為淺灰色了。
這個時候已經完成標記,PrevBitmap 又變成了第n-1次標記的結果。
你是什么垃圾-懟人
因為之前的文章已經發布了,所以我需要修改一下對應的內容。提醒后面的讀者,如果看到了文章,需要注意這些地方描述的有問題。
但是我在查找我文章的過程中發現了一些讓我很郁悶的事情,之前的文章,大都被剽竊了,我也見怪不怪,有時間就順手舉報一下了。
最過分的是下面這個:
這是一個百家號賬號,一字不差的抄我文章,還自己標注為“原創”?
我去寫了個評論:
他還不敢把評論放出來。
還有下面這個,你可長點心吧。你配的這張圖片,我倒是想在家拍,但是我拍不出來呀:
這樣的情況還有很多。說到底,就還是版權意識的問題。
版權問題,我之前在《訂閱號做了77天,我掙了487.52元》這篇文章里面聊過:
我的號不會傳播任何盜版資源,以前如此,現在如此,以后也會如此。
不做惡,就是最大的善。與君共勉。
所以我在此鄭重聲明,如果未經許可轉載我的文章,必須標明原文地址,且保留文末公眾號二維碼,否則我一定見一個舉報一個。
我先舉報你涉黃,引起工作人員的注意,再舉報你抄襲,讓工作人員懲罰你。
氣死我了。
最后說一句(求關注)
通過這件事我也再次感覺到了,看網上的野生文章(比如我的),要持有謹慎、懷疑、學習的態度。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。(我每篇技術文章都有這句話,我是認真的說的。)
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是why技術,一個不是大佬,但是喜歡分享,又暖又有料的四川好男人。
以上。
歡迎關注公眾號【why技術】,堅持輸出原創。分享技術、品味生活,願你我共同進步。