了解Android 垃圾回收
GC频繁原因
Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,就会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。
对象复用
如果我们发现了大量临时对象的创建该如何处理呢?
首先确定问题发生的原因,对象频繁创建一般有以下几种可能:
- 在循环操作中创建对象
- 在频繁被调用的方法中创建对象
例如:在onDraw中使用一些对象,由于onDraw方法会被程序频繁调用,所以我们不能在onDraw方法里面创建对象实例,我们可以考虑在onDraw方法外提前初始化这些对象。
能直接避免对象的频繁创建当然最好,但是有时候这些对象的初始化是不可避免的,那么我们就要考虑对象的复用,采用对象池来解决问题。
对象池工作过程
- Pool 表示的是对象池,用于存储可重复利用的对象
- 第二步操作就是取出对象。
- 第三步操作是使用取出的对象来完成一些任务。这时候并不需要对象池再做什么,但是这也意味着该对象将被租借一段时间并且不能在被其他组件借出。
- 第四步操作就是归还,组件归还借出的对象这样可以继续满足其他的租借请求。
在一个多线程的应用中,第二,第三,第四步操作都有可能发生并发操作。多线程的组件中分享对象导致了潜在的并发问题。也存在一种情况就是当所有对象都被借出时不能满足接下来的请求,对象池必须应对这些请求,不管是告诉组件已经没有对象可借还是允许组件等待直到有归还的对象。
Android官方对象池源码解析(见注释说明)
public final class Pools {
public interface Pool<T> {
/**
* @return 从对象池取出对象
*/
@Nullable
T acquire();
/**
* 释放对象并放入对象池
*
* @return true表示释放的对象成功放入对象池
*
* @throws IllegalStateException 如果对象已经存在于对象池中抛异常
*/
boolean release(@NonNull T instance);
}
private Pools() {
/* do nothing - hiding constructor */
}
/**
* 对象池的非同步实现
*/
public static class SimplePool<T> implements Pool<T> {
private final Object[] mPool; //对象池中真正用于存储对象的数组
private int mPoolSize; //对象池内的对象个数
/**
* Creates a new instance.
*
* @param maxPoolSize 对象池最大容量
*/
public SimplePool(int maxPoolSize) {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("The max pool size must be > 0");
}
mPool = new Object[maxPoolSize]; //初始化对象数组
}
//从mPool数组中取出mPoolSize - 1位置上的对象
@Override
@SuppressWarnings("unchecked")
public T acquire() {
if (mPoolSize > 0) {
final int lastPooledIndex = mPoolSize - 1;
T instance = (T) mPool[lastPooledIndex];
mPool[lastPooledIndex] = null;
mPoolSize--;
return instance;
}
return null;
}
//回收的对象放入mPool数组的mPoolSize 位置上
@Override
public boolean release(@NonNull T instance) {
if (isInPool(instance)) {
throw new IllegalStateException("Already in the pool!");
}
if (mPoolSize < mPool.length) {
mPool[mPoolSize] = instance;
mPoolSize++;
return true;
}
return false;
}
//判断对象是否已存在于对象池中
private boolean isInPool(@NonNull T instance) {
for (int i = 0; i < mPoolSize; i++) {
if (mPool[i] == instance) {
return true;
}
}
return false;
}
}
/**
* 对象池的同步实现
*
* @param <T> The pooled type.
*/
public static class SynchronizedPool<T> extends SimplePool<T> {
private final Object mLock = new Object(); //用于同步加锁的对象
/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public SynchronizedPool(int maxPoolSize) {
super(maxPoolSize);
}
@Override
public T acquire() {
synchronized (mLock) {
return super.acquire();
}
}
@Override
public boolean release(@NonNull T element) {
synchronized (mLock) {
return super.release(element);
}
}
}
}
优点
- 复用对象池中的对象,可以避免频繁创建和销毁堆中的对, 进而减少垃圾收集器的负担, 减少内存抖动;
- 不必重复初始化对象状态, 对于比较耗时的constructor和finalize来说非常合适;
缺点
- Java的对象分配操作不比c语言的malloc调用慢, 对于轻中量级的对象, 分配/释放对象的开销可以忽略不计;
- 并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;
- 由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;
- 很难正确的设定对象池的大小, 如果太小则起不到作用, 如果过大, 则占用内存资源高, 可以开启一个线程定期扫描分析, 将对象池压缩到一个合适的尺寸以节约内存,但为了获得不错的分析结果, 在扫描期间可能需要暂停复用以避免干扰(造成效率低下), 或者使用非常复杂的算法策略(增加维护难度);
- 设计和使用对象池容易出错, 设计上需要注意状态同步, 这是个难点, 使用上可能存在忘记归还(就像c语言编程忘记free一样), 重复归还(可能需要做个循环判断一下是否池中存在此对象, 这也是个开销), 归还后仍旧使用对象(可能造成多个线程并发使用一个对象的情况)等问题。
对象池的使用
在源码中摘取出来对象池的使用帮助类:
/**
* Helper class for creating pools of objects. An example use looks like this:
*
* public class MyPooledClass {
*
* private static final SynchronizedPool<MyPooledClass> sPool =
* new SynchronizedPool<MyPooledClass>(10);
*
* public static MyPooledClass obtain() {
* MyPooledClass instance = sPool.acquire();
* return (instance != null) ? instance : new MyPooledClass();
* }
*
* public void recycle() {
* // Clear state if needed.
* sPool.release(this);
* }
*
* }
* /