天天看點

JVM垃圾回收(上)

其實在之前的專欄中就已經對GC已經了一定的介紹了,現在覺得從更基礎的知識進行其他角度的叙述。

因為是第二次寫關于GC的文章了,是以我也會說一下自己各方面的看法。

我們經常寫代碼,但是從來沒有注重過垃圾回收這種東西,因為這是JVM裡面自動去做的一種行為,是以可你當沒法做到手動那種理想化的精确,那麼就會帶來一系列的問題。

引用計數法和可達性分析

GC,顧名思義,就是将配置設定出去的但是沒有使用到,但是卻占用資源的東西去回收回來,以便再次配置設定這些資源。就像線程池一樣,也是為了能夠最大化的使用我們已有的資源。

那麼,什麼是垃圾呢?

垃圾指的就是,死亡的對象所占用的堆空間。那麼,問題就來了,什麼是死亡的對象?

有兩個方法可以幫助我們去辨認哪些是死亡的對象。

  • 引用計數法

它的做法是為每個對象都添加一個引用計數器,它的做法是為每一個對象添加一個引用計數器,用來統計指向該對象的引用個數。一旦某個對象的引用計數器為0,則說明這個對象就沒有價值了,可以回收了。

但是從具體的實作上來說,我們的目的是更新所有引用的過程,相應的增減目标對象的引用計數器。

除了需要額外的空間來存儲計數器,以及繁瑣的更新操作,引用計數器還有一個重大的漏洞,那就是無法處理循環引用的對象。

舉個例子,假如AB互相都進行引用了,除此之外,沒有任何地方引用到了AB,那麼AB其實就是個垃圾,但是你還回收不了,時間長了就容易OOM。

  • 可達性分析

目前JVM所使用的主流垃圾回收器是采用的可達性分析算法。這個算法的實質在于一系列GC ROOTS作為樹根,探索能被GC ROOTS所有能被引用到的對象,然後加入該集合中,然後,沒有被探索到的對象,預設為死亡,然後回收。

JVM垃圾回收(上)

那麼,什麼是GCROOTS呢?我們可以了解為由堆外指向堆内的引用,一般來說是如下幾種:

  1. Java方法棧幀中的局部變量
  2. 已加載類的靜态變量
  3. JNI handles
  4. 已啟動且未停止的Java線程

可達性分析可以解決引用計數法所不能解決的循環引用問題,舉例來說,即便對象a和b互相引用,隻要從GC ROOTS出發無法到達a或者b,那麼可達性分析便不會将它們加入存活對象合集中。

雖然可達性分析可以解決引用計數法裡面的一些問題,但是本質上還是存在一些問題的。

比如在多線程環境下,其他線程可能會更新已經通路過的對象中的引用,進而造成誤報(将引用設定為null)或者漏報(将引用設定為未引用的對象)。

雖然誤報沒有什麼傷害,這最多也就是讓jvm損失部分GC的機會,但是漏報會讓還在使用的對象被回收掉,直接導緻JVM崩潰。

Stop-the-world

如何解決漏報的問題呢?在JVM中,傳統的垃圾回收算法采用的是一種簡單粗暴的方法——Stop-The-World,停止非垃圾回收線程的工作,直到垃圾回收完畢,這也就造成了回收所謂的暫停時間(GC pause)

JVM 中的Stop-the-world 是通過安全點(safepoint)機制來實作的。當Java虛拟機收到Stop-the-world請求,它便會等待所有的線程都到達安全點,才會去請求線程獨占的任務。

對于上面的解釋,其實也有另一種解釋——安全詞。當垃圾回收的線程喊出了安全詞,那麼java虛拟機棧将不會再發生變化,令垃圾回收器能夠安全的執行可達性分析。

舉個例子,當運作本地方法的時候,如果不涉及任何java對象和方法,那麼堆棧就不會改變,也就代表着這段本地代碼可以作為一個安全點。

隻要不離開安全點,就可以一邊回收,一邊繼續運作代碼。

同時,java虛拟機僅僅需要在API的入口進行安全點檢測,測試是否有其他線程請求停留在安全點裡,便可以在必要的時候挂機目前線程。

除此之外,java線程還有其他幾種狀态:解釋執行位元組碼,執行編譯器的機器碼和線程阻塞。因為阻塞的線程由于處于JVM線程排程器的掌控之下,是以屬于安全點。

其他幾種狀态則是運作狀态,需要虛拟機保證可預見的時間内進入安全點。否則,垃圾回收線程可能處于等待所有線程進入安全點的狀态,進而變相提高了垃圾回收的暫停時間。

  • 對于解釋執行位元組碼來說:

位元組碼與位元組碼之間都可以作為安全點。Java虛拟機采取的做法是,當有安全點的請求時,執行一條位元組碼便進行一次安全點檢測。

  • 對于執行編譯器的機器碼來說:

因為這些代碼運作在底層的硬體上,不受JVM控制,是以在生成機器碼的時候,即時編譯器需要插入安全點檢查,來避免機器碼長時間沒有安全點檢測的情況。HotSpot虛拟機的做法便是在生成代碼的方法出口以及非計數循環的循環回邊(back-edge)處插入安全點檢測。

垃圾回收的三種方法

第一種是清除(sweep),也就是将死亡對象占據的記憶體标記為空閑記憶體,并且記錄在一個空閑清單(free list)中,當需要建立對象時,記憶體管理子產品便會從該空閑清單中尋找空閑記憶體,并且劃分給建立的對象。

JVM垃圾回收(上)

清除這種回收方式很簡單,但是問題有兩個,一個是會産生空間碎片,因為JVM中的記憶體對象是連續分布的,是以可能出現總空閑記憶體足夠,但是無法配置設定的極端情況。

另一個是配置設定效率低,如果記憶體是連續的,我們就用指針加法配置設定,但是對于空閑清單,JVM需要逐個通路清單中的項,來查找能夠放入建立對象的空閑記憶體。

第二種方法是壓縮(compact),也就是把存活的對象聚集到記憶體區域的起始位置,進而留下一段連續的記憶體空間,解決碎片化的問題,代價是壓縮算法本身的性能消耗。

JVM垃圾回收(上)

第三種是複制,将記憶體區域劃分成兩份,用from to指針去維護,用from标記還活着的對象,複制到to的區域,缺點就是堆的使用效率低。然後不斷循環,。

JVM垃圾回收(上)