1. 程式人生 > >Java四種引用解析以及在Android的應用

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

ram 1.7 reference 不足 虛引用 足夠 不用 詳情 ide

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方法)引用的對象

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

技術分享圖片

左邊的都是可達的,而右邊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類來實現虛引用,由於沒辦法通過虛引用來獲取一個對象實例,為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知,一般情況下在實際的項目中不會用到,大家了解一下就好。

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