Java -- 深入淺出GC自動回收機制

2,GC簡單的瞭解

  GC :Garbage Collections 字面意思是垃圾回收器,釋放垃圾佔用的空間。讓創建的對象不需要像c、c++那樣delete、free掉 。對於c、c++的開發人員來說內存是開發人員分配的,也就是說還要對內存進行維護和釋放。對於Java程序員來說,一個對象的內存分配是在虛擬機的自動內存分配機制的幫助下,不再需要爲每一個new操作去寫配對的delete/free代碼,而且不容易出現內存泄露和內存溢出問題,但是,如果出現了內存泄露和內存溢出問題,而開發者又不瞭解虛擬機是怎麼分配內存的話,那麼定位錯誤和排除錯誤將是一件很困難的事情。(這裏留一個問題給大家思考一下,在什麼情況下會出現內存泄露和內存溢出問題)

  從上面的瞭解我們知道,內存的分配是在JVM虛擬機的自動內存分配機制下完成的,所以我們現在來簡單的瞭解一下JVM的內存管理結構,如圖:

  

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

JVM運行時數據區

①程序計數器(Program Counter Register)

 

  程序計數器是用於存儲每個線程下一步將執行的JVM指令,如該方法爲native的,則程序計數器中不存儲任何信息

 

②JVM棧(JVM Stack)

 

  JVM棧是線程私有的,每個線程創建的同時都會創建JVM棧,JVM棧中存放的爲當前線程中局部基本類型的變量(java中定義的八種基本類型:booleancharbyteshortintlongfloatdouble)、部分的返回結果以及Stack Frame,非基本類型的對象在JVM棧上僅存放一個指向堆上的地址

 

③堆(heap)

 

  它是JVM用來存儲對象實例以及數組值的區域,可以認爲Java中所有通過new創建的對象的內存都在此分配,Heap中的對象的內存需要等待GC進行回收。

 

  (1)堆是JVM中所有線程共享的,因此在其上進行對象內存的分配均需要進行加鎖,這也導致了new對象的開銷是比較大的

 

  (2)Sun Hotspot JVM爲了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的情況計算而得,在TLAB上分配對象時不需要加鎖,因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間分配

 

  (3)TLAB僅作用於新生代的Eden Space,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效。

 

④方法區(Method Area)

 

  (1)在Sun JDK中這塊區域對應的爲PermanetGeneration,又稱爲持久代。

 

  (2)方法區域存放了所加載的類的信息(名稱、修飾符等)、類中的靜態變量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息,當開發人員在程序中通過Class對象中的getName、isInterface等方法來獲取信息時,這些數據都來源於方法區域,同時方法區域也是全局共享的,在一定的條件下它也會被GC,當方法區域需要使用的內存超過其允許的大小時,會拋出OutOfMemory的錯誤信息。

 

⑤本地方法棧(Native Method Stacks)

 

  JVM採用本地方法棧來支持native方法的執行,此區域用於存儲每個native方法調用的狀態。

 

⑥運行時常量池(Runtime Constant Pool)

  存放的爲類中的固定的常量信息、方法和Field的引用信息等,其空間從方法區域中分配。JVM在加載類時會爲每個class分配一個獨立的常量池,但是運行時常量池中的字符串常量池是全局共享的。

  JVM將堆分成了二個大區新生代(Young)和老年代(Old),新生代又被進一步劃分爲Eden和Survivor區,而Survivor由FromSpace和ToSpace組成,也有些人喜歡用Survivor1和Survivor2來代替。這裏爲什麼要將Young劃分爲Eden、Survivor1、Survivor2這三塊,給出的解釋是  

  “Young中的98%的對象都是死朝生夕死,所以將內存分爲一塊較大的Eden和兩塊較小的Survivor1、Survivor2,JVM默認分配是8:1:1,每次調用Eden和其中的Survivor1(FromSpace),當發生回收的時候,將Eden和Survivor1(FromSpace)存活的對象複製到Survivor2(ToSpace),然後直接清理掉Eden和Survivor1的空間。”

  堆結構圖如下:

  

  

  新生代:新創建的對象都是用新生代分配內存,Eden空間不足時,觸發Minor GC,這時會把存活的對象轉移進Survivor區。
  老年代:老年代用於存放經過多次Minor GC之後依然存活的對象。

  新生代的GC(Minor GC):新生代通常存活時間較短基於Copying算法進行回收,所謂Copying算法就是掃描出存活的對象,並複製到一塊新的完全未使用的空間中,對應於新生代,就是在Eden和FromSpace或ToSpace之間copy。新生代採用空閒指針的方式來控制GC觸發,指針保持最後一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。當連續分配對象時,對象會逐漸從Eden到Survivor,最後到老年代。

  老年代的GC(Major GC/Full GC):老年代與新生代不同,老年代對象存活的時間比較長、比較穩定,因此採用標記(Mark)算法來進行回收,所謂標記就是掃描出存活的對象,然後再進行回收未被標記的對象,回收後對用空出的空間要麼進行合併、要麼標記出來便於下次進行分配,總之目的就是要減少內存碎片帶來的效率損耗。

  這裏我們要介紹幾種垃圾收集算法:

  ①Mark-Sweep(標記-清除)算法

  這是最基礎的垃圾回收算法,之所以說它是最基礎的是因爲它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。具體過程如下圖所示:

  

  從圖中可以很容易看出標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致後續過程中需要爲大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 

  ②.Copying(複製)算法

  爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:

  

  這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因爲能夠使用的內存縮減到原來的一半。 很顯然,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。我們的新生代GC算法採用的是這種算法

  ③Mark-Compact(標記-整理)算法

  爲了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。具體過程如下圖所示:

  

  在一般廠商JVM中老年代GC就是使用的這種算法,由於老年代的特點是每次回收都只回收少量對象。

  上面的是一些常見的垃圾收集算法,垃圾收集算法是內存回收的理論基礎,而垃圾收集器就是內存回收的具體實現。下面有幾種創建的垃圾收集器,用戶可以根據自己的需求組合出新年代和老年代使用的收集器。下面是常見的劃分辦法

  新生代GC :串行GC(SerialGC)、並行回收GC(ParallelScavenge)和並行GC(ParNew)

  串行GC:在整個掃描和複製過程採用單線程的方式來進行,適用於單CPU、新生代空間較小及對暫停時間要求不是非常高的應用上,是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定。

  並行回收GC:在整個掃描和複製過程採用多線程的方式來進行,適用於多CPU、對暫停時間要求較短的應用上,是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數。

     並行GC:與老年代的併發GC配合使用。

   老年代GC:串行GC(Serial MSC)、並行GC(Parallel MSC)和併發GC(CMS)。

   串行GC(Serial MSC):client模式下的默認GC方式,可通過-XX:+UseSerialGC強制指定。每次進行全部回收,進行Compact,非常耗費時間。

   並行GC(Parallel MSC):吞吐量大,但是GC的時候響應很慢:server模式下的默認GC方式,也可用-XX:+UseParallelGC=強制指定。可以在選項後加等號來制定並行的線程數。

   併發GC(CMS):響應比並行gc快很多,但是犧牲了一定的吞吐量。

   以上我們差不多吧GC的基本只是給瞭解完了。

3,思考“GC是在什麼時候,對什麼東西,做了什麼事情?”

  這是在知乎上看到大牛對GC回答的加單的思考,感覺這樣分析思路很清晰,就借鑑了過來

  • 什麼時候

  從字面上翻譯過來就是什麼時候觸發我們的GC機制

  ①在程序空閒的時候。這個回答無力吐槽

  ②程序不可預知的時候/手動調用system.gc()。關於手動調用不推薦

  ③Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程中創建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次之後仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。就是

  這時候如果你們講出新生代和老年代的話或許會更細的瞭解一下Minor GC、Full GC、OOM什麼時候觸發!

  創建對象是新生代的Eden空間調用Minor GC;當升到老年代的對象大於老年代剩餘空間Full GC;GC與非GC時間耗時超過了GCTimeRatio的限制引發OOM。

  • 什麼東西

  從字面的意思翻譯過來就是能被GC回收的對象都有哪些特徵

  ①超出作用域的對象/引用計數爲空的對象。

  引用計數算法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器都爲0的對象就是不可能再被使用的。

  ②從GC Root開始搜索,且搜索不到的對象

  跟搜索算法:以一系列名爲 GC Root的對象作爲起點,從這些節點開始往下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈的時候,則就證明此對象是不可用的。

  這裏會提出一個思考,什麼樣的對象能成爲GC Root : 虛擬機中的引用的對象、方法區中的類靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中jni的引用對象。

  ③從root搜索不到,而且經過第一次標記、清理後,仍然沒有復活的對象。

  • 做什麼

  不同年代、不同種類的收集器很多,不過總體的作用是刪除不使用的對象,騰出內存空間。補充一些諸如停止其他線程執行、運行finalize等的說明。

ok  現在來回答一下我們最上面的問題,上面時候容易發生內存泄露

  ①靜態集合類像HashMap、Vector等

  ②各種連接,數據庫連接,網絡連接,IO連接等沒有顯示調用close關閉,不被GC回收導致內存泄露。

  ③監聽器的使用,在釋放對象的同時沒有相應刪除監聽器的時候也可能導致內存泄露。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章