內存回收簡介
在Java中,它的內存管理包括兩個方面:內存分配和內存回收,這兩個方面的工作都是由JVM自動完成的,降低了Java程序員的學習難度,避免了像C/C++直接操作內存的危險。但這也使很多程序員不關心內存分配的問題,導致很多程序低效耗費內存。
Java語言規範沒有明確的說明JVM使用哪種垃圾回收算法。一般常用的算法有下列幾種:
在介紹之前先說明一個概念:根集。大多數垃圾回收算法使用了根集(root set)這個概念;所謂根集就量正在執行的Java程序可以訪問的引用變量的集合(包括局部變量、參數、類變量),程序可以使用引用變量訪問對象的屬性和調用對象的方法。
引用記數法(Reference Counting Collector)
引用計數法是唯一沒有使用根集的垃圾回收算法,該算法使用引用計數器來區分存活對象和不再使用的對象。一般來說,堆中的每個對象對應一個引用計數器。當每一次創建一個對象並賦給一個變量時,引用計數器置爲1。當對象被賦給任意變量時,引用計數器每次加1當對象出了作用域後(該對象丟棄不再使用),引用計數器減1,一旦引用計數器爲0,對象就滿足了垃圾收集的條件。
基於引用計數器的垃圾收集器運行較快,不會長時間中斷程序執行,適宜必須地實時運行的程序。但引用計數器增加了程序執行的開銷,因爲每次對象賦給新的變量,計數器加1,而每次現有對象出了作用域生,計數器減1。
tracing算法(Tracing Collector)
tracing算法是爲了解決引用計數法的問題而提出,它使用了根集的概念。基於tracing算法的垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每個可達對象設置一個或多個位。在掃描識別過程中,基於tracing算法的垃圾收集也稱爲標記和清除(mark-and-sweep)垃圾收集器.
compacting算法(Compating Collector)
爲了解決堆碎片問題,基於tracing的垃圾回收吸收了Compacting算法的思想,在清除的過程中,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閒內存區,收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在新的位置能識別原來的對象。在基於Compacting算法的收集器的實現中,一般增加句柄和句柄表。
Copying算法(Cpoing Collector)
該算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象 面和多個空閒面,程序從對象面爲對象分配空間,當對象滿了,基於coping算法的垃圾 收集就從根集中掃描活動對象,並將每個活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。
一種典型的基於coping算法的垃圾回收是stop-and-copy算法,它將堆分成對象面和空閒區域面,在對象面與空閒區域面的切換過程中,程序暫停執行。
generation算法(Genrational Collector)也就是分代回收
stop-and-copy垃圾收集器的一個缺陷是收集器必須複製所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作爲對象的一代 (generation)。由於多數對象存在的時間比較短,隨着程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行後,上次運行存活下來的對象移到下一最高代的子堆中,由於老一代的子堆不會經常被回收,因而節省了時間。
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方法開始的對象引用圖的話就是這樣的(頂點是對象和引用,有向邊是引用關係):
當程序運行起來之後,把它在內存中的狀態看成是有向圖之後,可以分爲三種:
- 可達狀態:在一個對象創建後,有一個以上的引用變量和它關聯,則它處於可達狀態。
- 可恢復狀態:如果程序中某個對象不再有引用變量和其相關聯,則它將先進入可恢復狀態,此時從有向圖的起始頂點不能再導航到該對象,在這個狀態下,系統的垃圾回收機制準備回收該對象的所佔用的內存,在回收之前。系統會調用finalize()方法進行資源清理,如果資源整理後重新讓一個以上引用變量和該對象關聯,則該對象的狀態會再次變爲可達狀態,否則就會進入不可達狀態。
- 不可達狀態:當對象的所有關聯都被切斷,且系統調用finalize()方法進行資源清理之後依舊沒有使該對象變爲可達狀態,則這個對象將永久性失去引用並且變成不可達狀態,系統纔會真正的去回收該對象所佔用的資源。
2. Java對象的4種引用
- 強引用:創建一個對象並把這個對象直接賦值給一個變量引用,eg:
Person person = new Person("sunny");
此時不管系統資源有多麼緊張都絕對不會被回收。 軟引用:通過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回收軟引用。
弱引用 :通過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..... 強制回收垃圾,若引用就會直接被回收
虛引用 :不能單獨使用,主要是用於追蹤對象被垃圾回收的狀態。通過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. 垃圾回收器分類
- 串行回收(只用一個cpu)和並行回收(多個cpu纔有用):串行回收是不管系統有多少個CPU,始終只用一個CPU來執行垃圾回收操作,而並行回收就是把整個回收工作拆分成對各部分,每個部分由一個CPU負責,從而讓多個CPU並行回收。並行回收的執行效率很高,但是複雜度增加,另外也有一些副作用,如內存碎片增加。
- 併發執行和應用程序停止 : 應用程序停止(stop-the-world)即在其垃圾回收方式在執行的時候同時會導致應用程序的暫停。併發執行的垃圾回收雖然不會導致應用程序的暫停,由於需要邊執行應用程序邊垃圾回收(可能在回收的時候修改對象,因此和應用程序的執行存在衝突問題),併發執行的系統開銷比Stop-the-world高,而且需要更多的堆內存。
壓縮和不壓縮和複製
- 支持壓縮的垃圾回收器(標記-壓縮 =標記清楚+壓縮)會把所有的可達對象搬遷到一端,然後直接清理掉邊界以外的內存,減少了內存碎片。
- 不支持壓縮的垃圾回收器(標記-清除)要遍歷兩次,第一次先從根開始訪問,標記所有可達狀態的對象,第二次遍歷整個內存區域,對爲標記可達狀態的對象進行回收處理。這種回收方式不壓縮,不需要額外的內存,但需要遍歷兩次,會產生碎片。
- 複製式的垃圾回收器:將堆內存分成兩個相同的控件,從根開始訪問每個可達對象,將A的所有可達對象都複製到B空間,然後一次性回收所有A空間。遍歷空間的成本小,不會產生碎片,但需要巨大的複製成本和較多的內存。
4.內存管理技巧
- 儘量使用直接量。 eg:String s = “hello world”;
- 使用StringBuilder和StringBuffer進行字符串的連接等操作;
- 儘早釋放無用對象;
- 少使用靜態變量;
- 緩存常用的對象,可以用開源的開源緩存實現。eg:OSCache,Ehcache;
- 儘量不使用finalize()方法;
- 在必要的時候考慮多使用軟飲用SoftReference;