Java垃圾回收(一) 內存回收簡介

內存回收簡介

    在Java中,它的內存管理包括兩個方面:內存分配內存回收,這兩個方面的工作都是由JVM自動完成的,降低了Java程序員的學習難度,避免了像C/C++直接操作內存的危險。但這也使很多程序員不關心內存分配的問題,導致很多程序低效耗費內存。

    Java語言規範沒有明確的說明JVM使用哪種垃圾回收算法。一般常用的算法有下列幾種:
    在介紹之前先說明一個概念:根集。大多數垃圾回收算法使用了根集(root set)這個概念;所謂根集就量正在執行的Java程序可以訪問的引用變量的集合(包括局部變量、參數、類變量),程序可以使用引用變量訪問對象的屬性和調用對象的方法。

  1. 引用記數法(Reference Counting Collector)

    引用計數法是唯一沒有使用根集的垃圾回收算法,該算法使用引用計數器來區分存活對象和不再使用的對象。一般來說,堆中的每個對象對應一個引用計數器。當每一次創建一個對象並賦給一個變量時,引用計數器置爲1。當對象被賦給任意變量時,引用計數器每次加1當對象出了作用域後(該對象丟棄不再使用),引用計數器減1,一旦引用計數器爲0,對象就滿足了垃圾收集的條件。

    基於引用計數器的垃圾收集器運行較快,不會長時間中斷程序執行,適宜必須地實時運行的程序。但引用計數器增加了程序執行的開銷,因爲每次對象賦給新的變量,計數器加1,而每次現有對象出了作用域生,計數器減1。

  2. tracing算法(Tracing Collector)

    tracing算法是爲了解決引用計數法的問題而提出,它使用了根集的概念。基於tracing算法的垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每個可達對象設置一個或多個位。在掃描識別過程中,基於tracing算法的垃圾收集也稱爲標記和清除(mark-and-sweep)垃圾收集器.

  3. compacting算法(Compating Collector)

    爲了解決堆碎片問題,基於tracing的垃圾回收吸收了Compacting算法的思想,在清除的過程中,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閒內存區,收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在新的位置能識別原來的對象。在基於Compacting算法的收集器的實現中,一般增加句柄和句柄表。

  4. Copying算法(Cpoing Collector)

    該算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象 面和多個空閒面,程序從對象面爲對象分配空間,當對象滿了,基於coping算法的垃圾 收集就從根集中掃描活動對象,並將每個活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。

    一種典型的基於coping算法的垃圾回收是stop-and-copy算法,它將堆分成對象面和空閒區域面,在對象面與空閒區域面的切換過程中,程序暫停執行。

  5. generation算法(Genrational Collector)也就是分代回收

    stop-and-copy垃圾收集器的一個缺陷是收集器必須複製所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作爲對象的一代 (generation)。由於多數對象存在的時間比較短,隨着程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行後,上次運行存活下來的對象移到下一最高代的子堆中,由於老一代的子堆不會經常被回收,因而節省了時間。

  6. adaptive算法

    在特定的情況下,一些垃圾收集算法會優於其它算法。基於Adaptive算法的垃圾收集器就是監控當前堆的使用情況,並將選擇適當算法的垃圾收集器。

1.Java在內存中的狀態

    開始先看一個例子:

Person.java

package test;

import java.io.Serializable;

public class Person implements Serializable {

static final long serialVersionUID = 1L;

String name; // 姓名

Person friend;    //朋友

public Person() {}

public Person(String name) {
  super();
  this.name = name;
}
}

Test.java

package test;

public class Test{

public static void main(String[] args) {
  Person p1 = new Person("Kevin");
  Person p2 = new Person("Rain");
  Person p3 = new Person("Sunny");

  p1.friend = p2;
  p3 = p2;
  p2 = null;
}
}

    把上面Test.java中main方面裏面的對象引用畫成一個從main方法開始的對象引用圖的話就是這樣的(頂點是對象和引用,有向邊是引用關係):
Image

    當程序運行起來之後,把它在內存中的狀態看成是有向圖之後,可以分爲三種:

  1. 可達狀態:在一個對象創建後,有一個以上的引用變量和它關聯,則它處於可達狀態。
  2. 可恢復狀態:如果程序中某個對象不再有引用變量和其相關聯,則它將先進入可恢復狀態,此時從有向圖的起始頂點不能再導航到該對象,在這個狀態下,系統的垃圾回收機制準備回收該對象的所佔用的內存,在回收之前。系統會調用finalize()方法進行資源清理,如果資源整理後重新讓一個以上引用變量和該對象關聯,則該對象的狀態會再次變爲可達狀態,否則就會進入不可達狀態。
  3. 不可達狀態:當對象的所有關聯都被切斷,且系統調用finalize()方法進行資源清理之後依舊沒有使該對象變爲可達狀態,則這個對象將永久性失去引用並且變成不可達狀態,系統纔會真正的去回收該對象所佔用的資源。
    Image

2. Java對象的4種引用

  1. 強引用:創建一個對象並把這個對象直接賦值給一個變量引用,eg:Person person = new Person("sunny");此時不管系統資源有多麼緊張都絕對不會被回收。
  2. 軟引用:通過SoftReference類實現,eg:SoftReference<Person> p = new SoftReference<Person>(new Person(“Rain”));內存非常緊張的時候會被回收,其他時候不會被回收,因此在使用之前要判斷是否已經被回收了。例如:

    class AB {
    protected void finalize() {
        System.out.println("finalize.....");
    }
    }
    
    public class JavaTest {
        public static void main(String[] args) {
            for (int i=0 ; i < 10000; i ++) {
                new SoftReference<AB> (new AB());
            }
        }
    }
    結果爲:finalize.....
    finalize.....
    finalize.....
    結果不一定,看個人電腦了,也可能沒有輸出,需要創建更多的對象來逼着JVM回收軟引用。
    
  3. 弱引用 :通過WeakReference類實現,eg : WeakReference<Person> p = new WeakReference<Person>(new Person(“Rain”));不管內存是否足夠,系統垃圾回收時必定會回收。

    class AB {
        protected void finalize() {
            System.out.println("finalize.....");
        }
    }
    public class JavaTest {
        public static void main(String[] args) {
            WeakReference<AB> wr = new WeakReference<AB> (new AB());
            System.gc();
        }
    }
    輸出結果爲:finalize.....
    強制回收垃圾,若引用就會直接被回收
    
  4. 虛引用 :不能單獨使用,主要是用於追蹤對象被垃圾回收的狀態。通過PhantomReference類和引用隊列ReferenceQueue類聯合使用實現,例子如下。

import java.lang.ref.PhantomReference;
    import java.lang.ref.ReferenceQueue;


    public class Test{

    public static void main(String[] args) {
      //創建一個對象
      Person person = new Person("Sunny");  
      //創建一個引用隊列  
      ReferenceQueue<Person> rq = new ReferenceQueue<Person>();
      //創建一個虛引用,讓此虛引用引用到person對象
      PhantomReference<Person> pr = new PhantomReference<Person>(person, rq);
      //切斷person引用變量和對象的引用
      person = null;
      //試圖取出虛引用所引用的對象
      //發現程序並不能通過虛引用訪問被引用對象,所以此處輸出爲null
      System.out.println(pr.get());
      //強制垃圾回收
      System.gc();
      System.runFinalization();
      //因爲一旦虛引用中的對象被回收後,該虛引用就會進入引用隊列中
      //所以用隊列中最先進入隊列中引用與pr進行比較,輸出true
      System.out.println(rq.poll() == pr);
        }
    }
    輸出結果爲 : null true

3. 垃圾回收器分類

  1. 串行回收(只用一個cpu)和並行回收(多個cpu纔有用):串行回收是不管系統有多少個CPU,始終只用一個CPU來執行垃圾回收操作,而並行回收就是把整個回收工作拆分成對各部分,每個部分由一個CPU負責,從而讓多個CPU並行回收。並行回收的執行效率很高,但是複雜度增加,另外也有一些副作用,如內存碎片增加。
  2. 併發執行和應用程序停止 : 應用程序停止(stop-the-world)即在其垃圾回收方式在執行的時候同時會導致應用程序的暫停。併發執行的垃圾回收雖然不會導致應用程序的暫停,由於需要邊執行應用程序邊垃圾回收(可能在回收的時候修改對象,因此和應用程序的執行存在衝突問題),併發執行的系統開銷比Stop-the-world高,而且需要更多的堆內存。
  3. 壓縮和不壓縮和複製

    • 支持壓縮的垃圾回收器(標記-壓縮 =標記清楚+壓縮)會把所有的可達對象搬遷到一端,然後直接清理掉邊界以外的內存,減少了內存碎片。
    • 不支持壓縮的垃圾回收器(標記-清除)要遍歷兩次,第一次先從根開始訪問,標記所有可達狀態的對象,第二次遍歷整個內存區域,對爲標記可達狀態的對象進行回收處理。這種回收方式不壓縮,不需要額外的內存,但需要遍歷兩次,會產生碎片。
    • 複製式的垃圾回收器:將堆內存分成兩個相同的控件,從根開始訪問每個可達對象,將A的所有可達對象都複製到B空間,然後一次性回收所有A空間。遍歷空間的成本小,不會產生碎片,但需要巨大的複製成本和較多的內存。

4.內存管理技巧

  1. 儘量使用直接量。 eg:String s = “hello world”;
  2. 使用StringBuilder和StringBuffer進行字符串的連接等操作;
  3. 儘早釋放無用對象;
  4. 少使用靜態變量;
  5. 緩存常用的對象,可以用開源的開源緩存實現。eg:OSCache,Ehcache;
  6. 儘量不使用finalize()方法;
  7. 在必要的時候考慮多使用軟飲用SoftReference;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章