- GC是什么? 為什么要有GC?
GC是垃圾收集的意思(Garbage Collection),內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的。
- 有向圖垃圾回收機制
.NET的垃圾回收采用引用計數,java的垃圾回收機制采取的是有向圖的方式來實現,具體的說,java程序中的每個線程對象就可以看作是一個有向圖的起點,有向邊從棧中的引用者指向堆中的引用對象。在這個有向圖中,如果一個對象和根節點之間是可達的,那么這個對象就是有效的,反之,這個對象就是可以被回收的。采取這樣一種機制的優點是可以有效的避免循環引用。
當程序員創建對象時,GC就開始監控這個對象的地址、大小以及使用情況。通過有向圖機制確定哪些對象是"可達的",哪些對象是"不可達的".當GC確定一些對象為"不可達"時,GC就有責任回收這些內存空間。
- GC在JVM中通常是由一個或一組線程來實現的,它本身也和用戶程序一樣占用heap空間,運行時也占用CPU.當GC進程運行時,應用程序停止運行。為了防止finalize函數拋出的異常影響到垃圾回收線程的運作,垃圾回收線程會在調用每一個finalize函數時進行try catch,如果捕獲到異常,就直接丟棄,然后接着處理下一個失效對象的finalize函數。所以finalize函數內一般需要自己處理拋出的異常,防止發生未處理異常情況。
- 當GC運行時間較長時,用戶能夠感到 Java程序的停頓,另外一方面,如果GC運行時間太短,則可能對象回收率太低,這意味着還有很多應該回收的對象沒有被回收,仍然占用大量內存。因此一種折中的方案就是每次GC處理一定比例的對象,分成多次進行,這就叫增量式GC。
- GC的分代
1) 在Young Generation中,有一個叫Eden Space的空間,主要是用來存放新生的對象,還有兩個Survivor Spaces(from、to),它們的大小總是一樣,它們用來存放每次垃圾回收后存活下來的對象。
2) 在Old Generation中,主要存放應用程序中生命周期長的內存對象。
3) 在Young Generation塊中,垃圾回收一般用Copying的算法,速度快。每次GC的時候,存活下來的對象首先由Eden拷貝到某個SurvivorSpace,當Survivor Space空間滿了后,剩下的live對象就被直接拷貝到OldGeneration中去。因此,每次GC后,Eden內存塊會被清空。
4) 在Old Generation塊中,垃圾回收一般用mark-compact的算法,速度慢些,但減少內存要求。
5) 垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收Young中的垃圾,內存溢出通常發生於OLD段或Perm段垃圾回收后,仍然無內存空間容納新的Java對象的情況。
- GC只回收堆區的內存,即處理java new出來的對象,但無法關閉其他資源,也無法處理java調用C或其他語言分配出的內存。
- 如果調用對象的finalize函數,對象處於不可達狀態,並且GC准備回收該對象的內存,即finalize函數的調用發生在回收內存之前。
- JVM不保證finalize函數一定會被調用。System.runFinalizersOnExit方法不安全,已經廢棄,所有並不能保證程序退出時一定調用finalize函數。另外,規范還保證finalize函數最多運行一次。
- System.gc並不保證GC執行,只是向JVM發送建議,並不是命令。
- 對象不可達,但是調用finalize之后又變得可達的情況存在,在finalize函數中通過this指針讓其他句柄執行本身即可,但是再下次回收時不會再調用finalize,因為只能調用一次。
protected void finalize() { main.ref=this; // 恢復本對象,讓本對象可達 }
- 垃圾回收器不能對用Java以外的代碼編寫的Class(比如JNI,C++的new方法分配的內存)進行正確的回收,這時就需要覆蓋默認finalize的方法來實現對這部分內存的正確釋放和回收(比如C++需要delete)。
- finalize不能等同於C++對象的析構函數,C++析構函數在在對象生命周期結束時會確定執行,而finalize函數的調用具有很大的不確定性。
調用時間不確定——有資源浪費的風險
如果把某些稀缺資源放到finalize()中釋放,可能會導致該稀缺資源等上很久很久以后才被釋放。造成資源的浪費!另外,某些類對象所攜帶的資源(比如某些JDBC的類)可能本身就很耗費內存,這些資源的延遲釋放會造成很大的性能問題。
可能不被調用——有資源泄漏的風險
在某些情況下,finalize()壓根兒不被調用。比如在JVM退出的當口,內存中那些對象的finalize函數可能就不會被調用了。
因此一些清理工作如文件的關閉,連接的關閉等不要放到finalize函數中,要在程序中單獨進行管理,一般finalize只做C/C++內存的回收。
- 即使有GC機制,Java還是存在內存泄露的問題
1.靜態集合類形成的對象引用
Static Vector v = new Vector(); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; }
2.當集合里面的對象屬性被修改后,再調用remove()方法時不起作用
3.各種連接,數據庫連接,網絡連接,IO連接等,顯示調用close關閉后才能被GC回收
- Java語言中的對象引用分為以下幾種:強引用,軟引用,弱引用,虛引用
強引用就是平時最常用的引用,當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。
只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。
虛引用,這種引用不常用,主要用途是結果引用關聯對象,實現對對象引用關系追蹤。虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
幾種引用分別位於java.lang.ref.SoftReference; java.lang.ref.WeakReference; 和 java.lang.ref.PhantomReference;
- 關於句柄和對象的一點記錄
String s = new String("abc") ; 在編譯階段在文字池中創建“abc”對象,運行new時,將pool中的對象復制一份放到heap中,並且把heap中的這個對象的引用交給s持有,因此這條語句創建了2個String對象。
String s1 = new String("abc") ; String s2 = new String("abc") ; 共創建三個對象,pool中一個,heap中2個。
句柄可以不依賴於對象而存在,例如String s,創建了句柄s,在棧中保存,但並沒有任何對象相關聯。句柄可以考慮為遙控器,對象為電視,擁有了遙控器,就可以操控電視,但是沒有電視,一樣可以有遙控器。
由於字符串對象的大量使用(它是一個對象,一般而言對象總是在heap分配內存),Java中為了節省內存空間和運行時間,在編譯階段就把所有的字符串文字放到一個文字池(pool of literal strings)中,而運行時文字池成為常量池的一部分。文字池的好處,就是該池中所有相同的字符串常量被合並,只占用一個空間。所以上述的 "abc" 只占一份空間。
String s1 = "abc" ;
String s2 = "abc" ;
這里 s1 == s2 成立
String s1 = new String("abc") ;
String s2 = new String("abc") ;
這里 s1 == s2 不成立,s1.equals(s2)成立
- 垃圾回收算法
Java語言規范沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾收集算法一般要做2件基本的事情:(1)發現無用信息對象;(2)回收被無用對象占用的內存空間,使該空間可被程序再次使用。
大多數垃圾回收算法使用了根集(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算法(Compacting Collector)
為了解決堆碎片問題,基於tracing的垃圾回收吸收了Compacting算法的思想,在清除的過程中,算法將所有的對象移到堆的一端,堆的另一端就變成了一個相鄰的空閑內存區,收集器會對它移動的所有對象的所有引用進行更新,使得這些引用在新的位置能識別原來 的對象。在基於Compacting算法的收集器的實現中,一般增加句柄和句柄表。
4、copying算法(Coping Collector)
該算法的提出是為了克服句柄的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個對象 面和多個空閑面, 程序從對象面為對象分配空間,當對象滿了,基於coping算法的垃圾 收集就從根集中掃描活動對象,並將每個 活動對象復制到空閑面(使得活動對象所占的內存之間沒有空閑洞),這樣空閑面變成了對象面,原來的對象面變成了空閑面,程序會在新的對象面中分配內存。
一種典型的基於coping算法的垃圾回收是stop-and-copy算法,它將堆分成對象面和空閑區域面,在對象面與空閑區域面的切換過程中,程序暫停執行。
5、generation算法(Generational Collector)
stop-and-copy垃圾收集器的一個缺陷是收集器必須復制所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作為對象的一代(generation)。由於多數對象存在的時間比較短,隨着程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行后,上次運行存活下來的對象移到下一最高代的子堆中,由於老一代的子堆不會經常被回收,因而節省了時間。
6、adaptive算法(Adaptive Collector)
在特定的情況下,一些垃圾收集算法會優於其它算法。基於Adaptive算法的垃圾收集器就是監控當前堆的使用情況,並將選擇適當算法的垃圾收集器。
參考:
http://blog.csdn.net/program_think/article/details/4302366 編程隨想 Java性能優化[4]:關於finalize函數
http://developer.51cto.com/art/201103/248642.htm 詳解Java GC的工作原理
http://tech.qq.com/a/20060726/000329.htm 全面分析Java的垃圾回收機制