Java四種引用解析以及在Android的應用

weixin_34127717發表於2017-12-19

JVM垃圾回收(GC)機制

我們知道,Java垃圾回收(GC)機制是JVM的重要組成部分,也是JVM平常工作的重點,事實上,JVM的後臺執行緒每時每刻都在監控整個應用程式的狀態,並在必要的時候啟動GC,回收記憶體一些沒有被引用的記憶體,那麼是如何找到這些需要回收的記憶體呢,我們先來看一段程式碼:

public class GCDemo {
    private Object instance = null;
    private static final int _1MB = 1024 * 1024;

    private byte[] bytes = new byte[_1MB];

    public static void main(String[] args) {
        GCDemo gcDemo = new GCDemo();
        GCDemo gcDemo1 = new GCDemo();
        gcDemo.instance=gcDemo1;
        gcDemo1.instance=gcDemo;

        gcDemo=null;
        gcDemo1=null;
        System.gc();
    }
}
複製程式碼

下面看一下記憶體回收的列印:,記得執行選項加上-XX:+PrintGCDetails選項

[GC (System.gc()) [PSYoungGen: 4669K->696K(37888K)] 4669K->704K(123904K), 0.0049445 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 696K->0K(37888K)] [ParOldGen: 8K->603K(86016K)] 704K->603K(123904K), [Metaspace: 3008K->3008K(1056768K)], 0.0078845 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 37888K, used 983K [0x00000000d6000000, 0x00000000d8a00000, 0x0000000100000000)
 這裡省略了一些非必要資訊
複製程式碼

可以看到從4669->696的結果來看,這種相互引用的記憶體,最後還是被回收了. 這種回收演算法叫做引用計數法,就是給物件中新增一個引用計數器,每當一個地方引用這個物件時,計數器值+1;當引用失效時,計數器值-1。任何時刻計數值為0的物件就是不可能再被使用的。這種演算法使用場景很多,但是這種演算法很難解決物件之間相互引用的情況,就比如上面的例子的執行結果顯示,所以Java並沒有用這種回收演算法,那麼Java是使用什麼演算法來找到按下需要被回收的記憶體的呢?答案是可達性分析演算法。

可達性分析法

這個演算法的基本思想是通過一系列稱為“GC Roots”的物件作為起始點,從這些節點向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈(即GC Roots到物件不可達)時,則證明此物件是不可用的。在Java語言中可以作為GC Roots的物件包括:

● 虛擬機器棧中引用的物件

● 方法區中靜態屬性引用的物件

● 方法區中常量引用的物件

● 本地方法棧中JNI(即Native方法)引用的物件

可達性分析演算法如圖所示:

Java四種引用解析以及在Android的應用
左邊的都是可達的,而右邊Object4,Object5,Object6雖然有引用,但是因為到GC Roots是不可達的,因此Java也是會回收掉這部分的記憶體的。

Java中四種引用狀態分析

在JDK1.2之前,Java中引用的定義很傳統:如果引用型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。這種定義沒有錯誤,但是過於籠統,實際上只是簡單的說明了一個物件只有被引用或者沒被引用兩種狀態。而我們希望描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體中;如果記憶體空間在進行垃圾收集後還是非常緊張,則可以拋棄這些物件。很多系統的快取功能都符合這樣的應用場景。因此在JDK1.2之後,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用4種,這4種引用強度依次減弱,下面將分析每種引用在記憶體回收時候的表現以及涉及到的在Android中的具體應用。

在寫程式碼之前,先配置一下引數: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails

這說明:

● 堆大小固定為20M(最小和最大都為20M,所以就固定為20M了)

● 新生代大小為10M,SurvivorRatio設定為8,則Eden區大小=8M,每個Survivor區大小=1M,每次有9M的新生代記憶體空間可用來new物件

● 當發生GC的時候列印GC的簡單資訊,當程式執行結束列印GC詳情

●. 強引用

程式碼中普遍存在的類似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件,哪怕是JVM丟擲OutOfMemoryError異常,也不會回收記憶體的,下面看一段程式碼即可明白

public class GCDemo {

    private static final int _1MB = 1024 * 1024;
    private byte[] bytes = new byte[_1MB];

    public static void main(String[] args) {
        test();
    }

    private static void test() {
        byte[] bytes1 = new byte[5 * _1MB];
        byte[] bytes2 = new byte[5 * _1MB];
        System.gc();
    }
}
結果如下:
  [Full GC (System.gc()) [Tenured: 5120K->5120K(10240K), 0.0018258 secs] 10993K->10843K(19456K), [Metaspace: 3090K->3090K(1056768K)], 0.0018492 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 6023K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  73% used [0x00000000fec00000, 0x00000000ff1e1db0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb00010, 0x00000000ffb00200, 0x0000000100000000)
 Metaspace       used 3110K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 340K, capacity 386K, committed 512K, reserved 1048576K
複製程式碼

我們可以看到,即使GC了也沒有回收,而且共有10993K記憶體轉移到了老年代了,從10993K->10843K可以判斷出並沒有回收掉,也就是說10M的位元組沒有被回收,那麼我們加大一點測試看看會不會記憶體錯誤,

public class GCDemo {

    private static final int _1MB = 1024 * 1024;
    private byte[] bytes = new byte[_1MB];

    public static void main(String[] args) {
        test();
    }

    private static void test() {
        byte[] bytes1 = new byte[5 * _1MB];
        byte[] bytes2 = new byte[10* _1MB];
        System.gc();
    }
}
可以看到發生錯誤了,
[Full GC (Allocation Failure) [TenuredException in thread "main" java.lang.OutOfMemoryError: Java heap space
	at Collections.GCDemo.test(GCDemo.java:17)
	at Collections.GCDemo.main(GCDemo.java:12)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
: 5725K->5700K(10240K), 0.0018018 secs] 5725K->5700K(19456K), [Metaspace: 3042K->3042K(1056768K)], 0.0018229 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 322K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
複製程式碼

那要怎樣釋放呢,手動置為null,就失去了GC Roots引用連,這樣就可以回收了

public class GCDemo {

    private static final int _1MB = 1024 * 1024;
    private byte[] bytes = new byte[_1MB];

    public static void main(String[] args) {
        test();
        //System.gc
    }

    private static void test() {
        byte[] bytes1 = new byte[5 * _1MB];
        byte[] bytes2 = new byte[5 * _1MB];
        bytes1 = null;
        bytes2 = null;
        System.gc();
    }
}
[Full GC (System.gc()) [TenuredJava HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
: 5120K->602K(10240K), 0.0018229 secs] 11015K->602K(19456K), [Metaspace: 3069K->3069K(1056768K)], 0.0018489 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 299K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
複製程式碼

可以看到從11015K->602K,說明手動置null之後經過了一個gc,那些都被回收了,實際上如果不手動置為null,也可以在方法執行之後再呼叫System.gc()方法的,這樣一樣可以回收記憶體,其原因是test()只是一個方法,當JVM執行完方法返回的時候,會清空當前的棧幀,而測試的是在方法內分配的,自然就會隨著方法結束而釋放掉記憶體了,就是註釋去掉,然後不用手動置null,是一樣的效果來的。

軟引用

軟引用是用來描述一些還有用但並非必需的物件,對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK1.2之後,提供了SoftReference類來實現軟引用,下面看程式碼,由於區別只是跟上面的方法的程式碼區別,因此這裡只寫出方法的程式碼了:

private static void test() {
        byte[] bytes1 = new byte[5 * _1MB];
        SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes1);
        System.out.println("GC前:" + softReference.get());
        bytes1 = null;
        System.gc();
        System.out.println("GC後:" + softReference.get());
    }
複製程式碼

結果如下:

GC前:[B@1540e19d
[Full GC (System.gc()) [Tenured: 0K->5725K(10240K), 0.0038452 secs] 6598K->5725K(19456K), [Metaspace: 3042K->3042K(1056768K)], 0.0038802 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
GC後:[B@1540e19d
Heap
 par new generation   total 9216K, used 322K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
複製程式碼

可以看到 6598K->5725K,並沒有回收掉,因為當前的記憶體還是足夠的,而且從獲取來看,也不為null,現在加大測試,看程式碼:

 private static void test() {
        SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[4*_1MB]);
        SoftReference<byte[]> softReference1 = new SoftReference<byte[]>(new byte[4*_1MB]);
        SoftReference<byte[]> softReference2 = new SoftReference<byte[]>(new byte[4*_1MB]);
        SoftReference<byte[]> softReference3 = new SoftReference<byte[]>(new byte[4*_1MB]);
        SoftReference<byte[]> softReference4 = new SoftReference<byte[]>(new byte[4*_1MB]);
        SoftReference<byte[]> softReference5 = new SoftReference<byte[]>(new byte[4*_1MB]);

        System.out.println("GC後:" + softReference.get());
        System.out.println("GC1後:" + softReference1.get());
        System.out.println("GC2後:" + softReference2.get());
        System.out.println("GC3後:" + softReference3.get());
        System.out.println("GC4後:" + softReference4.get());
        System.out.println("GC5後:" + softReference5.get());
    }
複製程式碼

下面是執行結果:

: 4195K->23K(9216K), 0.0021830 secs] 8870K->8793K(19456K), 0.0021967 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
GC後:null
GC1後:null
GC2後:null
GC3後:[B@1540e19d
GC4後:[B@677327b6
GC5後:[B@14ae5a5
Heap
 par new generation   total 9216K, used 4454K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
複製程式碼

大家可以看到,前面三個被回收了,而後面三個就正常列印了,這就證明了軟引用在記憶體不足的時候會釋放掉引用,進而被回收, 所以,很多時候對一些非必需的物件,我們可以將直接將其與軟引用關聯,這樣記憶體不夠時會先回收軟引用關聯的物件而不會丟擲OutOfMemoryError,畢竟丟擲OutOfMemoryError意味著整個應用將停止執行,這個軟引用在Android中發揮了重要的作用,特別是在快取方面,由於一些需求,需要加快顯示妥或者資料之類,需要用到記憶體上的快取,但是要求在系統記憶體緊張的時候就回收掉,因此這個場景下就非常適合用軟引用做快取了,下面舉個android中的圖片載入的例子,在還沒有ImageLoader,Glide,Fresco的時候,圖片載入需要自己封裝,記憶體快取也可以用這種的,下面看一下簡單的程式碼分析:

public class ImageLoader {

    private static volatile ImageLoader sInstance;
    private Handler mHandler = new Handler(Looper.getMainLooper());

    private ImageLoader() {
        mCache.clear();
    }

    public static ImageLoader getInstance() {
        if (sInstance == null) {
            synchronized (ImageLoader.class) {
                if (sInstance == null) {
                    sInstance = new ImageLoader();
                }
            }
        }
        return sInstance;
    }

    private Map<String, SoftReference<Drawable>> mCache = new HashMap<>();

    /**
     * 載入圖片
     */
    public void loadDrawable(final String path, ImageLoaderCallback callback) {
        //有快取
        if (mCache.containsKey(path)) {
            SoftReference<Drawable> softReference = mCache.get(path);
            if (softReference != null) {
                Log.d("[app]", "從快取獲取");
                Drawable drawable = softReference.get();
                if (drawable != null) {
                    callback.imageLoad(drawable);
                } else {
                    loadImageFromUrl(path, callback);
                }
            }
        }
        //沒有快取,將從網路獲取載入
        else {
            loadImageFromUrl(path, callback);
        }
    }

    private void loadImageFromUrl(String imagePath, ImageLoaderCallback callback) {
        UserThread userThread = new UserThread(imagePath, callback);
        userThread.start();
    }

    private Drawable loadImageFromUrl(String path) {
        try {
            return Drawable.createFromStream(new URL(path).openStream(), "imageLoader");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //定義一個回撥介面
    public interface ImageLoaderCallback {
        void imageLoad(Drawable drawable);
    }

    class UserThread extends Thread {
        private String imagePath;
        private ImageLoaderCallback callback;

        public UserThread(String imagePath, ImageLoaderCallback callback) {
            this.imagePath = imagePath;
            this.callback = callback;
        }

        @Override
        public void run() {
            super.run();
            final Drawable drawable = loadImageFromUrl(imagePath);
            if (drawable != null) {
                Log.d("[app]", "從網路獲取圖片並且加入快取");
                mCache.put(imagePath, new SoftReference<Drawable>(drawable));
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (callback != null) {
                            callback.imageLoad(drawable);
                        }
                    }
                });
            }
        }
    }
}
複製程式碼

當然,這是隨便寫的測試的程式碼,實際上需要考慮很多問題,在程式碼中,我們用一個Map<String,SortReference>來做記憶體的快取,可以看到在載入的時候首先判斷是否有快取,如果沒有的話,就從網路載入並且儲存起來,下次如果是有就直接載入圖片了,下面是測試程式碼:

ImageLoader.getInstance().loadDrawable("http://news.21-sun.com/UserFiles/x_Image/x_20150606083511_0.jpg",
                new ImageLoader.ImageLoaderCallback() {
                    @Override
                    public void imageLoad(Drawable drawable) {
                        Log.d("[app]", "drawable=" + drawable);
                    }
                });
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                ImageLoader.getInstance().loadDrawable("http://news.21-sun.com/UserFiles/x_Image/x_20150606083511_0.jpg",
                        new ImageLoader.ImageLoaderCallback() {
                            @Override
                            public void imageLoad(Drawable drawable) {
                                Log.d("[app]", "drawable=" + drawable);
                            }
                        });
            }
        },3000);
    結果為:
12-18 14:32:11.743 19735-19788/com.example.hotfixdemo D/[app]: 從網路獲取圖片並且加入快取
12-18 14:32:11.743 19735-19735/com.example.hotfixdemo D/[app]: drawable=android.graphics.drawable.BitmapDrawable@aef5e9
12-18 14:32:14.457 19735-19735/com.example.hotfixdemo D/[app]: 從快取獲取
12-18 14:32:14.457 19735-19735/com.example.hotfixdemo D/[app]: drawable=android.graphics.drawable.BitmapDrawable@aef5e9
複製程式碼

可以看到,在第一次載入的時候沒有快取便從網路獲取,然後會加入到快取裡面,第二次載入的時候就直接從快取獲取,這樣就加快了圖片的顯示了,當然了,除了圖片的圖片快取,列表的記憶體快取或者其他資料的記憶體快取都是可以利用軟引用的,大家可以在實際專案中用用就知道了。

弱引用

弱引用也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,跟軟引用記憶體不足被回收不同的是,被弱引用關聯的物件,只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK1.2之後,提供了WeakReference類來實現弱引用,下面我們來測試一下:

private static void test() {
        WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[8 * _1MB]);
        System.out.println("弱引用GC前:" + weakReference.get());
        System.gc();
        System.out.println("弱引用GC後:" + weakReference.get());
    }
    測試結果如下:
    弱引用GC前:[B@1540e19d
[Full GC (System.gc()) [Tenured: 8192K->608K(10240K), 0.0019216 secs] 9670K->608K(19456K), [Metaspace: 3095K->3095K(1056768K)], 0.0019498 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
弱引用GC後:null
Heap
 par new generation   total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
複製程式碼

從8192K->608K以及弱引用之後的結果來看,弱引用的物件在gc之後確實被回收了,而不是在記憶體不足的時候才會被回收,弱引用在Andsroid中也很多地方用到,由於在gc之後就會被斷掉引用鏈,因此,在防止記憶體洩露方面可以發揮作用,比如Handler在Activity裡面,如果沒有定義為靜態類,則持有外部類Activity的例項,在頁面銷燬的時候,如果還沒有釋放掉引用,就容易導致記憶體洩露。因此可以用靜態的Handler來弱引用Activity即可斷掉引用鏈,下面是程式碼:

 private static class UserHandler extends Handler{
        private WeakReference<MainActivity>  weakReference;
        public UserHandler(MainActivity mainActivity){
            weakReference=new WeakReference<>(mainActivity);
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (weakReference!=null){
                MainActivity mainActivity=weakReference.get();
                if (mainActivity!=null){
                    //具體業務邏輯...
                }
            }
        }
    }
複製程式碼

類似這樣的就可以有效防止持有外部Activity而造成記憶體洩露了,除了Handler,圖片的持有也是可以利用弱引用的,總之,要理解在垃圾收集器工作的時候,被弱引用的物件都會被回收,這個特點,然後根據實際業務就可以適當利用了。

虛引用

虛引用,它是最弱的一中引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。在JDK1.2之後,提供給了PhantomReference類來實現虛引用,由於沒辦法通過虛引用來獲取一個物件例項,為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知,一般情況下在實際的專案中不會用到,大家瞭解一下就好。

感謝大家閱讀。

相關文章