Java的垃圾回收(GC)是虚拟机自动管理的,前面我有篇文章专门讲GC:《JVM篇 - GC给你整明白》
Java内存管理分为内存分配和内存回收,都不需要程序员负责,垃圾回收的机制主要是看对象是否有引用指向该对象。
Java对象的引用包括:强引用,软引用,弱引用,虚引用,Java中提供这四种引用类型主要有两个目的:
目录:
1. 强引用(StrongReference)
强引用是指创建一个对象并它赋值给一个引用,引用是存在JVM中的栈中的。
例如:
private static void testStrongReference() {
Object object = new Object();
Object[] objects = new Object[1000];
}
可以看到,testStrongReference()方法中,创建了一个对象和一个对象数组,当testStrongReference()执行完,JVM会自动回收它们(属于局部变量)。但是因为它们都是强引用,所以当执行到testStrongRefenerce()时,如果内存不足,不足够在堆中分配这些内存,那么JVM将抛出OutOfMemory,因为强引用的对象,只要有引用变量指向它们的时候,它们将不会被垃圾回收。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
看看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使用的就是软引用。
如果一个对象具有软引用,只要内存空间足够,垃圾回收器就不会回收它。如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。一旦垃圾线程回收该Java对象之 后,get()方法将返回null。
分享一个平时开发中的例子,就是上面说的图片缓存。
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)
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。其实,上面图片缓存的框架用弱引用也可以做,特别是在移动设备上,内存容量有限。
还是以平时开发中遇到的问题为例:
(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
注意:PhantomReference的get()结果必定为null。
5. 引用队列(ReferenceQueue)
上面在虚引用中说到了引用队列,再举个例子,作为一个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 super T> 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 super T> q) {
super(referent, q);
}
}
public class PhantomReference extends Reference {
// get()只会返回null
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue super T> q) {
super(referent, q);
}
}
可以看到,几种引用都提供了传入引用队列的构造器。
public abstract class Reference {
private T referent;
volatile ReferenceQueue super T> 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 super Object> 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 super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
大致解释下:
Reference主要是负责内存的一个状态,当然它还和java虚拟机,垃圾回收器打交道。Reference类首先把内存分为4种状态Active,Pending,Enqueued,Inactive。
几个变量:
当 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的源码分析。