猛男噩梦OOM

作为一个双手起茧的猛男,当你碰到OutOfMemoryError的时候,你可能会爽一天。主要最近没什么可写的,所以觉得可以围绕着这个写篇文章,这个一直以来猛男的噩梦。

一. 一些OOM的场景

1. java.lang.StackOverflowError

当你发现 java.lang.StackOverflowError stack size 8MB 这样的错误的时候,你就基本能很快的推测是栈溢出导致的错误,从经验上来判断,这基本是因为递归导致的。
这时你需要根据log找到指向错误的地方,然后细心的去查到是不是哪里做了递归的操作。
我当时出现是这个错误,是指向一个获取某个参数的地方,但是我检查代码其实根本就没用用到递归。认真去查找之后才发现原来是调用我这个方法的人,因为拿不到参数,他就在接口的回调中再调用了一次这个方法,一直拿不到就一直调,所以形成了递归。
但是总的来说,这个溢出的问题还是能比较方便的找出错误的地方。虽然它报的不是OutOfMemoryError而是StackOverflowError,但我觉得有必要归类到这里一起讲。

2. Failed to allocate a 651239454 byte allocation with 12582912 free bytes and 253MB until OOM

当你发现java.lang.OutOfMemoryError Failed to allocate a 651239454 byte allocation with 12582912 free bytes and 253MB until OOM的时候,恭喜你,你今天可能不会拥有一个好心情了。
这就是真的内存溢出了,如果你是一个没遭受过OOM毒打的人,那即便有错误堆栈给你,你也无法很快去找出问题的原因,如果你是被OOM毒打过很多次的猛男,那你可以凭借经验很快的找出问题的原因,无非基本就是图片过大、一次性把大文件加载到内存中、做了什么循环导致内存溢出。
当然这是属于运气好的情况,运气不好的话恭喜你今天可能会发现新世界,我相信只有不断发现新世界,不对被新的OOM毒打,代码才能写得更好。
但如果出现内存溢出,我的第一反应是有没有加载大的文件、资源到内存中。我记得我遇到一次这个错误,当时我检查代码,确实没什么特殊操作会导致OOM,检查了好几遍,我甚至不知道为什么错误堆栈会指向那个地方,最后我发现原来是我请求的动态链接出了问题,那个动态链接是其它人员配置的,只是为了做个get请求获取一个状态,结果那位老铁粗心把那条链接配错成一个下载apk的连接,一个200多M的东西,我直接在代码中response.body().string(),那能不炸掉吗?
当然内存溢出也可能会出现这个问题,但是内存溢出就不是这么好去找问题了,这个放在最后再说。

3. pthread_create (1040KB stack)

当你发现java.lang.OutOfMemoryError pthread_create,恭喜你,你可能爽完今天还不够,明天接着爽。
出现这个问题一般是因为线程创建太多,没回收导致的。那一般来说是两种情况,你的项目有大量使用创建线程,但是没做回收,这种情况简单,如果你符合我说的这种情况,你可以先别管错误在什么地方,你先重构你的代码使用线程池,干就完了。第二种情况是线程池使用不当导致的,这就有些难找,比如这样的错误堆栈##

java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:745)
java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)
java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1359)

这就很明显的是指向线程池了,那就具体得去查到问题的原因了,我之前写过一篇文章okhttp引起OOM,出现的错误就是这种情况,那个问题是因为OkHttpClient在每次请求都创建,看源码之后发现,他内部每Build一个OkHttpClient就会创建一个线程池,当创建多个线程池的时候就会报这个错,后面改成了OkHttpClient使用单例嘛。但是这种问题还是比较不方便去查找的,像这个它不会在错误堆栈告诉你是因为线程池创建多个导致的,只能靠自己去细心查找问题原因。

4. 其它情况

上面是我觉得比较常发生的情况,当然也会有其它的OOM,像网上很多人碰到的Bitmap导致的OOM,其实这也算是第二种情况,一次性加载大量字节到内存中。如果有其它的也欢迎补充,毕竟我也希望在被下一次毒打之前能先熟悉熟悉,至少到那时就不会这么痛了。

二. 内存泄漏

1. 让人头疼的内存泄露

内存泄露也是导致内存溢出的原因,但不能一出现内存溢出就甩锅给内存泄露导致的。首先应该考虑经常溢出的情况,其次考虑到你的应用是不是一个重量级的,很多对象需要用时alive,这时候你需要的是扩大内存,最后再考虑是不是内存溢出导致的。
内存溢出也分很多情况,比如说整个应用从开启到关闭的过程中,才会调用的一次代码,那里发生了很小的内存泄露,其实这也基本也影响不大。
比如,你是不是某个地方写了大量的循环,在循环内部有内存泄露发行,那这种情况就需要重视了。
或者说你的某个功能会需要频繁的调用,虽然说一次泄露影响不大,但是频繁的调用每次都泄露,久而久之,也会溢出。这种情况我倒是有个建议,当然不一定管用,比如说你有做BUG的监测,自己做的监测或者用的第三方,有提示你出现OOM,然后当分析错误的时候,发现应用运行的时间都很长,十几二十个小时以上,那有就可能是某个频繁调用的功能出现了内存泄露,某个或者多个。
越庞大的项目,逻辑越复杂的项目,越容易出现内存泄露,越难找出问题。

2. 发现内存泄露

那么怎么发现代码中有内存泄露呢?
(1)LeakCanary
一般大众都是使用这个,接入方便,效果拔群。
https://square.github.io/leakcanary/getting_started/
(2)AndroidStudio Profiler
用AndroidStudio自带的Profiler也能监测应用运行中内存的情况,但是这个工具存在一定的局限性。

3. 常见的内存泄露

开发时一些比较需要注意的地方,发生内存泄露大多数情况是因为生命周期长的对象引用生命周期短的对象,导致生命周期短的对象内存无法释放。

(1)静态对象引用非静态对象
常见的就是单例模式,写单例一时爽,一直写一直爽,但是需要注意,比如在单例中定义全局变量Context,当这个activity关闭掉的时候,因为被这个单例所引用,所以无法释放,这个activity无法释放,那么他内部引用的对象也无法被释放,这确实是个比较严重的问题。所以在写单例的时候一定要注意释放持有的引用,当然你无需太担心,因为出现这种情况的话AndroidStudio会给你警告的提示。

(2)内部类的使用
内部类的或者匿名对象的使用需要注意,内部类会持有外部类的引用,举个有意思的栗子

public class Test {

    public void test(){
//        Thread thread = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                try {
//                    Thread.sleep(30000);
//                }catch (Exception e){
//                    e.printStackTrace();
//                }
//            }
//        });

        Thread thread = new Thread(()->{
            try {
                    Thread.sleep(30000);
                }catch (Exception e){
                    e.printStackTrace();
                }
        });

        thread.start();
    }

}

先看屏蔽的代码,当外部调用时

        Test test = new Test();
        test.test();

会发生内存泄露



有意思的地方就在下边没注释的使用lambda表达式的情况,这种情况不会发生泄露(我也是无意间看到别人说的,自己就试了一下)

(3)资源使用之后没有释放
比如说使用IO流,使用之后要close掉。handler、thread这些,都需要注意。使用别人的第三方框架,要按API调用释放资源的方法,如果有的话。

三. 防范内存溢出

首先写代码的时候,我们可以尽量细心点,防止内存泄露。
当然防是不可能防住的,我觉得啊,可能大厂的或是怎样的经过长时间的迭代真能防止吧,当逻辑变复杂的时候,内存溢出就可能会发生。有可能测试测不出,这时候就需要做BUG的监测,监测到具体的问题之后再进行具体的分析。
最后也许经历过才会懂得吧,毕竟人都是被打之后才长记性的生物。

推荐阅读更多精彩内容