天天看點

java垃圾回收淺析

垃圾回收(Garbage Collection),乍一看,垃圾收集應該處理顧名思義 - 查找并扔掉垃圾。事實上,它恰恰相反。垃圾收集正在追蹤所有仍在使用的對象,并将其餘标記為垃圾,然後釋放垃圾占用的記憶體,當然了,java有垃圾自動回收機制,是以我們基本上不需要去關心記憶體配置設定和垃圾回收的問題,不過了解一下還是挺好的。

一、什麼是垃圾?

如果一個對象沒有任何引用與之關聯那麼它就會被認定為是垃圾。那如何确定一個對象有沒有被引用呢?我們有兩種方法:

1、引用計數法,給對象添加一個引用計數器,當它被引用時,計數器加一,當引用失效時計數器減一,當任何時候計數器為都0時,判斷該對象為垃圾。這種方法簡單易懂,但是缺點也很明顯,就是不能解決循環引用,例如:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.object = object2;
        object2.object = object1;

        object1 = null;
        object2 = null;
    }
}

class MyObject{
    public Object object = null;
}
           

上面的代碼中object1和object2互相引用,形成了一個環路,即使兩者都是null,但由于它們的引用數不為0 ,是以它們不會被回收。java并沒有采用這種方式,不過Python用的是,不知道它具體是怎麼解決循環引用的。java用的是下面這種。

2、根搜尋(GC Root Tracing),它通過一系列的“GC Roots”對象作為起點進行搜尋,搜尋所走過的路徑叫做引用鍊,若果一個對象沒有任何路徑到達GC Roots,那麼就說這個對象是不可達的,不夠即使是不可達的對象也不能馬上就判斷其為垃圾,至少要經過過兩次标記,且兩次都不可達才能将其判斷為垃圾。這其中還有一個問題就是那些對象可以作為“GC Roots”,如下:

  • 虛拟機棧(裡面的本地變量表)中的引用的對象
  • 方法區中的類的靜态屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(Native方法)的引用的對象

    現在垃圾找到了,那麼如何回收呢?主要有以下幾種方法

    1、标記删除(Mark-Sweep)

    步驟很簡單,首先标記出所有需要被回收的對象,然後删除它們,就是回收這些對象所占的記憶體。雖然簡單易行,效率也高,但是它的缺點也很明顯:容易産生記憶體碎片。如下圖:

    java垃圾回收淺析

    2、标記複制

    标記複制的原理也很簡單,首先它将記憶體分為大小相同的兩部分,每次使用其中的一部分,當這一部分記憶體用完時,就将其中還存活着的對象複制到另一塊記憶體上,然後将這部分記憶體清空。這樣一來就解決了标記删除所産生的記憶體碎片問題,但是它的缺點也很明顯:能使用的記憶體隻有一半。如下圖:

    java垃圾回收淺析

    3、标記整理(Mark-Compact)

    标記整理能夠解決前面兩種算法的缺點,它在标記清理出垃圾對象後,會把所有活着的對象都向記憶體一段移動,并更新引用其對象的指針,這樣一來就不會産生記憶體碎片了,但是由于每次都要對或者的對象進行移動,是以它的效率比“标記清理”要低。

    java垃圾回收淺析

    4、分代收集(Generational Collection)

    java所采用的一種很經典的垃圾回收算法,最新的JDK9完全已經完全删除了它,而采用G1算法。為什麼會提出分代收集呢?因為研究人員觀察到,應用程式内的大部分對象分為兩類:一類是很快不被使用的;另一類是會存活一段時間的。如下圖:

    java垃圾回收淺析
    是以分代收集的主要思想就是:根據對象生命周期的長短把記憶體分為若幹個不同的區域。如下圖:
    java垃圾回收淺析
    其中新生代存放剛剛建立和那些存活時間比較短的對象,老年代存放存活時間較長的對象。新生代又分為Eden、survivor1、survivor2三個區,兩個servivor互為From和To邏輯區域,當其中一個為From時另一個就為To,這些區的大小也是不一樣的,Eden更大,因為IBM的專門研究表明新生代中的對象98%都是很快就會死的。當進行回收時将Eden和From survivor中還存活着的對象拷貝到另一塊To survivor中,并清空Eden和From survivor,清空後把From變成To,To變成From。如果在拷貝的過程中To survivor滿了,會将其存在老年代中。如下圖:
    java垃圾回收淺析

    在JDK8之前,還有一塊區域叫做permgen(永久代)的特殊空間,它是用來存放類的中繼資料的,但是它實際上用于給Java開發人員帶來很多麻煩,因為很難預測所有需要的空間。由于預測中繼資料是一項非常麻煩的工作,是以在Java 8中移除了永久代以支援Metaspace。它位于本機記憶體中,不會幹擾正常堆對象。預設情況下,Metaspace大小僅受Java程序可用的本機記憶體量的限制。當然你也可以自行設定。

    幾個不同的GC

    Minor GC:就是對年輕代的清理,發生頻率高,該過程會讓應用程式的線程暫停,如下圖:

    java垃圾回收淺析

    Major GC:就是對老年代的清理,頻率低,不會讓應用程式的線程暫停

    Full GC:就是對整個堆的清理,包括年輕代和年老代,如下圖:

    java垃圾回收淺析

    不過不必在意這些稱呼,你應該關注的是GC是否停止了所有的應用程式線程,或者是否能夠與應用程式線程同時進行。

    java垃圾收集器的演變曆程

    1、串行收集(Serial)

    在JDK的早期版本中,JVM僅能使用serial收集器,它的特點就是:它隻會使用一個CPU或者一個線程去進行垃圾回收,而且當進行垃圾回收時,其他線程必需停止,直到它完成垃圾回收後才能運作。

    2、并行收集(parallel)

    顧名思義,并行收集就是能采用多個線程進行垃圾回收,這樣可以充分利用CPU多核的特性,提高垃圾收集的效率,降低垃圾收集的時間。

    3、CMS收集

    CMS在Minor GC(就是對新生代進行垃圾回收)時會暫停所有應用線程,并采用多線程的方式進行垃圾回收。在Full GC時(就是對老年代進行垃圾回收)不會暫停其他應用線程,而是使用若幹個背景線程定期的對老年代空間進行掃描,及時回收其中不再使用的對象。

    4、G1收集

    garbage first垃圾優先收集,意思就是首先收集包含垃圾數最多的區域。G1的關鍵設計目标之一是通過垃圾收集來預測和配置垃圾回收時應用程式線程暫停持續的時間。事實上,Garbage-First是一個軟實時垃圾收集器,這意味着您可以為其設定特定的性能目标。您可以在任何給定的y毫秒時間範圍内請求停止stop-the-world(垃圾回收時應用程式線程暫停持續的時間)不超過x毫秒。為了實作這一點,G1采用了新的堆劃分方式。首先,堆不必分成連續的年輕一代和老一代。相反,堆被拆分成一個個可容納對象的小堆區域,通常有2048個。每個區域可能是Eden,也可能是survivor或者old。所有Eden和survivor區都叫做年輕代,所有old取都是老年代,如下圖:

    java垃圾回收淺析

    這樣做的好處就是避免一次收集整個堆,而是隻收集那些有垃圾最多的區域(G1會在并發階段估計每個區域包含的資料量,進而确定哪些區域垃圾多,需要收集)。

    其實在G1中,還有一種特殊的區域,是用來存放巨型對象的(占用的空間超過了分區容量50%以上)。這些巨型對象預設直接會被配置設定在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。

    G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。下面我們将分别介紹一下這2種模式。

    1、young GC

    當Eden區被填滿時,應用程式線程停止,将Eden區中還活着的對象複制到survivor區,如果Survivor空間不夠,Eden空間的部分資料會直接晉升到年老代空間。Survivor區的資料移動到新的Survivor區中,也有部分資料晉升到老年代空間中。最終Eden空間的資料為空,GC停止工作,應用線程繼續執行。

    2、mix GC

    在進行正常的新生代垃圾收集,同時也回收部分背景掃描線程标記的老年代分區。