前言

1.问题及现象

线上日志反馈内存溢出问题。根据用户反馈,客户操作一段时间之后,APP 内存溢出崩溃。

2.分析过程

(1) 分析线上日志,发现主要分两种:

第一种如下,可能是某个死循环导致内存不停增大导致:

java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying to throw OutOfMemoryError; no stack trace available

第二种如下,压死骆驼的最后一根稻草:

java.lang.OutOfMemoryError: Failed to allocate a 16 byte allocation with 0 free bytes and 3GB until OOM, max allowed footprint 536872136, growth limit 536870912

这两种情况下都无法定位问题所在。

通过观察发生的版本信息,是从V9.5.6开始的,于是就开始查那期改动的功能(主要是webview优化,其他的是SDK更新),由于没有线上出问题的机型,就找个类似的华为机器操作webview,发现内存增长并不快,操作很长时间内存变化不大。

(2) 分析用户操作路径

让工程导出出现OOM问题用户最近的操作记录,埋点,页面停留,崩溃日志,观察进入了哪些页面,但是照着操作路径操作,也没发现崩溃。

(3) 统计出现问题的机型,前五都是华为的,系统版本7.0以上,内存3GB以上。

(4) 使用LeakCanary + Android Profiler排查泄漏的地方,查看线上日志的时候,偶然发现测试有一部手机出现了问题,华为P8,然后使用工具根据用户操作路径查各个模块泄漏问题,发现委托登录有个持续的10M泄漏,比较大的泄漏还有开屏广告页和行情登录页面34M。这个手机给APP分配的可用内存最大为256M,操作几次登录页面就崩溃了。

开屏页:

  1. private ScheduledFuture<?> mTaskFuture = null;
  2. private void gotoMain() {
  3. ...
  4. AdCountDownTask adCountDownTask = new AdCountDownTask();
  5. HexinThreadPool.cancelTaskFutre(mTaskFuture, true);
  6. mTaskFuture = HexinThreadPool.getThreadPool().sheduleWithFixedDelay(xxx);
  7. handler.sendEmptyMessageDelayed(xxx, xxx);
  8. }
  9. protected void onDestory() {
  10. ...
  11. if (mTaskFuture != null) {
  12. HexinThreadPool.cancelTaskFutre(mTaskFuture, true);
  13. mTaskFuture = null;
  14. }
  15. }
  16. 复制代码

委托登录

  1. public class AdsCT {
  2. private Runnable runable = new Runnable {
  3. // 轮播图片
  4. handler.postDelay(runnable, time);
  5. }
  6. public void onRemove() {
  7. handler.removeCallbacksAndMessages(null);
  8. }
  9. }
  10. public class WeituoLogin {
  11. public void onRemove() {
  12. adsCt.onRemove();
  13. }
  14. }
  15. 复制代码

(5) 修改查出来比较大的内存泄漏,券商给之前出问题的两个用户单独安装,发现还是存在,券商反馈有个用户安装9.4.5的一直都没问题,升级到最新之后就出现问题了,现在怀疑还是9.4.6哪个模块导致的。

一:OOM是什么?

全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。

二:OOM是哪里报错的?

Java内存模型

JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;

方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

三:为什么会发生OOM?

概念

内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。

内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

GC机制

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

申请一块内存时,如果当前可用内存不足,则会出发一次GC,然后再次申请,此时如果内存还不足,则会抛出OOM。

四:OOM常见实例和解决方案

1.内部类持有外部引用-Handler,Context

  1. // 错误示例,内部类持有外部对象导致泄漏
  2. private class MyHandler extends Handler {
  3. @Override
  4. public void handleMessage(Message msg) {
  5. super.handleMessage(msg);
  6. }
  7. }
  8. // 修改后,在activity onDestory()的时候,调用myHandler.removeCallbacksAndMessages(null);
  9. private static class MyHandler extends Handler {
  10. private WeakReference<MainActivity> mainActivityWeakReference;
  11. public MyHandler(MainActivity mainActivity) {
  12. mainActivityWeakReference = new WeakReference<MainActivity>(mainActivity);
  13. }
  14. @Override
  15. public void handleMessage(Message msg) {
  16. MainActivity mainActivity = mainActivityWeakReference.get();
  17. if (mainActivity == null) {
  18. return;
  19. }
  20. mainActivity.jump(null);
  21. super.handleMessage(msg);
  22. }
  23. }
  24. // context被线程持有
  25. private void registerException(final Context context) {
  26. Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
  27. @Override
  28. public void uncaughtException(Thread t, Throwable e) {
  29. String appName = context.getResources().getString(R.string.app_name);
  30. }
  31. };
  32. }
  33. 复制代码

jvm四种引用

strong soft weak phantom(其实还有一种FinalReference,这个由jvm自己使用,外部无法调用到),主要的区别体现在gc上的处理,如下:

Strong类型,也就是正常使用的类型,不需要显示定义,只要没有任何引用就可以回收

SoftReference类型,如果一个对象只剩下一个soft引用,在jvm内存不足的时候会将这个对象进行回收

WeakReference类型,如果对象只剩下一个weak引用,那gc的时候就会回收。和SoftReference都可以用来实现cache

PhantomReference类型,

2.资源没有关闭-数据库,流

  1. private void testStream() {
  2. InputStream in = null;
  3. try {
  4. in = new FileInputStream("xxx.xml");
  5. } catch (FileNotFoundException e) {
  6. e.printStackTrace();
  7. } finally {
  8. if (in != null) {
  9. try {
  10. in.close();
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. in = null;
  15. }
  16. }
  17. }
  18. 复制代码

3.静态变量持有对象

单例持有Context,监听器等

五:OOM检查工具-LeakCanary原理

1.监听对象

Activity的监听,是在Activity onDestory()执行之后再看是否被回收,在Application创建的时候,注册了监听

  1. Application.ActivityLifecycleCallbacks lifecycleCallbacks = new..{
  2. ...
  3. @Override public void onActivityDestroyed(Activity activity) {
  4. ActivityRefWatcher.this.onActivityDestroyed(activity);
  5. }
  6. }
  7. 复制代码

工程中查看Page是否被释放,可以在onRemove()的时候使用

  1. RefWatcher watch(Object obj)
  2. 复制代码

2.如何判断对象是否被释放?

使用的WeakReference+ReferenceQueue实现

  1. final KeyedWeakReference reference =
  2. new KeyedWeakReference(watchedReference, key, referenceName, queue);
  3. 复制代码

如果软引用或弱引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用或弱引用加入到与之关联的引用队列中,由此可判断对象是否被回收。

3.怎么让系统回收对象?

  1. @Override public void runGc() {
  2. Runtime.getRuntime().gc();
  3. enqueueReferences();
  4. System.runFinalization(); //强制调用已经失去引用的对象的finalize方法,确保释放实例占用的全部资源。
  5. }
  6. private void enqueueReferences() {
  7. try {
  8. Thread.sleep(100);
  9. } catch (InterruptedException e) {
  10. throw new AssertionError();
  11. }
  12. }
  13. };
  14. 复制代码

4.如何分析对象泄漏?

(1) 对内存文件进行分析,由于这个过程比较耗时,因此最终会把这个工作交给运行在另外一个进程中的HeapAnalyzerService来执行。

(2) 利用HAHA将之前dump出来的内存文件解析成Snapshot对象,解析得到的Snapshot对象直观上和我们使用MAT进行内存分析时候罗列出内存中各个对象的结构很相似,它通过对象之间的引用链关系构成了一棵树,我们可以在这个树种查询到各个对象的信息,包括它的Class对象信息、内存地址、持有的引用及被持有的引用关系等。

(3) 为了能够准确找到被泄漏对象,LeakCanary通过被泄漏对象的弱引用来在Snapshot中定位它。因为,如果一个对象被泄漏,一定也可以在内存中找到这个对象的弱引用,再通过弱引用对象的reference就可以直接定位被泄漏对象。

(4) 在Snapshot中找到一条有效的到被泄漏对象之间的引用路径,从被泄露的对象开始,采用的方法是类似于广度优先的搜索策略,将持有它引用的节点(父节点),加入到一个FIFO队列中,一层一层向上寻找,哪条路径最先到达GCRoot就表示它应该是一条最短路径。

(5) 将之前查找的最短路径转换成最后需要显示的LeakTrace对象,这个对象中包括了一个由路径上各个节点LeakTraceElement组成的链表,代表了检查到的最短泄漏路径。最后一个步骤就是将这些结果封装成AnalysisResult对象然后交给DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便之后能够直接看到最近几次内存泄露的分析结果,同时以notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。

六:问题

1.OOM可以被捕获吗?

某些情况下是可以的,比如说局部申请一个很大的内存,如果造成了OOM,在catch的地方释放掉,程序还是可以继续往下执行的,但是如果捕获之后释放不掉,也还是会崩溃。

2.内部类为什么能访问外部类成员?

编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?

  1. public com.xxx.Outter$Inner(com.cxh.test2.Outter);
  2. 复制代码

我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

3.为什么局部内部类和匿名内部类只能访问局部final变量?

外部类和内部类编译后会生成两个.class文件,如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
拷贝的话,如果一个值改变了,就会造成数据不一致性,为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

4.静态变量会被回收吗?finalize用法?

  1. private static StaticObj staticObj = new StaticObj();
  2. static class StaticObj {
  3. @Override
  4. protected void finalize() throws Throwable {
  5. staticObj = new StaticObj();
  6. super.finalize();
  7. }
  8. }
  9. 复制代码

5.System.gc()和Runtime.getRuntime().gc()区别?

  1. // System.gc()
  2. /**
  3. * If we just ran finalization, we might want to do a GC to free the finalized objects.
  4. * This lets us do gc/runFinlization/gc sequences but prevents back to back System.gc().
  5. */
  6. private static boolean justRanFinalization;
  7. public static void gc() {
  8. boolean shouldRunGC;
  9. synchronized (LOCK) {
  10. shouldRunGC = justRanFinalization;
  11. if (shouldRunGC) {
  12. justRanFinalization = false;
  13. } else {
  14. runGC = true;
  15. }
  16. }
  17. if (shouldRunGC) {
  18. Runtime.getRuntime().gc();
  19. }
  20. }
  21. public static void runFinalization() {
  22. boolean shouldRunGC;
  23. synchronized (LOCK) {
  24. shouldRunGC = runGC;
  25. runGC = false;
  26. }
  27. if (shouldRunGC) {
  28. Runtime.getRuntime().gc();
  29. }
  30. Runtime.getRuntime().runFinalization();
  31. synchronized (LOCK) {
  32. justRanFinalization = true;
  33. }
  34. }
  35. 复制代码

参考:

Jvm 系列(二):Jvm 内存结构

理解StrongReference,SoftReference, WeakReference的区别

Java内部类详解

性能优化工具(九)-LeakCanary

LeakCanary原理分析

转载于:https://juejin.im/post/5c87a88af265da2da53f0870

OOM的起点到终点的更多相关文章

  1. dump 验证实例恢复的起点和终点

    什么时候会产生实例恢复呢?当你数据库服务器异常断电,重启数据库就会发生实例恢复.实例恢复是由数据库自动完成的,无须DBA的干涉.当然这里有个前提条件:数据文件. 在线日志文件.控制文件不得有损坏. 我 ...

  2. 【百度地图API】让用户选择起点和终点的驾车导航

    原文:[百度地图API]让用户选择起点和终点的驾车导航 摘要: 如果用户搜索“从机场到火车站”,使用驾车导航DrivingRoute会默认显示一条结果.但同一个城市可能有多个机场和火车站,那么,如何用 ...

  3. Coolest Ski Route-不定起点和终点----在有向变的情况下---求最长路

    这题最开始给你了N个点,M条边,边是单向边,问不指定起点和终点,最长路是什么??? 脑补一下,不定起点和终点的最短路,用弗洛伊德算法搞一搞,但是...那个垃圾算法的复杂度是N^3的,但是这个算法的M高 ...

  4. hdu 2066 多起点 多终点

    多起点 多终点 无向图 结点的个数要自己求 Sample Input6 2 3 //边数 起点数 终点数1 3 5 //u v w1 4 72 8 123 8 44 9 129 10 21 2 //起 ...

  5. 【10.31校内测试】【组合数学】【记忆化搜索/DP】【多起点多终点二进制拆位Spfa】

    Solution 注意取模!!! Code #include<bits/stdc++.h> #define mod 1000000007 #define LL long long usin ...

  6. Key Vertex (hdu 3313 SPFA+DFS 求起点到终点路径上的割点)

    Key Vertex Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Tota ...

  7. 【10.3校内测试【国庆七天乐!】】【DP+组合数学/容斥】【spfa多起点多终点+二进制分类】

    最开始想的暴力DP是把天数作为一个维度所以怎么都没有办法优化,矩阵快速幂也是$O(n^3)$会爆炸. 但是没有想到另一个转移方程:定义$f[i][j]$表示每天都有值的$i$天,共消费出总值$j$的方 ...

  8. HDU2066一个人的旅行---(多起点多终点最短路径)

    http://acm.hdu.edu.cn/showproblem.php?pid=2066 一个人的旅行 Time Limit: 1000/1000 MS (Java/Others)    Memo ...

  9. HDU——2612Find a way(多起点多终点BFS)

    Find a way Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total ...

随机推荐

  1. python爬虫之requests的高级使用

    1.requests能上传文件 # 导入requests模块 import requests # 定义一个dict files = {'file': open('D:/360Downloads/1.t ...

  2. 除了chrome、Firefox之外其他浏览器全都连不上网

    在调试jsp时,总是会遇到eclipse打开jsp网页失败,没有网络,浏览器也除了chrome.Firefox之外其他浏览器全都连不上网,这里我也不清楚是什么问题,但是解决方法是: 打开Interne ...

  3. Kubernetes Pod钩子

    目录 1.Pod容器钩子最终目的 2.何为Pod容器钩子 3.基于PostStart演示 4.基于PreStop演示 5.优雅停止Java应用 1.Pod容器钩子最终目的 之前在生产环境中使用dubb ...

  4. Disruptor 基础篇

    Disruptor 基本概念 RingBuffer结构 Sequencer (生产.消费协调者) EventFactory & EventTranslator SequenceBarrier ...

  5. python之excel的封装

    python之excel的封装 将所有excel的操作都使用面向对象的思维进行封装,即将所有操作都放入一个类中即为封装. 它将excel的处理极大程度的进行了简化操作 封装前需要先处理的操作: 1.在 ...

  6. 用最新的版本,蹦最野的迪~~~IDE写大数据程序避坑指南

    文章更新于:2020-04-05 注:本次实验使用的操作系统及各个程序版本号 类别 版本号 说明 操作系统 Ubuntu 16.04.6 LTS 代号 xenial jdk java version ...

  7. "斜体显示"组件:<i> —— 快应用组件库H-UI

     <import name="i" src="../Common/ui/h-ui/text/c_tag_i"></import> &l ...

  8. python3(十九)Partial func

    # 偏函数(Partial function) # 如int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换 # 但int()函数还提供额外的base参数,默认值为10 ...

  9. L22 Data Augmentation数据增强

    数据 img2083 链接:https://pan.baidu.com/s/1LIrSH51bUgS-TcgGuCcniw 提取码:m4vq 数据cifar102021 链接:https://pan. ...

  10. POJ 跳蚤

    Z城市居住着很多只跳蚤.在Z城市周六生活频道有一个娱乐节目.一只跳蚤将被请上一个高空钢丝的正中央.钢丝很长,可以看作是无限长.节目主持人会给该跳蚤发一张卡片.卡片上写有N+1个自然数.其中最后一个是M ...