前言

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,操作几次登录页面就崩溃了。

开屏页:

private ScheduledFuture<?> mTaskFuture = null;

private void gotoMain() {
...
AdCountDownTask adCountDownTask = new AdCountDownTask();
HexinThreadPool.cancelTaskFutre(mTaskFuture, true);
mTaskFuture = HexinThreadPool.getThreadPool().sheduleWithFixedDelay(xxx);
handler.sendEmptyMessageDelayed(xxx, xxx);
} protected void onDestory() {
...
if (mTaskFuture != null) {
HexinThreadPool.cancelTaskFutre(mTaskFuture, true);
mTaskFuture = null;
}
} 复制代码

委托登录

public class AdsCT {
private Runnable runable = new Runnable {
// 轮播图片
handler.postDelay(runnable, time);
} public void onRemove() {
handler.removeCallbacksAndMessages(null);
}
} public class WeituoLogin {
public void onRemove() {
adsCt.onRemove();
}
}
复制代码

(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

    // 错误示例,内部类持有外部对象导致泄漏
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
} // 修改后,在activity onDestory()的时候,调用myHandler.removeCallbacksAndMessages(null);
private static class MyHandler extends Handler {
private WeakReference<MainActivity> mainActivityWeakReference; public MyHandler(MainActivity mainActivity) {
mainActivityWeakReference = new WeakReference<MainActivity>(mainActivity);
} @Override
public void handleMessage(Message msg) {
MainActivity mainActivity = mainActivityWeakReference.get();
if (mainActivity == null) {
return;
}
mainActivity.jump(null);
super.handleMessage(msg);
}
} // context被线程持有
private void registerException(final Context context) {
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
String appName = context.getResources().getString(R.string.app_name);
}
};
}
复制代码

jvm四种引用

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

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

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

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

PhantomReference类型,

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

    private void testStream() {
InputStream in = null;
try {
in = new FileInputStream("xxx.xml");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
}
} 复制代码

3.静态变量持有对象

单例持有Context,监听器等

五:OOM检查工具-LeakCanary原理

1.监听对象

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

Application.ActivityLifecycleCallbacks lifecycleCallbacks = new..{
...
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
}
复制代码

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

RefWatcher watch(Object obj)
复制代码

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

使用的WeakReference+ReferenceQueue实现

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

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

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

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

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.内部类为什么能访问外部类成员?

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

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

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

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

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

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

    private static StaticObj staticObj = new StaticObj();

    static class StaticObj {
@Override
protected void finalize() throws Throwable {
staticObj = new StaticObj();
super.finalize();
}
}
复制代码

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

    // System.gc()
/**
* If we just ran finalization, we might want to do a GC to free the finalized objects.
* This lets us do gc/runFinlization/gc sequences but prevents back to back System.gc().
*/
private static boolean justRanFinalization; public static void gc() {
boolean shouldRunGC;
synchronized (LOCK) {
shouldRunGC = justRanFinalization;
if (shouldRunGC) {
justRanFinalization = false;
} else {
runGC = true;
}
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
} public static void runFinalization() {
boolean shouldRunGC;
synchronized (LOCK) {
shouldRunGC = runGC;
runGC = false;
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
Runtime.getRuntime().runFinalization();
synchronized (LOCK) {
justRanFinalization = true;
}
}
复制代码

参考:

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. 201771010108 -韩腊梅-java学习进度表

    2018面向对象程序设计(Java)课程进度表 周次 (阅读/编写)代码行数  发布博文量/评论他人博文数量  课余学习时间(小时)  学习收获最大的程序阅读或编程任务 1 30/40 1/0 8   ...

  2. chrome浏览器的json格式化插件

    JSON-Handle   下载地址:         http://jsonhandle.sinaapp.com/ 插件下载后,在浏览器输入:chrome://extensions/ 将下载后的文件 ...

  3. 1055 The World's Richest (25分)(水排序)

    Forbes magazine publishes every year its list of billionaires based on the annual ranking of the wor ...

  4. Nginx知多少系列之(二)安装

    目录 1.前言 2.安装 3.配置文件详解 4.Linux下托管.NET Core项目 5.Linux下.NET Core项目负载均衡 6.Linux下.NET Core项目Nginx+Keepali ...

  5. resetFields() 有时无效问题

    elementui在重置表单时,无法使用this.$refs['formRefVal'].resetFields()清空表单数据; elementui 设置rules后没有效果 解决方法: prop属 ...

  6. Flutter 实现虎牙/斗鱼 弹幕效果

    老孟导读:用Flutter实现弹幕功能,轻松实现虎牙.斗鱼的弹幕效果. 先来一张效果图: 实现原理 弹幕的实现原理非常简单,即将一条弹幕从左侧平移到右侧,当然我们要计算弹幕垂直方向上的偏移,不然所有的 ...

  7. Linux配置dhcp服务器

    一.安装dhcp软件 yum -y install dhcp 二.配置 dhcp 主配置文件 /etc/dhcp/dhcpd.conf ns-update-style interim; log-fac ...

  8. java中查询某个类已经创建了多少个对象了

    这个代码主要是使用类的静态字段和构造函数,可以跟踪某个类所创建对象的个数.请写一个类,在任何时候都可以向它查询“你已经创建了多少个对象? 主要是在构造函数中用到了静态数据,进行显示已经构造了多少个类对 ...

  9. Linux下修改efi启动项

    Linux下有一个efibootmgr工具可以编辑efi启动项,十分方便,简单介绍如下 直接运行efibootmgr会显示出当前所有efi启动项,每个启动项前都有相应编号, 可以使用efibootmg ...

  10. docker-compose 部分错误

    Get https://hub.17kxkx.com/v2/: dial tcp 39.106.209.67:443: connect: connection refused vim /etc/doc ...