目錄
一 . 垃圾回收機制(GC)
二 . 垃圾回收的具體步驟?
(1)先找出誰是垃圾?
方案一:引用計數
方案二:可達性分析
(2)釋放垃圾的內存空間?
方案一:標記清除
方案二:復制算法
方案三:標記 — 整理
方案四:分代回收算法
一 . 垃圾回收機制(GC)
在我們上一期講了 Java 在運行時內存的各個區域,對于程序計數器、虛擬機棧、本地方法棧這三個部分區域而言,其生命周期與相關線程有關,隨線程而生,隨線程而滅。并且這三個區域的內存分配與回收具有確定性,因為當方法結束或線程結束時,內存就自然跟著線程回收了,
什么是垃圾回收機制呢?顧名思義,就是清理掉我們在 Java 堆中用完的、不用的對象實例,垃圾回收器在對堆進行垃圾回收前,首先要判斷這些對象哪些還 “ 活著 ”,哪些已經 “ 死去 ” 。
這個垃圾回收機制是很香的,程序員寫代碼放心大膽的 new ,JVM 會自動識別哪些 new 完的對象再也不用了,就會將其自動釋放掉。
像我們主流語言中絕大部分都是內置了 GC 的,例如?GO、Python、PHP、JS ...... 但是我們的C++ 卻不支持 GC ,這是為什么呢?
任何功能都是有代價的,C++ 的大佬們評估了風險之后,不愿意承擔,所以 C++ 并不支持 GC,其主要原因有:要想在JVM 中引入?GC 機制,就需要額外的邏輯,首先會消耗不少的 CPU 開銷,進行垃圾掃描和釋放,其次在進行 GC 的時候可能會觸發 STW(Stop The World)問題,導致程序卡頓。
二 . 垃圾回收的具體步驟?
(1)先找出誰是垃圾?
需要針對每個對象分別判定是否為 “ 垃圾 ”
方案一:引用計數
給每個對象分配一個計數器,這一計數器用來衡量該對象有多少個引用指向。每增加一個引用,計數器 + 1 ,每減少一個引用,計數器 - 1 ,當計數器減為 0 ,此時該對象就是 “ 垃圾?” 了。
但是這個方案在實施過程中存在著一些問題,例如會消耗額外的空間。假設我們 Test 類就只有一個 int 成員(4個字節),為了引入引用計數,少說得搞個 short(2字節),內存就多占了 50% 。其次很可能導致 “ 循環引用 ” 使得判定出錯,這跟我們之前學過的死鎖問題有些相似。循環引用也是有解決方案的,需要引入更多的機制,例如環路檢測(在 Python 中就使用的這種機制),但是這樣一來,代價就更大了。所以綜上所述,Java 并沒有采用這種引用計數的方法,而是另一種:
方案二:可達性分析
可達性分析主要就是 “ 用時間換空間 ” ,在 JVM 中,專門搞了一波線程,周期性的去掃描代碼中的所有對象,來判定每個對象是否 “?可達?”(可以被訪問到),對應的,不可達的對象,就會被視為垃圾。
可達性分析的起點稱為 GC root ,從 root 出發,盡可能的通過 root 訪問到更多的對象,相當于遍歷的過程,但嚴格來說,并不是樹的遍歷,而是圖的遍歷。一個程序中,GC root 不是只有一個,而是有很多很多,可以作為 GC root 的對象有三種:棧上的局部變量(引用類型)、方法區中,靜態成員變量(引用類型)、常量池引用指向的對象。把所有的 GC root 都遍歷一遍,針對每一個盡可能往下延伸。
(2)釋放垃圾的內存空間?
方案一:標記清除
標記清除法就是將需要回收的垃圾標記上,然后直接對其進行清理。
標記清除法主要有兩大缺陷,首先是效率問題,標記和清除這兩個過程效率都不高,其次是空間碎片問題,在清除之后會產生大量不連續的內存碎片,空間碎片太多可能導致以后在程序運行中需要分配較大對象時,就可能申請不了,因為無法找到足夠的連續的空間(申請內存一定是需要連續的空間)。盡量避免內存碎片,時釋放內存的關鍵,所以我們又有了第二種方案:
方案二:復制算法
復制算法就是將我們的內存空間一分為二,每次只使用一半,當我們進行垃圾清理的時候,將不是垃圾的對象拷貝到另一半中,并且確保拷貝的這些對象在這一半內存空間中是連續的,然后直接將原本那一半的內存空間全部釋放掉。
復制算法的缺點也很明顯:首先就是每次只能用一半的內存,內存空間利用率非常低,其次是如果存活下來的對象很多,復制的成本將會非常大。于是我們引入了第三種方案:
方案三:標記 — 整理
標記、整理非常類似與我們之前學習過的順序表,刪除中間元素。當我們需要對垃圾對象進行清理時,依次將還存活的對象往一端移動,將垃圾對象替換掉,最后直接清理掉端邊界以外的內存。
這里需要注意的是,這里對于存活對象搬運的開銷也不少。所以,綜上所述,其實并沒有哪一種方案能做到十全十美,在實際開發過程中,我們都是結合運用,取長補短,于是就有了分代回收:
方案四:分代回收算法
JVM 根據對象的 “ 年齡 ” ,將對象進行區分,年輕的我們叫做新生代,年老的我們稱作老年代。這種 “ 年齡?” 是怎樣劃分的呢?通過我們的可達性分析,周期性的,每次經過一輪的掃描對象仍然存活(不是垃圾),其年齡就 + 1 。
分代回收,是 JVM 的 GC 中的基本思想,當具體落實到 JVM 的實現層面上,JVM 還提供了許多種 “ 垃圾回收器?” ,這些垃圾回收器在具體實施中就會對分代回收做進一步的擴充和實現,如下:
CMS 的原理同樣也是采用分代回收,它的設計理念就是把整個 GC 過程拆分成多個階段,能和業務線程并發執行就盡量并發,從而盡可能的減少 STW 的耗時:
G1 就是把整個內存分成很多個小塊,不同的顏色(字母)就表示這一小塊是新生代(伊甸區 / 幸存區)還是老年區。進行 GC 的時候,不要求一個周期就將其中的所有內存都回收一遍,而是一輪 GC 只回收其中的一部分就好,這樣就可以很好的限制你一輪 GC 所花的時間,這樣就使得?STW 的耗時在一個可控的范圍之內。
OKK,咱們有關 JVM 的原理及其相關機制的內容板塊就說這么多了,這部分主要就是靠被,這些知識就是靠我們去背八股文這種來記,當然了,大家也需要理解記憶,并不是說硬背啦。就這樣吧,咱們下期再見咯,與諸君共勉!!!