JVM 内存泄漏、OOM原理分析简易教程

先挖个坑放着,暂时没时间写。

前言

Android系统在内存管理上有一个Generational Heap Memory模型,内存回收的大部分压力不需要应用层关心,Generational Heap Memory有一套自己管理的机制,当内存达到某一个阈值时,系统会根据不同的规则自动释放系统认为可以释放的内存,也正是因为Android程序把内存控制的权力交给了Generational Heap Memory,一旦出现内存泄漏(Memory Leak)和溢出(OOM)方面的问题,排查错误将会成为一项异常艰难的工作。

内存泄漏

内存泄漏是因为堆内存中的长生命周期的对象持有短生命周期对象的引用造成的。
——内存泄漏与排查流程——安卓性能优化

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于疏忽或者是错误造成程序未能释放已经不再使用的内存的情况。

内存泄漏指的是由应用程序分配某段内存后,由于设计的错误,失去了对该内存的控制,因此造成了内存的浪费。

一般来说,内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式的释放内存。
在这里插入图片描述

图片来源于——内存泄漏与排查流程——安卓性能优化

运行数据区域说明
程序计数器让程序中各个线程知道自己接下来需要执行哪一行指令。
虚拟机栈存储栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。包含了StackOverFlowErrorOutOfMemoryError
方法区存储已加载的类信息(由ClassLoader加载)、常量、静态变量、编译后的字节码等信息。
存储几乎所有的对象实例和数组数据。包含了OutOfMemoryError
运行时常量池属于“方法区”的一部分,用于存放编译器生成的各种字面量和符号引用。
字面量包含了文本符号串、final类型的常量值等。
符号引用指的是类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

Java对象的生命周期

在这里插入图片描述

生命周期说明
创建阶段CreatedJava对象在创建的时候会经历一下的步骤:

1.为对象分配存储空间
2.构建对象
3.从超类到子类对static成员进行初始化,类的static成员的初始化在ClassLoader加载该类时进行。
4.超类成员变量按顺序初始化,递归调用超类的构造方法。
5.子类成员变量按顺序初始化,一旦对象被创建,子类构造方法就调用该对象并为某些变量赋值,完成后这个对象的阶段就切换。
应用阶段InUse对象至少被一个强引用(Strong Reference)持有,除非在系统中显式地使用了软引用(Soft Reference)、弱引用(Week Reference)或者虚引用(Phantom Reference)。

软引用可以加速虚拟机对垃圾内存的回收速度,更可靠地维护系统的运行安全,防止**内存溢出(Out Of Memory)**等问题产生。
不可见阶段Invisible处于不可见阶段的对象在虚拟机的对象引用根集合中再也找不到直接或者间接的强引用,这些对象一般是所有线程栈中的临时变量。

当一个对象处于不可见状态的时候,说明程序本身不在持有该对象的任何强引用,虽然该对象仍然是存在的。
然而,该对象仍可能被虚拟机下的某些已经装载的静态变量线程或者JNI等强引用持有,这些特殊的强引用称为“GC Root”。存在这些GC Root会导致对象的内存泄漏(Memory Leak)。存在这些GC Root会导致对象的内存泄漏,无法被回收。
不可达阶段Unreachable对象处于不可达阶段是指对象不再被任何强引用持有,回收器发现该对象已经不可达。
收集阶段Collected当垃圾收集器发现该对象已经处于“不可达”阶段并且垃圾回收器已经对该对象的内存空间重新分配做好准备的时候,对象进入“收集阶段”。如果该对象已经重写了finalize()方法,则执行该方法的操作。
终结阶段Finalized当对象执行完finalize()方法后仍然处于不可达状态的时候,该对象进入终结阶段。在该阶段,等待垃圾回收器回收该对象空间。
对象空间重新分配阶段Deallocated若垃圾回收器对该对象占用的内存空间进行回收或者再分配,则该对象彻底消失,这个阶段称为“对象空间重新分配阶段”。

Java的四种引用

Android 面试集锦

  1. 强引用是使用最普遍的引用:Object o=new Object(); 特点:不会被GC;
  2. 软引用SoftReference用来描述一些还有用但是并非必须的对象。软引用SoftReference用来描述一些还有用但是并非必须的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存,比如网页缓存、图片缓存等。
  3. 弱引用WeakReference与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  4. 虚引用PhantomReference也称为幻影引用:一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,Java中用PhantomReference来实现虚引用。
引用类型GC回收时间用途生存时间
强引用never对象的一般状态JVM停止运行时
软引用内存不足的时候对象缓存内存不足时终止
弱应用GC时对象缓存GC后终止
虚引用unknownunknownunknown

更多细致的内容请见:

软引用

软引用用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生抛出OutOfMemoryError异常之前,将会把这些对象列入回收范围之内进行第二次回收。如果这次回收还没有足够的内存,才会抛出OutOfMemoryError异常。在JDK1.2之后,提供了SoftReference类来实现软引用。

/**
 * JVM只有在出现OutOfMemoryError之前才会进行回收。
 */
private static void TestSoftRefDemo() {
    List<String> myList = new ArrayList<>();
    SoftReference<List<String>> refObj = new SoftReference<>(myList);
    // 正确的使用,使用强引用指向对象保证获得对象之后不会被回收
    List<String> list = refObj.get();
    if (null != list) {
        list.add("hello");
        System.out.println("myList 还没有被回收!");
    } else {
        // 整个列表已经被垃圾回收了,做其他处理
        System.out.println("myList 已经被回收了!");
    }
}

弱引用

弱引用也是用来描述非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

/**
 * 测试虚引用的例子
 */
private static void TestWeakRefDemo() {
    ReferenceQueue<String> refQueue = new ReferenceQueue<>();
    // 用于检查引用队列中的引用值被回收
    Thread checkRefQueueThread = new Thread(() -> {
        while (true) {
            Reference<? extends String> clearRef = refQueue.poll();
            if (null != clearRef) {
                System.out.println("引用对象被回收, ref = " + clearRef
                        + ", value = " + clearRef.get());
            }
        }
    });
    checkRefQueueThread.start();
    // 需要注意的是,虚引用的第一个参数不能是"value1",而应该是new String("value1")
    // 传入"value1",表示在常量池中创建一个"value1"。这个没办法被回收。
    // 传入"new String("value1")",表示在堆中创建一个"value1"。这个可以被回收。
    WeakReference<String> weakRef1 = new WeakReference<>(new String("value1"), refQueue);
    WeakReference<String> weakRef2 = new WeakReference<>(new String("value2"), refQueue);
    WeakReference<String> weakRef3 = new WeakReference<>(new String("value3"), refQueue);
    System.out.println("ref1 value = " + weakRef1.get());
    System.out.println("ref2 value = " + weakRef2.get());
    System.out.println("ref3 value = " + weakRef3.get());
    System.out.println("开始通知JVM的gc进行垃圾回收");
    // 通知JVM的gc进行垃圾回收
    System.gc();
}

虚引用

当(垃圾回收线程)准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。

如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了,所以要找出所有被回收的对象,只需要遍历引用队列。

/**
 * 使用虚引用来判断是否需要进行资源回收,比finalize()方法更加可靠和灵活
 * <p>
 * PS:JVM并不能保证finalize()一定执行。
 */
private static void TestPhantomRefDemo() {
    Object obj = new Object();
    ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
    PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);
    // null
    System.out.println("phantomRef.get()->" + phantomRef.get());
    // null
    System.out.println("refQueue.poll()->" + refQueue.poll());
    // 置空对象,方便后续GC线程回收
    obj = null;
    // 通知JVM的gc进行垃圾回收
    System.out.println("开始进行GC...");
    System.gc();
    System.out.println("GC完毕!");
    // null, 调用phantomRef.get()不管在什么情况下会一直返回null
    System.out.println("phantomRef.get()->" + phantomRef.get());
    
    // 当GC发现了虚引用,GC会将phantomRef插入进我们之前创建时传入的refQueue队列
    // 注意,此时phantomRef对象,并没有被GC回收。
    // 在我们显式地调用refQueue.poll返回phantomRef之后,GC线程第二次发现虚引用,
    // 而此时JVM将phantomRef插入到refQueue会插入失败,此时GC才会对phantomRef对象进行回收。
    try {
        Thread.sleep(200);
    } catch (InterruptedException ignore) {
    }
    Reference<?> pollObj = refQueue.poll();
    // java.lang.ref.PhantomReference@2f7c7260
    System.out.println("refQueue.poll()->" + pollObj);
    if (null != pollObj) {
        // 进行资源回收的操作
        System.out.println("发现有对象被回收了,开始执行资源回收的操作!");
    }
}

垃圾回收

垃圾回收的过程请见——JVM 垃圾收集器

内存泄漏是因为堆内存中的长生命周期的对象持有短生命周期对象的引用造成的。
——内存泄漏与排查流程——安卓性能优化

内存泄漏

请见——Android 性能优化工具集合内存检测工具集部分章节。

内存泄露可能发生的场景

区域是否线程私有是否会发生OOM
程序计数器
虚拟机栈
本地方法栈
方法区
直接内存

常见内存泄漏原因及解决方案

这里我就不单独写了,其实就是这篇文章的第6和第7章节的内容——内存泄漏与排查流程——安卓性能优化

这里我简单总结下几种处理方案:

情况处理方案
单例、集合持有短生命周期的实例对象的强引用由于强引用的存在,使得短生命周期的实例对象在JVM中的引用计数不为0,不能被GC掉。这个时候应该将强引用改为软引用
内部非静态类内部非静态类默认持有外部类的强引用,可以另起一个文件单独写,也可以使用内部静态类来处理。
HandleHandle可能会持有一个Activity的“强引用”,可以将强引用改为“软引用”通过构造函数的形式传递进来。
资源未关闭对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

常见OOM原因及解决方案

情况处理方案
单例、集合持有短生命周期的实例对象的强引用由于强引用的存在,使得短生命周期的实例对象在JVM中的引用计数不为0,不能被GC掉,导致程序占用的内存越来越大,超过了阈值,就会出现OOM。这个时候应该将强引用改为软引用
RecyclerView构造Adapter时,没有使用缓存的convertView,每次都在创建新的converView。这里推荐使用ViewHolder。
BitmapBitmap没调用recycle()方法,对于Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才它设置为null。因为加载Bitmap对象的内存空间,一部分是java的,一部分C的(因为Bitmap分配的底层是通过JNI调用的),而这个recyle()就是针对C部分的内存释放。

后果

内存泄漏会因为减少可用内存的数量,从而降低计算机的性能。在最糟糕的情况下,过多的可用内存被分配掉导致全部或者部分设备停止正常工作,或者应用程序崩溃。

附录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值