1. 程式人生 > >深入理解JAVA虛擬機之JVM性能篇---垃圾回收

深入理解JAVA虛擬機之JVM性能篇---垃圾回收

小數據 alt tro 調優 permsize 多次 快速 com src

一、基本垃圾回收算法


1. 按基本回收策略分

  1) 引用計數(Reference Counting)

    對象增加一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的對象。此算法最致命的是無法處理循環引用的問題。

  2)標記-清除(Mark-Sweep)

  技術分享

   執行分兩階段。第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。

   缺點是此算法需要暫停整個應用,同時會產生內存碎片。

  3)復制(Copying)

  技術分享

  把內存空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象復制到另外一個區域中。

  此算法每次只處理正在使用中的對象,因此復制成本比較小,同時復制過去以後還能進行相應的內存整理,不會出現“碎片”問題。

  但缺點也是需要兩倍內存空間。

  4)標記-整理(Mark-Compact)

   技術分享

   此算法結合了“標記-清除”和“復制”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。

   此算法避免了“標記-清除”的碎片問題,同時也避免了“復制”算法的空間問題。

2. 按分區對待的方式

  1)增量收集

    實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。

  2)分代收集

    基於對對象生命周期分析後得出的垃圾回收算法。把對象分為年青代、年老代、持久代,對不同生命周期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。

3. 按系統線程分

  1)串行收集

   使用單線程處理所垃圾回收工作,因為無需多線程交互,實現容易,而且效率比較高。但是,其局限性也比較明顯,即無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小數據量(100M左右)情況下的多處理器機器上。

  2)並行收集

   使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。

  3)並發收集

   相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運行環境,而只有垃圾回收程序在運行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會更長,因為堆越大而越長。

二、如何區分垃圾

  除“引用計數”法外,後續實現的垃圾判斷算法中,都是從程序運行的根節點出發,遍歷整個對象引用,查找存活的對象。那麽在這種方式的實現中,垃圾回收從哪兒開始的呢?

  結合堆和棧的區別,其中棧是真正進行程序執行地方,所以要獲取哪些對象正在被使用,則需要從Java棧開始。同時,一個棧是與一個線程對應的,因此,如果有多個線程的話,則必須對這些線程對應的所有的棧進行檢查。

   同時,除了棧外,還有系統運行時的寄存器等,也是存儲程序運行數據的。這樣,以棧或寄存器中的引用為起點,我們可以找到堆中的對象,又從這些對象找到對堆中其他對象的引用,這種引用逐步擴展,最終以null引用或者基本類型結束,這樣就形成了一顆以Java棧中引用所對應的對象為根節點的一顆對象樹,如果棧中有多個引用,則最終會形成多顆對象樹。在這些對象上的對象,都是當前系統運行所需要的對象,不能被垃圾回收。而其他剩余對象,則可以視為無法被引用到的對象,可以被當做垃圾進行回收。

  因此,垃圾回收的起點是一些根對象(java棧, 靜態變量, 寄存器...)。而最簡單的Java棧就是Java程序執行的main函數。這種回收方式,也是上面提到的“標記-清除”的回收方式。

三、分代垃圾回收策略

  1. 為什麽要分代?

   不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。

  在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接等,這類對象跟業務直接掛鉤,因此生命周期比較長。但也有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,如:String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。
  所以,如果在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活對象,但實際上,對於生命周期長的對象而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收采用分治的思想,進行代的劃 分,把不同生命周期的對象放在不同代上,不同代上采用最適合它的垃圾回收方式進行回收。

  2. 如何分代?

   1)年輕代

    所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。年輕代分個區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被復制到Survivor區(兩個中的一個),當這個 Survivor區滿時,此區的存活對象將被復制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區復制 過來的並且此時還存活的對象,將被復制“年老區(Tenured)”。需要註意,Survivor的兩個區是對稱的,沒先後關系,所以同一個區中可能同時存在從Eden復制過來 對象,和從前一個Survivor復制過來的對象,而復制到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空 的。同時,根據程序需要,Survivor區是可以配置為多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。

   2)年老代

    在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。

   3)持久代

     用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=<N>進行設置。

   4) 示意圖如下:
   技術分享

    5)什麽情況下觸發回收?

    由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GC和Full GC。

    Scavenge GC

    一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。

    Full GC
    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個對進行回收,所以比Scavenge GC要慢,因此應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

  • · 年老代(Tenured)被寫滿
  • · 持久代(Perm)被寫滿
  • · System.gc()被顯示調用
  • · 上一次GC之後Heap的各域分配策略動態變化

深入理解JAVA虛擬機之JVM性能篇---垃圾回收