在 Android 中我们写的 .java 文件,最终会编译成 .class 文件, class 又由类装载器加载后,在 JVM 中会形成一份描述 class 结构的元信息对象,通过该元信息对象可以知道 class 的结构信息 (构造函数、属性、方法)等。JVM 会把描述类的数据从 class 文件加载到内存,Java 有一个很好的管理内存的机制,垃圾回收机制 GC 。为什么 Java 都给我们提供了垃圾回收机制,程序有时还会导致内存泄漏,内存溢出 OOM,甚至导致程序 Crash 。接下来我们就对实际开发中出现的这些内存问题,来进行优化。
线程独占区程序计数器
- 相当于一个执行代码的指示器,用来确认下一行执行的地址
- 每个线程都有一个
- 没有 OOM 的区
虚拟机栈
- 我们平时说的栈就是这块区域
- java 虚拟机规范中定义了 OutOfMemeory , stackoverflow 异常
本地方法栈
- java 虚拟机规范中定义了 OutOfMemory ,stackoverflow 异常
注意
- 在 hotspotVM 中把虚拟机栈和本地方法栈合为了一个栈区
方法区
- ClassLoader 加载类信息
- 常量、静态变量
- 编译后的代码
- 会出现 OOM
- 运行时常量池
- public static final
- 符号引用类、接口全名、方法名
java 堆 (本次需要优化的地方)
- 虚拟机能管理的最大的一块内存 GC 主战场
- 会出现 OOM
- 对象实例
- 数据的内容
随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。
目前虚拟机基本都是采用可达性分析算法
可达性分析算法
GC Roots 的对象作为起始点,向下搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链,即从GC roots 到这个对象不可达,则证明对象不可用,可被回收。
可以作为 GC Roots 的对象
- 虚拟机栈正在运行使用的引用
- 静态属性 常量
- JNI 引用的对象
GC 是需要 2 次扫描才回收对象,所以我们可以使用 finalize 去救活丢失的引用
@Override
protected void finalize() throws Throwable {
super.finalize();
instace = this;
}
不同引用类型的回收状态
问题一:软引用、虚引用和弱引用的区别、
- 软引用 (SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。
- 弱引用 (WeakReference)
弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。
注意这里的 referenceQueuee 是装的被回收的对象。
- 虚引用 (PhantomReference)
@Test public void onPhantomReference()throws InterruptedException{ String str = new String("123456"); ReferenceQueue queue = new ReferenceQueue(); // 创建虚引用,要求必须与一个引用队列关联 PhantomReference pr = new PhantomReference(str, queue); System.out.println("PhantomReference:" + pr.get()); System.out.printf("ReferenceQueue:" + queue.poll()); }
虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
总结
引用类型调用方式GC是否内存泄漏强引用直接调用不回收是软引用.get()视内存情况回收否弱引用.get()回收不可能虚引用null任何时候都可能被回收,相当于没有引用一样否虚引用:该对象在回收前和回收后都无法通过虚引用获取到这个,在该对象被回收掉时会将对象部分信息存储到虚引用回收队列中,仅仅是拿到一个对象被回收掉的通知;
弱引用:在该对象被回收前可以通过弱引用获取到该对象,回收后无法获取到这个对象,在该对象被回收掉时会将对象部分信息存储到弱引用回收队列中,可以拿到一个对象被回收掉的通知;
下面通过程序我们来看下实验结果:
- 虚引用:不对生存造成任何影响,用于跟踪GC的回收通知
一般没有什么用处,使用场景是要做一些系统跟踪工具,例如打log要看看GC什么时候来过,打印这些GC信息时
@Test
public void testPhantomReference() throws InterruptedException {
//虚引用:功能,不会影响到对象的生命周期的,
// 但是能让程序员知道该对象什么时候被 回收了
ReferenceQueue referenceQueue = new ReferenceQueue();
Object phantomObject = new Object();
PhantomReference phantomReference = new PhantomReference(phantomObject, referenceQueue);
phantomObject = null;
System.out.println("GC回收前");
System.out.println("phantomReference:" + phantomReference.get());//虚引用无法获取到对象,返回null
System.out.println("referenceQueue:" + referenceQueue.poll());//引用回收队列中还未回收掉,返回null
System.gc();
Thread.sleep(2000);
System.out.println("GC回收后");
System.out.println("phantomReference:" + phantomReference.get());//虚引用无法获取到对象,返回null
//referenceQueue:最后能够拿到一个被回收掉的对象的信息,而不是原来的对象,原来的对象已经被回收掉了
System.out.println("referenceQueue:" + referenceQueue.poll());
}
将一个对象放到虚引用中,在System.gc()前,通过虚引用无法获取到这个对象,且引用回收队列中没有任何信息;在GC后,引用回收队列中可以拿到回收对象的信息,但注意这个不是原对象,仅是原对象的一些信息:
- 弱引用:
第一次扫到了,就标记下来,第二次扫到直接回收
@Test
public void testWeakReference() throws InterruptedException {
ReferenceQueue referenceQueue = new ReferenceQueue();
Object weakObject = new Object();
//弱引用,可以将这个引用记录到引用队列 referenceQueue 中去
WeakReference weakReference = new WeakReference(weakObject, referenceQueue);
System.out.println("GC回收前");
System.out.println("WeakReference:" + weakReference.get());//弱引用中可以拿到该对象
System.out.println("referenceQueue:" + referenceQueue.poll());//引用回收队列中没有对象的任何信息
weakObject = null;
System.gc();
Thread.sleep(2000);
System.out.println("GC回收后");
//对象被回收掉返回null
System.out.println("WeakReference:" + weakReference.get());
//被回收掉对象的信息,在回收队列中referenceQueue 可以查询到
System.out.println("referenceQueue:" + referenceQueue.poll());
}
GC回收前,通过弱引用我们可以拿到放在其中的对象;GC回收后,无法拿到该对象,且在回收队列中能够获取到被回收对象的一些信息:
问题二:枚举的性能损耗以及注解的解决方案
日常我们使用枚举来定义一些常量的取值,使用枚举能够确保参数的安全性。但是Android开发文档上指出,使用枚举会比使用静态变量多消耗两倍的内存,应该尽量避免在Android中使用枚举,那么枚举为什么会更消耗内存呢?下面一起分析一下。
public enum Sex {
MAN, WOMAN;
}
1、使用javac将其编译成.class文件,命令为:javac Sex.java;
2、用jad反编译.class文件,生成Sex.jad,命令为jad Sex.class,jad的下载地址:http://www.javadecompilers.com/jad
从反编译的代码来看,我们定义的枚举,编译器会将其转换成一个类,这个类继承自java.lang.Enum类,除此之外,编译器还会帮我们生成多个枚举类的实例,赋值给我们定义的枚举类型常量,并且还声明了一个枚举对象的数组,保存了所有的枚举对象。下面我们分别来计算一下采用静态变量和枚举占用内存的大小对比。
a、静态变量占用内存大小为:int占用内存大小为4,加起来占用内存大小为8
public final static int MAN = 0;
public final static int WOMAN = 1;
b、枚举占用内存大小为:216字节。打开Sex.jad文件:
public final class Sex extends Enum {
public static Sex[] values()
{
return (Sex[])$VALUES.clone();
}
public static Sex valueOf(String s)
{
return (Sex)Enum.valueOf(com/liunian/androidbasic/enumtest/Sex, s);
}
private Sex(String s, int i)
{
super(s, i);
}
public static final Sex MAN;
public static final Sex WOMAN;
private static final Sex $VALUES[];
static
{
MAN = new Sex("MAN", 0);
WOMAN = new Sex("WOMAN", 1);
$VALUES = (new Sex[] {
MAN, WOMAN
});
}
}
可以看到,枚举占用内存的大小比静态变量多得多。
枚举类型数据的内存优化:使用注解的方案
例如我们有个SHAPE类,里面有四种形状类型,在此我们定义一个注解:
public class SHAPE {
public static final int RECTANGLE = 0;
public static final int TRIANGLE = 1;
public static final int SQUARE = 2;
public static final int CIRCLE = 3;
// 其中flag为true则使用时可以用|来连接,可以允许取多个值;
// 后面的value是注解使用时的可选值
@IntDef(flag = true, value = {RECTANGLE, TRIANGLE, SQUARE, CIRCLE})
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Model {
}
private @Model
int value = RECTANGLE;
public void setShape(@Model int value) {
this.value = value;
}
@Model
public int getShape() {
return this.value;
}
}
在需要使用注解的地方使用:
SHAPE shape = new SHAPE();
shape.setShape(SHAPE.CIRCLE | SHAPE.RECTANGLE);
问题三:使用对象池避免GC回收将来要重用的对象
内存设计模式对象沲+LRU算法:
设计的对象池基类:
import android.util.SparseArray;
public abstract class ObjectPool {
//空闲沲,用户从这个里面拿对象
private SparseArray freePool;
//正在使用沲,用户正在使用的对象放在这个沲记录
private SparseArray lentPool;
//沲的最大值
private int maxCapacity;
public ObjectPool(int initialCapacity, int maxCapacity) {
//初始化对象沲
initalize(initialCapacity);
this.maxCapacity = maxCapacity;
}
private void initalize(int initialCapacity) {
lentPool = new SparseArray();
freePool = new SparseArray();
for (int i = 0; i < initialCapacity; i++) {
freePool.put(i, create());
}
}
/**
* 申请对象
*
* @return
*/
public T acquire() throws Exception {
T t = null;
synchronized (freePool) {
int freeSize = freePool.size();
for (int i = 0; i < freeSize; i++) {
int key = freePool.keyAt(i);
t = freePool.get(key);
if (t != null) {
this.lentPool.put(key, t);
this.freePool.remove(key);
return t;
}
}
//如果没对象可取了
if (t == null && lentPool.size() + freeSize < maxCapacity) {
//这里可以自己处理,超过大小
if (lentPool.size() + freeSize == maxCapacity) {
throw new Exception();
}
t = create();
lentPool.put(lentPool.size() + freeSize, t);
}
}
return t;
}
/**
* 回收对象
*
* @return
*/
public void release(T t) {
if (t == null) {
return;
}
int key = lentPool.indexOfValue(t);
//释放前可以把这个对象交给用户处理
restore(t);
this.freePool.put(key, t);
this.lentPool.remove(key);
}
protected void restore(T t) {
}
protected abstract T create();
public ObjectPool(int maxCapacity) {
this(maxCapacity / 2, maxCapacity);
}
}
然后我们自己MyObjectPool去继承基类即可使用:
public class MyObjectPool extends ObjectPool{
public MyObjectPool(int initialCapacity, int maxCapacity) {
super(initialCapacity, maxCapacity);
}
public MyObjectPool(int maxCapacity) {
super(maxCapacity);
}
@Override
protected Object create() {//LRU
return new Object();
}
}
使用:
MyObjectPool pool = new MyObjectPool(2, 4);
Object o1 = pool.acquire();
Object o2 = pool.acquire();
Object o3 = pool.acquire();
Object o4 = pool.acquire();
Object o5 = pool.acquire();