JVM03------垃圾收集(下)
一. 什麼是GC
Java與C語言相比的一個優勢是,可以通過自己的JVM自動分配和回收記憶體空間。
垃圾回收機制是由垃圾收集器Garbage Collection來實現的,GC是後臺一個低優先順序的守護程序。在記憶體中低到一定限度時才會自動執行,因此垃圾回收的時間是不確定的。
為何要這樣設計:因為GC也要消耗CPU等資源,如果GC執行過於頻繁會對Java的程式的執行產生較大的影響,因此實行不定期的GC。
與GC有關的是:JVM執行時資料區中的堆(物件例項會儲存在這裡)和 gabagecollector方法。
垃圾回收GC只能回收通過new關鍵字申請的記憶體(在堆上),但是堆上的記憶體並不完全是通過new申請分配的。還有一些本地方法,這些記憶體如果不手動釋放,就會導致記憶體洩露,所以需要在finalize中用本地方法(nativemethod)如free操作等,再使用gc方法
System.gc();
二. 什麼是垃圾
Java中那些不可達的物件就會變成垃圾。物件之間的引用可以抽象成樹形結構,通過樹根(GC Roots)作為起點,從這些樹根往下搜尋,搜尋走過的鏈稱為引用鏈。
當一個物件到GC Roots沒有任何引用鏈相連時,則證明這個物件為可回收的物件。
可以作為GC Roots的主要有以下幾種:
(1)棧幀中的本地變量表所引用的物件。
(2)方法區中類靜態屬性和常量引用的物件。
(3)本地方法棧中JNI(Native方法)引用的物件。
//垃圾產生的情況舉例: //1.改變物件的引用,如置為null或者指向其他物件 Object obj1 = newObject(); Object obj2 = new Object(); obj1 = obj2; //obj1成為垃圾 obj1 = obj2 = null ; //obj2成為垃圾
//2.引用型別 //第2句在記憶體不足的情況下會將String物件判定為可回收物件,第3句無論什麼情況下String物件都會被判定為可回收物件 String str = new String("hello"); SoftReference<String> sr = new SoftReference<String>(new String("java")); WeakReference<String> wr = newWeakReference<String>(new String("world"));
//3.迴圈每執行完一次,生成的Object物件都會成為可回收的物件 for(int i=0;i<10;i++) { Object obj = new Object(); System.out.println(obj.getClass()); }
//4.類巢狀 class A{ A a; } A x = new A();//分配了一個空間 x.a = new A();//又分配了一個空間 x = null;//產生兩個垃圾
//5.執行緒中的垃圾 calss A implements Runnable{ void run(){ //.... } } //main A x = new A(); x.start(); x=null; //執行緒執行完成後x物件才被認定為垃圾
三. Java引用型別劃分
引用型別可以說是整個Java開發的靈魂所在,如果沒有合理的引用操作,那麼就有可能產生垃圾問題。引用也需要一些合理化的設計,再很多時候並不是所有的物件需要一直被使用。
JDK1.2之後關於引用提出了四種方案:
- 強引用:當記憶體不足的時候,JVM寧可出現OutOfMermeory,也需要儲存,並且不會將此空間回收。Object obj=new Object();
- 軟引用:當記憶體不足的時候,進行物件的回收處理。往往用於快取記憶體中。
- 弱引用:不管記憶體是否緊張,只要有垃圾產生了,就立即回收
- 虛引用(幽靈引用):和沒有引用是一樣的
在我們目前接觸中,只需要接觸到強引用。
1. 強引用
它是JVM預設支援的引用模式,即:在引用期間內,如果該記憶體被指定的棧記憶體引用,那麼該物件就無法被GC回收。一旦出現了記憶體空間不足,就會出現“OOM”錯誤。
舉例:觀察強引用
public class Test { public static void main(String args[]) { Object object=new Object();//強引用 Object ref=object;//引用傳遞 object=null;//斷開了一個連線,斷開了obj到new object的連線,但是ref還在引用它 System.gc(); System.out.println(ref); } }
[email protected]
執行結果發現物件還在。就說明了,如果堆記憶體有一個棧記憶體指向,那麼該物件將無法被GC回收。
強引用是我們一直在使用的模式,並且也是以後的開發之中主要的使用模式。正因為強引用有這樣的記憶體分配問題,因此儘量少例項化物件。
2. 軟引用
在許多的開源元件中,往往會使用軟引用作為快取元件出現。它最大的特點就是在記憶體空間不足的時候回收,充足就不回收。如果想要實現軟引用,則需要有一個單獨的類來實現控制:java.lang.ref.SoftReference。
軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。
舉例:觀察軟引用
public class Test { public static void main(String args[]) { Object object=new Object(); SoftReference<Object> reference=new SoftReference<Object>(object); object=null;//斷開連線 System.gc(); System.out.println(reference.get()); } }
[email protected]
Obj=null的執行本來會讓new Object()成為垃圾,可以因為軟引用ref的存在,使用了這個obj物件,所以,它不會被回收。現在記憶體不緊張。
如果要弄成記憶體緊張,可以配置VM引數來測試,輸出將會是null
3. 弱引用
弱引用的本質含義就是隻要已進行GC,就會被立即回收,不過由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些弱引用的物件。弱引用需要使用的是MAP介面的子類(java.util.WeakHashMap)。
弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。
舉例:觀察弱引用
public class Test { public static void main(String args[]) { String key=new String("hello"); String value=new String("world"); Map<String, String> map=new WeakHashMap<>(); map.put(key, value); System.out.println(map); key=null; System.out.println(map); System.gc(); System.out.println(map); } }
{hello=world} {hello=world} {}
只要一旦出現GC,則必須進行回收處理。
使用WeakHashMap和HashMap的區別
前者是弱引用,後者是強引用。
Java.lang.ref.WeakRefernce(這個弱引用類相較於上面weakhashmap更少用)
觀察這個弱引用類:
public class Test { public static void main(String args[]) { String key=new String("hello"); WeakReference<String> reference=new WeakReference<String>(key); System.out.println(reference.get()); key=null; System.out.println(reference.get()); System.gc(); System.out.println(reference.get()); } }
hello hello null
我們一般不使用弱引用。它之所以不敢輕易使用的原因就是因為其本身一旦有了GC之後,就會立刻清空,這不利於程式的開發。(我們有個弱引用的概念就行了)
4. 引用佇列
它指的是儲存那些準備被回收的物件。很多得時候所有的物件的回收掃描都是從根物件開始的。那麼對於整個GC而言,如果想要確定哪些物件可以被回收,那麼就需要確定好引用的強度,這個也就是所謂得引用路徑的設定。
若果現在要找到物件5,那麼很明顯1找到5是(強+軟),2到5是(強+弱),軟引用要比弱引用儲存得更強一些,所以這個時候對於物件得引用而言,如果要進行引用得關聯得判斷,就必須找到強關聯。那麼為了避免非強關聯得引用物件帶來得記憶體問題,所以有一個引用佇列的概念。如果在建立軟引用或者弱引用的時候使用了引用佇列得方式,那麼這個物件被回收得時候會自動儲存在佇列之中。
舉例:使用引用佇列
弱引用中假如queue表示物件被回收之後會存在佇列裡面
public class Test { public static void main(String args[]) throws InterruptedException { Object object=new Object(); ReferenceQueue<Object> queue=new ReferenceQueue<>(); WeakReference<Object> reference=new WeakReference<Object>(object,queue); System.out.println(queue.poll()); object=null; System.gc(); Thread.sleep(200); System.out.println(queue.poll()); } }
null [email protected]
(note:物件儲存到引用佇列之中會需要一定時間,因此,觀察後一個列印,需要等一會兒)
這種引用佇列只是進行了一些被回收物件得控制,意義不大。
5. 虛引用(幽靈引用)
永遠取得不了幽靈資料
舉例:觀察幽靈引用
public class Test { public static void main(String args[]) throws InterruptedException { Object object=new Object(); ReferenceQueue<Object> queue=new ReferenceQueue<>(); PhantomReference<Object> reference=new PhantomReference<Object>(object, queue); System.gc(); System.out.println(reference.get()); System.out.println(queue.poll()); } }
null null
雖然沒有斷強引用連線,但是輸出還是Null,幽靈總是為Null
所有在幽靈引用型別中得資料都不會真正得保留。
(我們開發中關心最多得就是強引用)
四. 典型的垃圾回收演算法
在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裡面涉及到一個問題是:如何高效地進行垃圾回收。
下面討論幾種常見的垃圾收集演算法。
1. Mark-Sweep(標記-清除)演算法
標記-清除演算法分為兩個階段:標記階段和清除階段。
標記階段的任務是標記出所有需要被回收的物件,清除階段就是回收被標記的物件所佔用的空間。
標記-清除演算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生記憶體碎片,碎片太多可能會導致後續過程中需要為大物件分配空間時無法找到足夠的空間而提前觸發GC。
2. Coping(複製)演算法
Copying演算法將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把第一塊記憶體上的空間一次清理掉,這樣就不容易出現記憶體碎片的問題,並且執行高效。
但是該演算法導致能夠使用的記憶體縮減到原來的一半。而且,該演算法的效率跟存活物件的數目多少有很大的關係,如果存活物件很多,那麼Copying演算法的效率將會大大降低。(這也是為什麼後面提到的新生代採用Copying演算法)
3. Mark-Compact(標記-整理)演算法
為了解決Copying演算法的缺陷,充分利用記憶體空間,提出了Mark-Compact演算法。
該演算法標記階段標記出所有需要被回收的物件,但是在完成標記之後不是直接清理可回收物件,而是將存活的物件都移向一端,然後清理掉端邊界以外的所有記憶體(只留下存活物件)。
....還有別的。上面三種正式年輕代,老年代之前介紹的回收演算法。
目前大部分垃圾收集器對於新生代都採取Copying演算法,因為新生代中每次垃圾回收都要回收大部分物件,也就是說需要複製的操作次數較少,該演算法效率在新生代也較高。但是實際中並不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間(比例8:1:1),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將還存活的物件複製到另一塊Survivor空間中,然後清理掉Eden和A空間。在進行了第一次GC之後,使用的便是Eden space和B空間了,下次GC時會將存活物件複製到A空間,如此反覆迴圈。
當物件在Survivor區躲過一次GC的話,其物件年齡便會加1,預設情況下,物件年齡達到15時,就會移動到老年代中。一般來說,大物件會被直接分配到老年代,所謂的大物件是指需要大量連續儲存空間的物件,最常見的一種大物件就是大陣列,比如:byte[] data = newbyte[4*1024*1024]。
當然分配的規則並不是百分之百固定的,這要取決於當前使用的是哪種垃圾收集器組合和JVM的相關引數。這些搬運工作都是GC完成的,GC不僅負責在Heap中搬運例項,同時負責回收儲存空間。
最後,因為每次回收都只回收少量物件,所以老年代一般使用的是標記整理演算法。
Minor GC是新生代Copying演算法。MinorGC觸發條件:
(1)當Eden區滿時,觸發Minor GC。
Full GC的老年代,採取的Mark-Compact。Full GC觸發條件:
(1)呼叫System.gc時,系統建議執行Full GC,但是不必然執行。
(2)老年代空間不足。
(3)方法區空間不足。
(4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體。
五. 典型的垃圾回收器
G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。因此它是一款並行與併發收集器,並且它能建立可預測的停頓時間模型。
參考文獻:
https://blog.csdn.net/SEU_Calvin/article/details/51404589
《深入理解JAVA虛擬機器》
李興華老師的《Java記憶體模型》