細述 Java垃圾回收機制→How Java Garbage Collection Works?
這是垃圾回收機制系列文章的第二篇。希望您已經讀過了***部分Java垃圾回收簡介。
Java垃圾回收是一個自動運行的管理程序運行時使用的內存的進程。通過GC的自動執行JVM將程序員從申請和釋放內存的繁重操作中解放出來。
Java垃圾回收GC初始化
作為一個自動執行的進程,程序員不需要在代碼中主動初始化GC。Java提供了System.gc()和Runtime.gc()這兩個hook來請求JVM調用GC進程。
盡管要求系統機制給程序員提供調用GC的機會,但是實際上這是由JVM負責決定的。JVM可以選擇拒絕啟動GC的請求,因此并不保證這些請求會真的調用垃圾回收。這是JVM基于內存堆空間的Eden區的使用情況做出的決定。JVM規范將這個選擇權利留給了各個JVM的具體實現,因此實際上JVM是如何選擇的視不同JVM的實現而定(不過要記住的是,不能依賴于這兩個方法的調用,它們是不被保證執行的)。
毫無疑問的是,我們知道垃圾回收進程是不能強制執行的。不過我剛發現一個調用System.gc()確實有意義的場景。看下這篇文章你就會了解System.gc()調用是可用的這個特殊的場景。
Java 垃圾回收進程
垃圾回收是一個回收不再使用的內存空間并將它變成能夠為將來的實例使用的過程。
Eden Space:當一個實例被創建的時候,它最初被存放在堆內存空間的年輕代的Eden區中。
注意:如果您不太理解這些術語,建議您先看下介紹內存模型、JVM架構及這些術語的詳細解釋的文章:garbage-collection-introduction-tutorial
Survivor Space(S0 和S1):作為minor回收周期的一部分,還活著的對象(還有引用指向它)被從eden區中移動到survivor空間S0。同樣的,垃圾回收器掃描S0并將活著的實例移動到S1。
無用的對象(沒有引用指向)被標記并回收。垃圾回收器(有四種可用的垃圾回收器,將在下一篇文章中介紹)決定這些被標記的實例是在掃描的過程中移出內存還是在另外獨立的遷移進程中執行。
Old Generation:老年代或者***代是堆內存的第二個邏輯部分。當垃圾回收器在做minor GC周期中,S1 survivor區中還活著的實例會被提升到老年代中。S1區中不再被引用的對象被標記并清除。
Major GC:在Java垃圾回收過程中實例生命周期的***一個階段。Major GC在垃圾回收過程中掃描屬于Old Generation部分的堆內存。如果實例沒有被任何引用關聯,它們將被標記、清除;如果它們還被引用關聯著,則將繼續存留在old generation。
Memory
Fragmentation:一旦實例從堆內存中刪除了,它們原來的位置將空出來給以后分配實例使用。顯然這些空閑空間很容易在內存空間中產生碎片。為了能夠更快地分配實例地址,需要對內存做去碎片化操作。根據不同垃圾回收器的策略,被回收的內存將在回收的過程同時或者在GC另外獨立的過程中壓縮整合。
垃圾回收過程中的對象銷毀–Finalization
就在移除一個對象并回收它的內存空間之前,Java垃圾回收器將會調用各個實例的finalize()方法,這樣實例對象就有機會可以釋放掉它占用的資源。盡管finalize()方法是保證在回收內存空間之前執行的,但是對具體的執行時間和執行順序是沒有任何保證的。多個實例之間的finalize()執行順序是不能提前預知的,甚至有可能它們是并行執行的。程序不應該預先假設實例執行finalize()的方法,也不應該使用finalize()方法來回收資源。
- 在finalize過程中拋出的任何異常都默認被忽略掉了,同時對象的銷毀過程被取消
- JVM規范并沒有討論關于弱引用的垃圾回收,這是明確聲明的。具體的細節留給實現者決定。
- 垃圾回收是由守護進程執行的
對象何時變成可被垃圾回收的?
- 所有不能被活著的線程到達實例
- 不能被其他對象到達的循環引用對象 Java中有多種不同的引用類型。實例的可回收性取決于它的引用類型。
在編譯過程中Java編譯器有個優化機制,編譯器可以選擇將null賦值給一個實例,這樣就將這個實例標志為可被回收的。
- class Animal {
- public static void main(String[] args) {
- Animal lion = new Animal();
- System.out.println("Main is completed.");
- }
- protected void finalize() {
- System.out.println("Rest in Peace!");
- }
- }
在上面這個類中,實例lion在除了初始化那一行在其他地方都沒有被使用到。因此作為一種優化方法,Java編譯器可以在初始化那一行后面立即賦值lion = null。這樣finlizer可能會在Main方法的SOP之前打印結果。
- Rest in Peace!
- Main is completed.
但結果的順序是不確定的,它取決于JVM的實現以及運行時的內存使用情況。從中我們能知道的一點是:編譯器在發現一個實例的之后的程序中不再被引用時可以選擇提前釋放實例內存。
- 這里有個實例何時變成可回收更好的例子。實例所有的屬性可以被存儲在寄存器中之后可以從寄存器中讀取這些屬性值,且未來在任何情況下都不會將值寫回到實例對象中。這樣盡管這個實例在未來還是被使用到了,但是實例對象依然可以被標記為可回收的。
- 何時能被垃圾回收可以簡單到僅僅認為在賦值為null的時候也可以復雜到如上面那一點所說的那樣。JVM的實現者會做一些取舍。其目標都是希望留下最少的痕跡,提高響應時間增大吞吐量。為了能夠達到這些目的,JVM實現者可以在垃圾回收中選擇更好的模式或算法來回收內存。
- 當finalize()被調用的時候,JVM釋放掉當前線程的所有同步塊。
Example Program for GC Scope
- class GCScope {
- GCScope t;
- static int i = 1;
- public static void main(String args[]) {
- GCScope t1 = new GCScope();
- GCScope t2 = new GCScope();
- GCScope t3 = new GCScope();
- //沒有任何一個對象是可以被GC的
- t1.t = t2;//沒有任何一個對象是可以被GC的
- t2.t = t3;//沒有任何一個對象是可以被GC的
- t3.t = t1;//沒有任何一個對象是可以被GC的
- t1 = null;//沒有任何一個對象是可以被GC的,t3.t還有對t1的引用
- t2 = null;//沒有任何一個對象是可以被GC的,t3.t.t還有對t2的引用
- t3 = null;//所有3個對象都可以被GC(沒有一個被引用了)
- //只有各個對象的變量t互相循環引用形成了一個孤立的引用環,而沒有外部引用
- }
- protected void finalize() {
- System.out.println("Garbage collected from boject" + i);
- i++;
- }
- }
Example Program for GC OutOfMemoryError
垃圾回收機制并不保證發生內存溢出時的安全,事實上內存溢出將會導致程序的崩潰,拋出OutOfMemoryError。import java.util.LinkedList;
- import java.util.List;
- public class GC {
- public static void main(String[] args[]) {
- List l = new LinkedList();
- //進入內部***循環直接向鏈表中不斷添加元素
- do {
- l.add(new String("Hello, World!");
- } while (true);
- }
- }
Output
- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- at java.util.LinkedList.linkLast(LinkedList.java:142)
- at java.util.LinkedList.add(LinkedList.java:338)
- at com.javapapers.java.GCScope.main(GCScope.java:12)