深入淺出JVM(十四)之記憶體溢位、洩漏與引用

菜菜的后端私房菜發表於2024-02-27

本篇文章將深入淺出的介紹Java中的記憶體溢位與記憶體洩漏並說明強引用、軟引用、弱引用、虛引用的特點與使用場景

引用

在棧上的reference型別儲存的資料代表某塊記憶體地址,稱reference為某記憶體、某物件的引用

實際上引用分為很多種,從強到弱分為:強引用 > 軟引用 > 弱引用 > 虛引用

平常我們使用的引用實際上是強引用,各種引用有自己的特點,下文將一一介紹

強引用就是Java中普通的物件,而軟引用、弱引用、虛引用在JDK中定義的類分別是SoftReferenceWeakReferencePhantomReference

下圖是軟引用、弱引用、虛引用、引用佇列(搭配虛引用使用)之間的繼承關係

image-20201119220035297.png

記憶體溢位與記憶體洩漏

為了更清除的描述引用之間的作用,首先需要介紹一下記憶體溢位和記憶體洩漏

當發生記憶體溢位時,表示JVM沒有空閒記憶體為新物件分配空間,丟擲OutOfMemoryError(OOM)

當應用程式佔用記憶體速度大於垃圾回收記憶體速度時就可能發生OOM

丟擲OOM之前通常會進行Full GC,如果進行Full GC後依舊記憶體不足才丟擲OOM

JVM引數-Xms10m -Xmx10m -XX:+PrintGCDetails

image-20210504133922780.png

記憶體溢位可能發生的兩種情況:

  1. 必須的資源確實很大,堆記憶體設定太小 (透過-Xmx來調整)

<!---->

  1. 發生記憶體洩漏,建立大量物件,且生命週期長,不能被回收

記憶體洩漏Memory Leak: 物件不會被程式用到了,但是不能回收它們

物件不再使用並且不能回收就會一直佔用空間,大量物件發生記憶體洩漏可能發生記憶體溢位OOM

廣義記憶體洩漏:不正確的操作導致物件生命週期變長

  1. 單例中引用外部物件,當這個外部物件不用了,但是因為單例還引用著它導致記憶體洩漏
  2. 一些需要關閉的資源未關閉導致記憶體洩漏

強引用

強引用是程式程式碼中普遍存在的引用賦值,比如List list = new ArrayList();

只要強引用在可達性分析演算法中可達時,垃圾收集器就不會回收該物件,因此不當的使用強引用是造成Java記憶體洩漏的主要原因

軟引用

當記憶體充足時不會回收軟引用

只有當記憶體不足時,發生Full GC時才將軟引用進行回收,如果回收後還沒充足記憶體則丟擲OOM異常

JVM中針對不同的區域(年輕代、老年代、元空間)有不同的GC方式,Full GC的回收區域為整個堆和元空間

軟引用使用SoftReference

記憶體充足情況下的軟引用
     public static void main(String[] args) {
         int[] list = new int[10];
         SoftReference listSoftReference = new SoftReference(list);
         //[I@61bbe9ba
         System.out.println(listSoftReference.get());
     }
記憶體不充足情況下的軟引用(JVM引數:-Xms5m -Xmx5m -XX:+PrintGCDetails)
 //-Xms5m -Xmx5m -XX:+PrintGCDetails
 public class SoftReferenceTest {
     public static void main(String[] args) {
         int[] list = new int[10];
         SoftReference listSoftReference = new SoftReference(list);
         list = null;
 ​
         //[I@61bbe9ba
         System.out.println(listSoftReference.get());
 ​
         //模擬空間資源不足
         try{
             byte[] bytes = new byte[1024 * 1024 * 4];
             System.gc();
         }catch (Exception e){
             e.printStackTrace();
         }finally {
             //null
             System.out.println(listSoftReference.get());
         }
     }
 }

弱引用

無論記憶體是否足夠,當發生GC時都會對弱引用進行回收

弱引用使用WeakReference

記憶體充足情況下的弱引用
     public static void test1() {
         WeakReference<int[]> weakReference = new WeakReference<>(new int[1]);
         //[I@511d50c0
         System.out.println(weakReference.get());
 ​
         System.gc();
         
         //null
         System.out.println(weakReference.get());
     }
WeakHashMap

JDK中有一個WeakHashMap,使用與Map相同,只不過節點為弱引用

image-20210504212605513.png

當key的引用不存在引用的情況下,發生GC時,WeakHashMap中該鍵值對就會被刪除

     public static void test2() {
         WeakHashMap<String, String> weakHashMap = new WeakHashMap<>();
         HashMap<String, String> hashMap = new HashMap<>();
 ​
         String s1 = new String("3.jpg");
         String s2 = new String("4.jpg");
 ​
         hashMap.put(s1, "圖片1");
         hashMap.put(s2, "圖片2");
         weakHashMap.put(s1, "圖片1");
         weakHashMap.put(s2, "圖片2");
 ​
         //只將s1賦值為空時,堆中的3.jpg字串還會存在強引用,所以要remove
         hashMap.remove(s1);
         s1=null;
         s2=null;
 ​
         System.gc();
 ​
         //4.jpg=圖片2
         test2Iteration(hashMap);
 ​
         //4.jpg=圖片2
         test2Iteration(weakHashMap);
     }
 ​
     private static void test2Iteration(Map<String, String>  map){
         Iterator iterator = map.entrySet().iterator();
         while (iterator.hasNext()){
            Map.Entry entry = (Map.Entry) iterator.next();
             System.out.println(entry);
         }
     }

未顯示刪除weakHashMap中的該key,當這個key沒有其他地方引用時就刪除該鍵值對

軟引用,弱引用適用的場景

資料量很大佔用記憶體過多可能造成記憶體溢位的場景

比如需要載入大量資料,全部載入到記憶體中可能造成記憶體溢位,就可以使用軟引用、弱引用來充當快取,當記憶體不足時,JVM對這些資料進行回收

使用軟引用時,可以自定義Map進行儲存Map<String,SoftReference<XXX>> cache

使用弱引用時,則可以直接使用WeakHashMap

軟引用與弱引用的區別則是GC回收的時機不同,軟引用存活可能更久,Full GC下才回收;而弱引用存活可能更短,發生GC就會回收

虛引用

使用PhantomReference建立虛引用,需要搭配引用佇列ReferenceQueue使用

無法透過虛引用得到該物件例項(其他引用都可以得到例項)

虛引用只是為了能在這個物件被收集器回收時收到一個通知

引用佇列搭配虛引用使用
 public class PhantomReferenceTest {
     private static PhantomReferenceTest reference;
     private static ReferenceQueue queue;
 ​
     @Override
     protected void finalize() throws Throwable {
         super.finalize();
         System.out.println("呼叫finalize方法");
         //搭上引用鏈
         reference = this;
     }
 ​
     public static void main(String[] args) {
         reference = new PhantomReferenceTest();
         //引用佇列
         queue = new ReferenceQueue<>();
         //虛引用
         PhantomReference<PhantomReferenceTest> phantomReference = new PhantomReference<>(reference, queue);
         
         Thread thread = new Thread(() -> {
             PhantomReference<PhantomReferenceTest> r = null;
             while (true) {
                 if (queue != null) {
                     r = (PhantomReference<PhantomReferenceTest>) queue.poll();
                     //說明被回收了,得到通知
                     if (r != null) {
                         System.out.println("例項被回收");
                     }
                 }
             }
         });
         thread.setDaemon(true);
         thread.start();
 ​
 ​
         //null (獲取不到虛引用)
         System.out.println(phantomReference.get());
 ​
         try {
             System.out.println("第一次gc 物件可以復活");
             reference = null;
             //第一次GC 引用不可達 守護執行緒執行finalize方法 重新變為可達物件
             System.gc();
             TimeUnit.SECONDS.sleep(1);
             if (reference == null) {
                 System.out.println("object is dead");
             } else {
                 System.out.println("object is alive");
             }
             reference = null;
             System.out.println("第二次gc 物件死了");
             //第二次GC 不會執行finalize方法 不能再變為可達物件
             System.gc();
             TimeUnit.SECONDS.sleep(1);
             if (reference == null) {
                 System.out.println("object is dead");
             } else {
                 System.out.println("object is alive");
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 ​
     }
 }

結果:

 /*
 null
 第一次gc 物件可以復活
 呼叫finalize方法
 object is alive
 第二次gc 物件死了
 例項被回收
 object is dead
 */

第一次GC時,守護執行緒執行finalize方法讓虛引用重新可達,所以沒死

第二次GC時,不再執行finalize方法,虛引用已死

虛引用回收後,引用佇列有資料,來通知告訴我們reference這個物件被回收了

使用場景

GC只能回收堆內記憶體,而直接記憶體GC是無法回收的,直接記憶體代表的物件建立一個虛引用,加入引用佇列,當這個直接記憶體不使用,這個代表直接記憶體的物件為空時,這個虛記憶體就死了,然後引用佇列會產生通知,就可以通知JVM去回收堆外記憶體(直接記憶體)

總結

本篇文章圍繞引用深入淺出的解析記憶體溢位與洩漏、強引用、軟引用、弱引用、虛引用

當JVM沒有足夠的記憶體為新物件分配空間時就會發生記憶體溢位丟擲OOM

記憶體溢位有兩種情況,一種是分配的資源太少,不滿足必要物件的記憶體;另一種是發生記憶體洩漏,不合理的設定物件的生命週期、不關閉資源都會導致記憶體洩漏

使用最常見的就是強引用,強引用只有在可達性分析演算法中不可達時才會回收,強引用使用不當是造成記憶體洩漏的原因之一

使用SoftReference軟引用時,只要記憶體不足觸發Full GC時就會對軟引用進行回收

使用WeakReference弱引用時,只要發生GC就會對弱引用進行回收

軟、弱引用可以用來充當大資料情況下的快取,它們的區別就是軟引用可能活的更久Full GC才回收,使用弱引用時可以直接使用JDK中提供的WeakHashMap

虛引用無法在程式中獲取,與引用佇列搭配使用,當虛引用被回收時,能夠從引用佇列中取出(感知),可以在直接引用不使用時,發出訊息讓JVM進行回收

最後(一鍵三連求求拉~)

本篇文章將被收入JVM專欄,覺得不錯感興趣的同學可以收藏專欄喲~

本篇文章筆記以及案例被收入 gitee-StudyJavagithub-StudyJava 感興趣的同學可以stat下持續關注喔\~

有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關注、收藏支援一下\~

關注菜菜,分享更多幹貨,公眾號:菜菜的後端私房菜

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章