Java四种引用解析以及在Android的应用

时间:2022-04-26
本文章向大家介绍Java四种引用解析以及在Android的应用,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

作者 | stormWen

地址 | https://juejin.im/post/5a37355b6fb9a0450003256c

声明 | 本文是 stormWen 原创,已获授权发布,未经原作者允许请勿转载

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类来实现虚引用,由于没办法通过虚引用来获取一个对象实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,一般情况下在实际的项目中不会用到,大家了解一下就好。