1. 程式人生 > >垃圾收集器與內存分配策略(一)

垃圾收集器與內存分配策略(一)

log 只需要 fullgc 拋出異常 run protect 一點 string 搜索

  在前面的Java自動內存管理機制(上)和Java自動內存管理機制(下)中介紹了關於JVM的一些基礎知識,包括運行時數據區域劃分和一些簡單的參數配置,而其中也談到了GC,但是沒有深入了解,所以這裏開始簡單的了解一下GC知識。本篇中主要介紹垃圾收集器回收對象的時候怎樣判斷對象是否已死和一些垃圾收集算法的概念。

一、GC概述

  在Java內存運行時數據區域中,程序計數器、虛擬機棧、本地方法棧是線程私有的,隨著線程產生而存在、線程執行結束回收,棧中的棧幀隨著方法的執行和退出進行相應的入棧和出棧的操作,每一個棧幀被分配多少空間在概念上基本是在類結構確定下來的時候就已經知道的。因此這幾個區域的回收可以說是具備一定的確定性(方法結束或者線程結束的時候,相應的內存就會被回收掉了)。所以同前面介紹的一樣,GC的主要區域還是在Java堆上面(某個接口的實現類所需要的內存可能是不一樣的,某個方法中的某些分支循環所需要的內存也不一樣),這些都是動態變化的,需要在程序開始運行之後才能知道會創建的對象,而這些程序創建之後的動態變化所導致的也就是GC需要動態執行,而Java堆就是GC主關註的區域。

二、GC判斷對象是否需要被回收的一些算法

 1、引用計數算法

  a)引用計數法的簡單概念:給對象添加以一個計數器,當一個地方引用到這個對象的時候,這個計數器的值就加1;當引用失效的時候,計數器的值就減1,計數器值為0的對象就是不可以被使用的。

  b)關於循環引用的問題,我們可以參考下面的這個例子,其中t1和t2兩個對象相互引用,然後在結束的時候將兩個對象置為null,引用計數算法不能回收這兩個對象,然後我們通過打印GC日誌來查看GC的詳細信息

package cn.jvm.test;

public class Test05 {
    public Object instance = null
; public byte[] testArr = new byte[2 * 1024 * 1024]; public static void main(String[] args) { Test05 t1 = new Test05(); Test05 t2 = new Test05(); t1.instance = t2; t2.instance = t1; t1 = null; t2 = null; System.gc(); } }

  c)下面是PrintGCDetails的結果,從結果中我們可以看出,GC的時候並沒有因為對象之間的循環引用就沒有回收他們,也說明虛擬機中不是通過引用計數法來進行判斷對象的存活以及是否需要GC的的

  技術分享圖片

[GC (System.gc()) [
PSYoungGen:  //GC類型為Parallel Scavenge,為eden+from區
6358K->744K(18432K)] //分別代表年輕帶GC前占用內存和GC後占用內存,括號中是年輕代棧中的總內存
6358K->752K(60928K),  //代表的是JVM堆內存GC前占用和GC後占用,括號中的是JVM堆所占內存大小
0.0314987 secs] [Times: user=0.00 sys=0.00, real=0.04 secs] 
[Full GC  //代表GC類型:如果直接是GC代表的是MinorGC,這裏面的是FullGC
(System.gc()) [
PSYoungGen: 744K->0K(18432K)] [ //年輕代
ParOldGen: 8K->658K(42496K)]  //老年代
752K->658K(60928K), 
[Metaspace: 3465K->3465K(1056768K)], 0.0093210 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 

Heap
 PSYoungGen      total 18432K, used 159K [0x00000000eb600000, 0x00000000eca80000, 0x0000000100000000)
  eden space 15872K, 1% used [0x00000000eb600000,0x00000000eb627c58,0x00000000ec580000)
  from space 2560K, 0% used [0x00000000ec580000,0x00000000ec580000,0x00000000ec800000)
  to   space 2560K, 0% used [0x00000000ec800000,0x00000000ec800000,0x00000000eca80000)
 ParOldGen       total 42496K, used 658K [0x00000000c2200000, 0x00000000c4b80000, 0x00000000eb600000)
  object space 42496K, 1% used [0x00000000c2200000,0x00000000c22a48c8,0x00000000c4b80000)
 Metaspace       used 3471K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

 2、可達性分析算法

  a)可達性分析算法是采用圖論中的思想,算法的基本思想就是通過被稱為GC Root的對象作為源點,然後向下搜索(搜索的所經過路徑被稱為引用鏈),當一個對象沒有任何可達GC Root的引用鏈的時候(簡單而言就是沒有可達路徑),說明對象是不可用的,會被判定為是可回收的

技術分享圖片

 3、引用

  a)可以看出上面的兩種算法對於判斷對象是否需要被回收都和引用有關系, 在1.2之前,JVM對於引用所指的是:如果reference類型的數據中存儲的數值代表的是另一塊內存的其實地址,稱這塊內存代表著一個引用

  b)在1.2之後,對於引用的概念進行了擴充,將引用分為:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference),下面具體說明一下這四種引用的概念和區別

  ①強引用(Strong Reference):類似於TestClass testClass = new TestClass()之中的引用,只要這種引用還存在,就不會被GC

  ②軟引用(Soft Reference):描述一些有用但是並非必需的對象,對於軟引用的對象而言,只要系統中沒有發生OOM異常之前,不會被GC;換而言之就是,在系統將要發生OOM之前,就會把這些引用的對象列進回收範圍內進行二次回收,如果此時有足夠的內存可以使得系統不發生OOM,就不會拋出異常,如果回收之後沒有足夠的內存,就會發生內存溢出異常

  ③弱引用(Weak Reference):同軟引用一樣,弱引用也是描述一些非必需的對象的,但是如其名其引用的強度也比全引用更弱一些,具體而言就是:被弱引用所引用的對象只能生存到下一次GC之前,當發生GC時候,無論當前內存是否足夠,弱引用所引用的對象都會被回收掉

  ④虛引用(Phantom Reference):虛引用是所有引用中最弱的一種引用,其存在就是為了將關聯虛引用的對象在被GC掉之後收到一個通知

 4、對象的存活

  a)在可達性分析算法中我們說到,如果一個對象沒有到達GC Root的引用鏈,那麽這個對象在發生GC時候就會被判定為可回收的對象,但是這種對象也並非是必須要被直接回收掉的,這種時候他們需要經歷兩個標記的過程,第一次標記過程:如果對象在進行可達性分析之後發現沒有與GC ROOT相連接的引用鏈,那麽會被進行一次標記和篩選(篩選的條件是是否有必要執行finalize()方法)

  ①當對象沒有覆蓋finalize()方法的時候,或者已經被虛擬機調用過,那麽虛擬機會將這兩種情況視為“沒有必要執行”

  ②在上面的介紹中,如果虛擬機認為某個對象是有必要執行finalize()方法的時候,這個對象會被放在一個F-Queue等待隊列之中,虛擬機會單獨開設一個線程Finalize(自動創建、低優先級)執行他們,但是不會等待線程運行結束。

  ③虛擬機不會等待FInalize線程運行結束的原因:如果某個對象finalize()方法執行的比較緩慢,或者在執行的過程中發生了死循環,那麽會使得整個F-Queue對象會處在一個等待的過程中,可能導致整個GC崩潰

  b)第二次標記的過程:

  在F-Queue隊列中,GC會對隊列中的每一個對象進行第二次標記,如果對象要想自己不被回收掉,那麽就需要將自己與其他任何一個對象建立關系即可,這樣就會在第二次標記的過程中移除隊列。

  c)下面是一個對象修改finalize方法,在首次將要被回收的時候,進行“逃脫”的一個例子,其中Test06.test06 = this這個是修改this指向自己的類變量(靜態變量)或者對象的成員變量,會使得在第二次標記的過程中被虛擬機移除F-Queue隊列而不被回收

 1 package cn.jvm.test;
 2 
 3 public class Test06 {
 4     public  static Test06 test06 = null;
 5 
 6     public void print() {
 7         System.out.println("存活");
 8     }
 9 
10     @Override
11     protected void finalize() throws Throwable {
12         super.finalize();
13         System.out.println("finalize方法被執行一次");
14         Test06.test06 = this;  //修改this指向自己的類變量(靜態變量)或者對象的成員變量,會使得在第二次標記的過程中被虛擬機移除F-Queue隊列而不被回收
15     }
16 
17     public static void main(String[] args) throws  Throwable{
18 
19         test06  = new Test06();
20 
21         //第一次逃脫回收
22         test06 = null;
23         System.gc();
24         Thread.sleep(500);
25         if(test06 != null) {
26             test06.print();
27         } else {
28             System.out.println("test對象被回收");
29         }
30 
31         //第二次想要逃脫GC
32         test06 = null;
33         System.gc();
34         Thread.sleep(500);
35         if(test06 != null) {
36             test06.print();
37         } else {
38             System.out.println("test對象被回收");
39         }
40     }
41 }

  d)這是執行結果

finalize方法被執行一次
存活 //第一次沒有被回收,因為第一次在程序中將this賦值給了類變量
test對象被回收 //每一個對象的finalize方法都只會被系統調用一次,第二次雖然執行相同的代碼,但是finalize方法不會被再次執行

  e)最後補上一點:finalize建議不要使用

三、垃圾收集算法

  1、標記清除算法

   a)標記-清除算法(Mark-Sweep),分為標記和清除兩個過程:首先標記出所有需要回收的對象,在標記完成後統一回收所有標記的對象。但是這種算法也有其不足之處:

   ①標記和清除的效率都不高;

   ②標記和清除之後會產生很多不連續的空間碎片,導致後續在分配給大對象內存的時候,無法找到足夠大的內存從而導致提前觸發GC。

   b)一個簡易的標記清除算法的執行過程像如下圖所示:

技術分享圖片

  2、復制算法

   a)復制算法:將可用內存按照容量劃分為相等大小的兩塊,每次只是使用其中的一塊,當這一塊的內存用完了,就將還存在的對象復制到另外一塊上面,然後再將剛剛使用的內存空間清理回收。

   b)復制算法的優點:每次對半個內存區域進行回收,所以內存分配的時候也不用考慮內存碎片燈復雜情況,實現簡單高效。缺點也很明顯,每次只能使用一半的內存空間,代價比較高,下面是一次復制算法的執行過程

技術分享圖片

   c)在HotSpot虛擬機中,在JVM自動內存管理機制(上)裏面的堆區中新生代分為eden、from、to三個區域,而新生代中的from和to區域就是使用的復制算法。因為新生代中的對象很大一部分都是“朝生夕死”的,所以不需要按照1:1的比例來劃分內存空間,而是劃分為eden和較小的兩塊survivor區域(from和to區域,大小相同),對於eden、from、to區域的比例大概就是8:1:1,所以每次新生代的可使用內存空間大概就是整個新生代空間的90%,浪費的內存比較少。下面使用一個簡單的例子通過使用-XX:+PrintGCDetails查看GC日誌

 1 package cn.jvm.test;
 2 
 3 public class Test07 {
 4 
 5     public static void main(String[] args) {
 6 
 7         //查看GC信息
 8         System.out.println("沒有分配時候");
 9         System.out.println("最大堆內存===" + Runtime.getRuntime().maxMemory());
10         System.out.println("空閑內存===" + Runtime.getRuntime().freeMemory());
11         System.out.println("總內存===" + Runtime.getRuntime().totalMemory());
12     }
13 }

   下面是運行的結果

技術分享圖片

  3、標記整理算法

   采用復制算法時,如果存活率比較高的時候就需要進行比較多的復制操作,會降低GC的效率;所以要想提高在老年代的GC效率,在JVM中采用適用於老年代特點的標記整理(Mark-Compact)算法進行GC操作。標記整理算法可以歸結為這樣的過程:首先標記出所有需要回收的對象,然後讓所有的對象都向一端移動,直接處理掉端邊界以外的內存。這種方式避免了類似於標記清除算法產生的內存碎片問題。下面是一個標記整理算法的簡單實例

技術分享圖片

  4、分代收集算法

    分代收集算法的思想比較簡答,就是根據對象不同的存活時期將內存劃分為不同的區域。在JVM中,將堆區分為新生代和老年代,在新生代中由於對象的創建比較頻繁,而且有大量的對象在GC的時候要被回收掉,采用復制算法只需要將少量存活的對象復制到另一塊區域就可以。而在老年代中,對象的創建不是那麽頻繁,對象的存活率也比較高,我們采用標記整理算法來進行回收

垃圾收集器與內存分配策略(一)