type
status
date
slug
summary
tags
category
icon
password
面试中常问的内存泄漏和内存溢出
内存溢出:(out of memory)指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
内存泄漏:(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏最终会导致内存溢出。

Java中的内存泄漏

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
GC Root的对象有:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
(2)方法区中的类静态属性引用的对象。
(3)方法区中常量引用的对象。
(4)本地方法栈中JNI(即一般说的Native方法)引用的对象。
(5)正在运行的线程。

Android 中内存泄漏的原因

在 Android 中内存泄漏的原因其实和在 Java 中是一样的,即某个对象已经不需要再用了,但是它却没有被系统所回收,一直在内存中占用着空间,而导致它无法被回收的原因大多是由于它被一个生命周期更长的对象所引用。其实要分析 Android 中的内存泄漏的原因非常简单,只要理解一句话,那就是生命周期较长的对象持有生命周期较短的对象的引用。

常见内存泄漏场景

1单例造成的内存泄漏

因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄露。以Activity为例,当我们启动一个Activity,并调用getInstance(Context context)方法去获取AppSettings的单例,传入Activity.this作为context,这样AppSettings类的单例sInstance就持有了Activity的引用,当我们退出Activity时,该Activity就没有用了,但是因为sIntance作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。
为了避免这样单例导致内存泄露,我们可以将context参数改为全局的上下文:

2静态变量导致内存泄漏

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。 比如下面这样的情况,在Activity中为了避免重复的创建info,将sInfo作为静态变量:
Info作为Activity的静态成员,并且持有Activity的引用,但是sInfo作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfo仍然引用了Activity,Activity不能被回收,这就导致了内存泄露。
在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。

3非静态内部类导致内存泄露

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。见Handler。

LeakCanary检测原理

  1. 在onActivityDestroyed(Activity activity)的回调中,去检测Activity是否被回收,检测方式如以下步骤。
  1. 使用一个弱引用WeakReference指向这个activity,并且给这个弱引用指定一个引用队列queue,同时创建一个key来标识该activity。
  1. 然后将检测的方法ensureGone()投递到空闲消息队列。
  1. 当空闲消息执行的时候,去检测queue里面是否存在刚刚的弱引用,如果存在,则说明此activity已经被回收,就移除对应的key,没有内存泄漏发生。
  1. 如果queue里不存在刚刚的弱引用,则手动进行一次gc。
  1. gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,否则没有发生内存泄漏,流程结束。
关键问题
1、为什么要放入空闲消息里面去执行? 因为gc就是发生在系统空闲的时候的,所以当空闲消息被执行的时候,大概率已经执行过一次gc了。
2、为什么在空闲消息可以直接检测activity是否被回收? 跟问题1一样,空闲消息被执行的时候,大概率已经发生过gc,所以可以检测下gc后activity是否被回收。
3、如果没有被回收,应该是已经泄漏了,为什么再次执行了一次gc,然后再去检测? 根据问题2,空闲消息被执行的时候,大概率已经发生过gc,但是也可能还没发生gc,那么此时activity没有被回收是正常的,所以我们手动再gc一下,确保发生了gc,再去检测activity是否被回收,从而100%的确定是否发生了内存泄漏。

KOOM检测原理

KOOM根据OOM产生的5个场景设计了5个OOMTracker用来检测内存问题。
(1)堆内存溢出;这个是典型的OOM场景;
(2)没有连续的内存空间分配;这个主要是因为内存碎片过多(标记清除算法),导致即便内存够用,也会造成OOM;
(3)打开过多的文件;如果有碰到这个异常OOM:open to many file的伙伴,应该就知道了;
(4)虚拟内存空间不足
(5)开启过多的线程;一般情况下,开启一个线程大概会分配500k的内存,如果开启线程过多同样会导致OOM

1 HeapOOMTracker

(1)获取内存占用率,与配置的阈值比较。
内存占用率计算方法:两个参数:-xmx和-xms,其中-xmx代表当前进程允许占用的最大内存(例如64M或者128M),-xms代表当前进程初始申请的内存,内存占用率就是这两个值的比例。
(2)两次采样的内存占用率升高,mOverThresholdCount变量会自增1。当变量达到阈值(第二个阈值,比如5次),认定系统发生了内存泄漏,这个时候就需要告警,并dump内存快照分析问题。
如果降低,重置count。

2 ThreadOOMTracker

线程检测器跟内存检测器原理基本一致,同样也是在循环检测中,拿到线程的总数与阈值进行比较,如果超出范围那么就认为是异常,需要上报。

3 FastHugeMemoryOOMTracker

当进程内存占用率超过设定的forceDumpJavaHeapMaxThreshold阈值(例如0.9),直接上报。
因为HeapOOMTracker属于高内存持续监测,需要连续多次检测才会报警;但是如果我们程序中加载了一张大图片,内存直接暴涨(超过0.9),可能都等不到HeapOOMTracker检测多次程序直接Crash,这个时候就需要FastHugeMemoryOOMTracker出马了,主要进入高危阈值,直接报警。
还有一个判断条件就是,会比较前后两次的内存使用情况,如果超出了阈值也会直接报警,例如加载大图

4 dump方案

KOOM是线上监控方案,对实时性要求不高,因此dump操作是放在子进程中进行的。(fork()创建子进程就是父进程的一份拷贝,大部分属性都继承过来)
不放在子线程dump是因为:第一在任意线程dump都会STW,第二虽然是在子线程内,但是还是会产生内存垃圾,还是需要GC去清理,如果放在单独的进程中,就不会加快主进程的GC,也是尽可能避免在dump时发生崩溃影响主进程。
 
Android基础-事件分发Android源码-Glide
LuluNotion
LuluNotion
一个普通的干饭人🍚
公告
type
status
date
slug
summary
tags
category
icon
password
🎉NotionNext 4.0即将到来🎉
-- 感谢您的支持 ---
👏欢迎更新体验👏