猛男噩梦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的监测,监测到具体的问题之后再进行具体的分析。
最后也许经历过才会懂得吧,毕竟人都是被打之后才长记性的生物。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容