Java篇 - 四种引用(Reference)实战

Java篇 - 四种引用(Reference)实战_第1张图片

 

Java的垃圾回收(GC)是虚拟机自动管理的,前面我有篇文章专门讲GC:《JVM篇 - GC给你整明白》

Java内存管理分为内存分配和内存回收,都不需要程序员负责,垃圾回收的机制主要是看对象是否有引用指向该对象。

Java对象的引用包括:强引用,软引用,弱引用,虚引用,Java中提供这四种引用类型主要有两个目的:

  • 可以让程序员通过代码的方式决定某些对象的生命周期。
  • 有利于JVM进行垃圾回收。

 

目录:

  1. 强引用(StrongReference)
  2. 软引用(SoftReference)
  3. 弱引用(WeakReference)
  4. 虚引用(PhantomReference)
  5. 引用队列(ReferenceQueue)

 

 

1. 强引用(StrongReference)

 

  • 1.1 概念

强引用是指创建一个对象并它赋值给一个引用,引用是存在JVM中的栈中的。

例如:

    private static void testStrongReference() {
        Object object = new Object();
        Object[] objects = new Object[1000];
    }

可以看到,testStrongReference()方法中,创建了一个对象和一个对象数组,当testStrongReference()执行完,JVM会自动回收它们(属于局部变量)。但是因为它们都是强引用,所以当执行到testStrongRefenerce()时,如果内存不足,不足够在堆中分配这些内存,那么JVM将抛出OutOfMemory,因为强引用的对象,只要有引用变量指向它们的时候,它们将不会被垃圾回收。

如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

 

  • 1.2 例子

看看Vector类的清理方法:

    protected Object[] elementData;
    
    public synchronized void removeAllElements() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < elementCount; i++)
            elementData[i] = null;

        elementCount = 0;
    }

在清除数据的时候,将数组中的每个元素都置为null,中断强引用与对象之间的关系,让GC的时候能够回收这些对象的内存。

 

 

 

2. 软引用(SoftReference)

我做Android时,第一次接触引用这个概念就是从软引用开始的,那是13年的时候,当时还没有那么多的第三方图片加载框架,图片加载都得自己写。当时的实现方法中,内存中缓存的Bitmap使用的就是软引用。

 

  • 2.1 概念

如果一个对象具有软引用,只要内存空间足够,垃圾回收器就不会回收它。如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。   
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

 

  • 2.2 例子

分享一个平时开发中的例子,就是上面说的图片缓存。

public class ImageMemoryCache {
	/**
	 * 从内存读取数据速度是最快的,为了更大限度使用内存,这里使用了两层缓存。 硬引用缓存不会轻易被回收,用来保存常用数据,不常用的转入软引用缓存。
	 */
	private static final int SOFT_CACHE_SIZE = 20; // 软引用缓存容量
	private static LruCache mLruCache; // 硬引用缓存
	private static LinkedHashMap> mSoftCache; // 软引用缓存

	public ImageMemoryCache(Context context) {
		int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
		int cacheSize = 1024 * 1024 * memClass / 4; // 硬引用缓存容量,为系统可用内存的1/4
		mLruCache = new LruCache(cacheSize) {
			@Override
			protected int sizeOf(String key, Bitmap value) {
				if (value != null)
					return value.getRowBytes() * value.getHeight();
				else
					return 0;
			}

			@Override
			protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
				if (oldValue != null)
					// 硬引用缓存容量满的时候,会根据LRU算法把最近没有被使用的图片转入此软引用缓存
					mSoftCache.put(key, new SoftReference(oldValue));
			}
		};
		mSoftCache = new LinkedHashMap>(SOFT_CACHE_SIZE, 0.75f, true) {
			private static final long serialVersionUID = 6040103833179403725L;

			@Override
			protected boolean removeEldestEntry(Entry> eldest) {
				if (size() > SOFT_CACHE_SIZE) {
					return true;
				}
				return false;
			}
		};
	}

	/**
	 * 从缓存中获取图片
	 */
	public Bitmap getBitmapFromCache(String url) {
		Bitmap bitmap;
		// 先从硬引用缓存中获取
		synchronized (mLruCache) {
			bitmap = mLruCache.get(url);
			if (bitmap != null) {
				// 如果找到的话,把元素移到LinkedHashMap的最前面,从而保证在LRU算法中是最后被删除
				mLruCache.remove(url);
				mLruCache.put(url, bitmap);
				return bitmap;
			}
		}
		// 如果硬引用缓存中找不到,到软引用缓存中找
		synchronized (mSoftCache) {
			SoftReference bitmapReference = mSoftCache.get(url);
			if (bitmapReference != null) {
				bitmap = bitmapReference.get();
				if (bitmap != null) {
					// 将图片移回硬缓存
					mLruCache.put(url, bitmap);
					mSoftCache.remove(url);
					return bitmap;
				} else {
					mSoftCache.remove(url);
				}
			}
		}
		return null;
	}

	/**
	 * 添加图片到缓存
	 */
	public void addBitmapToCache(String url, Bitmap bitmap) {
		if (bitmap != null) {
			synchronized (mLruCache) {
				mLruCache.put(url, bitmap);
			}
		}
	}
	
	public void removeBitmap(String url) {
		if (url != null) {
			synchronized (mLruCache) {
				mLruCache.remove(url);
			}
			synchronized (mSoftCache) {
				mSoftCache.remove(url);
			}
		}
	}

	public void clearCache() {
		mSoftCache.clear();
	}
}

 

 

 

3. 弱引用(WeakReference)

 

  • 3.1 概念

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。其实,上面图片缓存的框架用弱引用也可以做,特别是在移动设备上,内存容量有限。

 

  • 3.2 例子

还是以平时开发中遇到的问题为例:

(1) Handler内存泄漏问题

Android中使用Handler可以实现在不同线程间通信,如果在Activity界面使用Handler,由于Handler的机制(这边就不细说),当Activity退出后,处理不当,Handler会一直持有该Activity的引用,那引起的内存泄漏是很大的。如何解决呢?

private static class NoLeakHandler extends Handler{

        private WeakReference mActivityRef;

        public NoLeakHandler(Activity activity){
            mActivityRef = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mActivityRef.get() != null) {
                 // 处理相应的逻辑
            }
        }
    }
}

使用静态内部类(脱离外部类引用的限制) + 弱引用的方式解决这种内存泄漏的问题。当然解决源头还是在Activity退出的时候,移除Handler内部消息队列的数据。

 

(2) ViewPager适配器

Android中的ViewPager + Fragment是很多项目界面采用的组合,但是Fragment用的不好,性能上会相差很大,造成内存占用大,卡顿的问题。我一般会这么写ViewPager的适配器:

    private static final class TabPagerAdapter extends FragmentPagerAdapter {

        private final WeakReference[] data;

        @SuppressWarnings("unchecked")
        TabPagerAdapter(FragmentManager fm) {
            super(fm);
            data = (WeakReference[]) Array.newInstance(WeakReference.class, getCount());
        }

        @Override
        public Fragment getItem(int position) {
            if (position < 0 || position >= getCount()) {
                return new Fragment();
            }
            Fragment f = null;
            if (!Requires.isNull(data[position])) {
                f = data[position].get();
            } else {
                if (position == 0) {
                    f = HomeFragment.newInstance();
                } else if (position == 1) {
                    f = DappFragment.newInstance();
                } else if (position == 2) {
                    f = SettingsFragment.newInstance();
                }
                data[position] = new WeakReference<>(f);
            }
            return f;
        }

        @Override
        public int getCount() {
            return 3;
        }
    }

第一是懒加载方式,滑动到某个界面的时候,再构造对应的Fragment,同时Fragment使用弱引用包装。

 

 

 

4. 虚引用(PhantomReference)

 

4.1 概念

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

 

4.2 例子

    public static void main(String[] args) {
        Object object = new Object();
        ReferenceQueue referenceQueue = new ReferenceQueue<>();
        PhantomReference phantomReference = new PhantomReference<>(object, referenceQueue);
        // 一直返回null,PhantomReference的get()结果必定为null
        System.out.println("phantomReference.get() = " + phantomReference.get());
        Reference reference;
        while ((reference = referenceQueue.poll()) != null) {
            if (reference.equals(phantomReference)) {
                System.out.println("被回收了");
            }
        }
    } 
  

注意:PhantomReference的get()结果必定为null。

 

 

 

5. 引用队列(ReferenceQueue)

 

  • 5.1 概念

上面在虚引用中说到了引用队列,再举个例子,作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法:

ReferenceQueue queue = new ReferenceQueue();  
SoftReference  ref = new SoftReference(object, queue);  

那么当这个SoftReference所软引用的对象被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。

在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收,于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。

SoftReference ref = null;
while ((ref = (EmployeeRef) q.poll()) != null) {
    // 清除ref
}

再来看看几种引用的源码定义:

public class SoftReference extends Reference {

    static private long clock;

    private long timestamp;

    public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }

    public SoftReference(T referent, ReferenceQueue q) {
        super(referent, q);
        this.timestamp = clock;
    }

    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }
}
public class WeakReference extends Reference {

    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}
public class PhantomReference extends Reference {
        
    // get()只会返回null
    public T get() {
        return null;
    }

    public PhantomReference(T referent, ReferenceQueue q) {
        super(referent, q);
    }
}

可以看到,几种引用都提供了传入引用队列的构造器。

 

  • Reference类
public abstract class Reference {

    private T referent;

    volatile ReferenceQueue queue;

    @SuppressWarnings("rawtypes")
    Reference next;

    transient private Reference discovered; 

    static private class Lock { }
    private static Lock lock = new Lock();

    private static Reference pending = null;

    private static class ReferenceHandler extends Thread {

        private static void ensureClassInitialized(Class clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }

        static {
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }

    static boolean tryHandlePending(boolean waitForNotify) {
        Reference r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    if (waitForNotify) {
                        lock.wait();
                    }
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }

        if (c != null) {
            c.clean();
            return true;
        }

        ReferenceQueue q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

    public T get() {
        return this.referent;
    }

    public void clear() {
        this.referent = null;
    }

    public boolean isEnqueued() {
        return (this.queue == ReferenceQueue.ENQUEUED);
    }

    public boolean enqueue() {
        return this.queue.enqueue(this);
    }

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
} 
  

大致解释下:

Reference主要是负责内存的一个状态,当然它还和java虚拟机,垃圾回收器打交道。Reference类首先把内存分为4种状态Active,Pending,Enqueued,Inactive。

  • 一般来说内存一开始被分配的状态都是Active。
  • Pending是指快要被放进队列的对象,也就是马上要回收的对象。
  • Enqueued就是对象的内存已经被回收了,我们已经把这个对象放入到一个队列中,方便以后我们查询某个对象是否被回收。
  • Inactive就是最终的状态,不能再变为其它状态。

几个变量:

  • referent表示其引用的对象,即在构造的时候需要被包装在其中的对象。
  • queue是对象即将被回收时所要通知的队列。当对象即将被回收时,整个reference对象,而不仅仅是被回收的对象,会被放到queue里面,然后外部程序即可通过监控这个queue即可拿到相应的数据了。
  • next即当前引用节点所存储的下一个即将被处理的节点。但next仅在放到queue中才会有意义,因为只有在enqueue的时候,会将next设置为下一个要处理的Reference对象。为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了。而是引用一个特殊的 ENQUEUED(内部定义的一个空队列)。因为已经放到队列当中,并且不会再次放到队列当中。
  • discovered表示要处理的对象的下一个对象。即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered不断地拿到下一个对象赋值给pending即可,直到取到了最有一个。它是被JVM使用的。
  • pending是等待被入队的引用列表。JVM收集器会添加引用到这个列表,直到Reference-handler线程移除了它们。这个列表使用discovered 字段来连接它下一个元素(即 pending 的下一个元素就是discovered对象。r = pending; pending = r.discovered)。

当 Refrence类被加载的时候,会执行静态代码块。在静态代码块里面,会启动ReferenceHandler线程,并设置线程的级别为最大级别:Thread.MAX_PRIORITY。

在 tryHandlePending()方法里面,检查 pending是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入 wait状态。简单来说,垃圾回收器会把 References添加进入,Reference-handler thread会移除它,即discovered和pending 是由垃圾回收器进行赋值的。

 

总结一下:

Refrence和引用队列ReferenceQueue联合使用时,如果Refrence持有的对象被垃圾回收,Java虚拟机就会把这个引用加入到与之关联的引用队列中。Android中著名的内存泄漏检测工具LeakCannary就是用这种方式做的,后面的Android第三方库源码分析会有一篇LeakCannary的源码分析。


 

你可能感兴趣的:(Java篇)