天天看點

java垃圾回收了解與算法

垃圾回收機制概述

Java語言中一個顯著的特點就是引入了垃圾回收機制,使c++程式員最頭疼的記憶體管理的問題迎刃而解,它使得Java程式員在編寫程式的時候不再需要考慮記憶體管理。由于有個垃圾回收機制,Java中的對象不再有“作用域”的概念,隻有對象的引用才有“作用域”。垃圾回收可以有效的防止記憶體洩露,有效的使用空閑的記憶體。

ps:記憶體洩露是指該記憶體空間使用完畢之後未回收,在不涉及複雜資料結構的一般情況下,Java 的記憶體洩露表現為一個記憶體對象的生命周期超出了程式需要它的時間長度,我們有時也将其稱為“對象遊離”。

垃圾回收簡要過程

這裡必須點出一個很重要的誤區:不可達的對象并不會馬上就會被直接回收,而是至少要經過兩次标記的過程。

第一次被标記過的對象,會檢查該對象是否重寫了finalize()方法。如果重寫了該方法,則将其放入一個F-Query隊列中,否則,直接将對象加入“即将回收”集合。在第二次标記之前,F-Query隊列中的所有對象會逐個執行finalize()方法,但是不保證該隊列中所有對象的finalize()方法都能被執行,這是因為JVM建立一個低優先級的線程去運作此隊列中的方法,很可能在沒有周遊完之前,就已經被剝奪了運作的權利。那麼運作finalize()方法的意義何在呢?這是對象避免自己被清理的最後手段:如果在執行finalize()方法的過程中,使得此對象重新與GC Roots引用鍊相連,則會在第二次标記過程中将此對象從F-Query隊列中清除,避免在這次回收中被清除,恢複成了一個“正常”的對象。但顯然這種好事不能無限的發生,對于曾經執行過一次finalize()的對象來說,之後如果再被标記,則不會再執行finalize()方法,隻能等待被清除的命運,之後,GC将對F-Queue中的對象進行第二次小規模的标記,将隊列中重新與GC Roots引用鍊恢複連接配接的對象清除出“即将回收”集合。所有此集合中的内容将被回收。

下面是一個手動回收的程式,一般情況下我們無需手動操作,

public class JVMDemo05 {
    public static void main(String[] args) {
        JVMDemo05 jvmDemo05 = new JVMDemo05();
        //jvmDemo05 = null;
        System.gc();
    }
    protected void finalize() throws Throwable {
       System.out.println("gc在回收對象...");
    }
}           

複制

垃圾回收機制算法

(1).引用計數算法:

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不再被使用的,垃圾收集器将回收該對象使用的記憶體。

引用計數算法實作簡單,效率很高,微軟的COM技術、ActionScript、Python等都使用了引用計數算法進行記憶體管理,但是引用計數算法對于對象之間互相循環引用問題難以解決,是以java并沒有使用引用計數算法。

優點:

引用計數收集器可以很快的執行,交織在程式運作中。對程式需要不被長時間打斷的實時環境比較有利。

缺點:

無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0.而且每次加減非常浪費記憶體。

标記清除算法

标記-清除(Mark-Sweep)算法顧名思義,主要就是兩個動作,一個是标記,另一個就是清除。

标記就是根據特定的算法(如:引用計數算法,可達性分析算法等)标出記憶體中哪些對象可以回收,哪些對象還要繼續用。

标記訓示回收,那就直接收掉;标記訓示對象還能用,那就原地不動留下。

java垃圾回收了解與算法

最基礎的垃圾收集算法,算法分為“标記”和“清除”兩個階段:首先标記出所有需要回收的對象,在标記完成之後統一回收掉所有被标記的對象。

标記-清除算法的缺點有兩個:首先,效率問題,标記和清除效率都不高。其次,标記清除之後會産生大量的不連續的記憶體碎片,空間碎片太多會導緻當程式需要為較大對象配置設定記憶體時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

複制算法

S0和s1将可用記憶體按容量分成大小相等的兩塊,每次隻使用其中一塊,當這塊記憶體使用完了,就将還存活的對象複制到另一塊記憶體上去,然後把使用過的記憶體空間一次清理掉。這樣使得每次都是對其中一塊記憶體進行回收,記憶體配置設定時不用考慮記憶體碎片等複雜情況,隻需要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。

複制算法的缺點顯而易見,可使用的記憶體降為原來一半。

複制算法用于在新生代垃圾回收

标記-整理算法

标記-整理算法在标記-清除算法基礎上做了改進,标記階段是相同的标記出所有需要回收的對象,在标記完成之後不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,在移動過程中清理掉可回收的對象,這個過程叫做整理。

标記-整理算法相比标記-清除算法的優點是記憶體被整理以後不會産生大量不連續記憶體碎片問題。

複制算法在對象存活率高的情況下就要執行較多的複制操作,效率将會變低,而在對象存活率高的情況下使用标記-整理算法效率會大大提高。

标記壓縮法在标記清除基礎之上做了優化,把存活的對象壓縮到記憶體一端,而後進行垃圾清理。(java中老年代使用的就是标記壓縮法)

分代收集算法

根據記憶體中對象的存活周期不同,将記憶體劃分為幾塊,java的虛拟機中一般把記憶體劃分為新生代和年老代,當新建立對象時一般在新生代中配置設定記憶體空間,當新生代垃圾收集器回收幾次之後仍然存活的對象會被移動到年老代記憶體中,當大對象在新生代中無法找到足夠的連續記憶體時也直接在年老代中建立。

對于新生代和老年代來說,新生代回收頻率很高,但是每次回收耗時很短,而老年代回收頻率較低,但是耗時會相對較長,是以應該盡量減少老年代的GC.

垃圾回收時的停頓現象

垃圾回收的任務是識别和回收垃圾對象進行記憶體清理,為了讓垃圾回收器可以更高效的執行,大部分情況下,會要求系統進如一個停頓的狀态。停頓的目的是為了終止所有的應用線程,隻有這樣的系統才不會有新垃圾的産生。同時停頓保證了系統狀态在某一個瞬間的一緻性,也有利于更好的标記垃圾對象。是以在垃圾回收時,都會産生應用程式的停頓。

什麼是Java垃圾回收器

Java垃圾回收器是Java虛拟機(JVM)的三個重要子產品(另外兩個是解釋器和多線程機制)之一,為應用程式提供記憶體的自動配置設定(Memory Allocation)、自動回收(Garbage Collect)功能,這兩個操作都發生在Java堆上(一段記憶體快)。某一個時點,一個對象如果有一個以上的引用(Rreference)指向它,那麼該對象就為活着的(Live),否則死亡(Dead),視為垃圾,可被垃圾回收器回收再利用。垃圾回收操作需要消耗CPU、線程、時間等資源,是以容易了解的是垃圾回收操作不是實時的發生(對象死亡馬上釋放),當記憶體消耗完或者是達到某一個名額(Threshold,使用記憶體占總記憶體的比列,比如0.75)時,觸發垃圾回收操作。有一個對象死亡的例外,java.lang.Thread類型的對象即使沒有引用,隻要線程還在運作,就不會被回收。

串行回收器(Serial Collector)

單線程執行回收操作,回收期間暫停所有應用線程的執行,client模式下的預設回收器,通過-XX:+UseSerialGC指令行可選項強制指定。參數可以設定使用新生代串行和老年代串行回收器

年輕代的回收算法(Minor Collection)

把Eden區的存活對象移到To區,To區裝不下直接移到年老代,把From區的移到To區,To區裝不下直接移到年老代,From區裡面年齡很大的更新到年老代。 回收結束之後,Eden和From區都為空,此時把From和To的功能互換,From變To,To變From,每一輪回收之前To都是空的。設計的選型為複制。

年老代的回收算法(Full Collection)

年老代的回收分為三個步驟,标記(Mark)、清除(Sweep)、合并(Compact)。标記階段把所有存活的對象标記出來,清除階段釋放所有死亡的對象,合并階段 把所有活着的對象合并到年老代的前部分,把空閑的片段都留到後面。設計的選型為合并,減少記憶體的碎片。

并行回收器(ParNew回收器)

并行回收器在串行回收器基礎上做了改進,他可以使用多個線程同時進行垃

圾回收,對于計算能力強的計算機而言,可以有效的縮短垃圾回收所需的尖

際時間。

ParNew回收器是一個工作在新生代的垃圾收集器,他隻是簡單的将串行回收

器多線程快他的回收政策和算法和串行回收器一樣。

使用XX:+UseParNewGC 新生代ParNew回收器,老年代則使用市行回收器

ParNew回收器工作時的線程數量可以使用XX:ParaleiGCThreads參數指

定,一般最好和計算機的CPU相當,避免過多的栽程影響性能。

本文為從大資料到人工智能部落客「jellyfin」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://lrting.top/backend/9289/