内存泄漏分析和开发注意点

概要

Android的程序由Java语言编写,所以Android的内存管理与Java的内存管理相似。程序员通过new为对象分配内存,所有对象在java堆内分配空间;然而对象的释放是由垃圾回收器来完成的。C/C++中的内存机制是“谁污染,谁治理”,java的就比较人性化了,给我们请了一个专门的清洁工(GC)。

深入理解 Java 垃圾回收机制

Java GC回收算法

介绍

Java中的内存回收全交由GC回收器,程序员无法手动释放内存,可用几个函数访问GC,如`System.gc()`

通知GC回收器开始执行。不同额JVM虚拟机实现不同,所以效果有限。通常Java中GC线程优先级较弱。

​ CG回收为使用对象占用的内存资源时,会想判断此对象是否处于存活状态,有没有被其他对象引用或关联,如果没有,则回收。内存溢出就是因为对象已经使用完毕,缺没有断掉对此对象的引用,GC回收器则没有回收此对象占用的内存资源。

对象存活分析

​ 不同的Java虚拟机会采用不同的判断机制,Java采取的是可达性分析算法。

引用计数法

~
在堆中存储对象A时,在对象A头处维护一个counter计数器,如果一个对象引用了对象A,则将A的counter++,引用失效则counter--。若 counter == 0 则对象已被废弃,可回收。
~

此逻辑无法解决对象A、B相互持有,类似死锁循环的情况。

可达性分析

Java使用可达性分析来判断对象是否存活。

示例图
~
通过一组 GC Roots 对象作为引用关系的起点,往下搜索经过的路径称为引用链(Reference Chain),当一个对象到GC Roots不与任何引用链相连时,则判定此对象到GC Roots不可达,是可回收对象。
~

可做GC Roots的对象:

  • 虚拟机栈的的引用对象
  • 方法区中的类静态属性的引用对象
  • 方法区中的常量的引用对象
  • 本地方法栈中JNI的引用对象

关于的虚拟机栈方法区本地方法栈的理解可看 拓展知识 - Java虚拟机内存分区

内存泄漏原因

简单了解Java GC回收算法后,便知道内存泄漏的根本原因:

GC回收工作时,应当回收的未使用对象通过了对象存活判断,确认为存活状态。于是对此未使用对象不做回收处理。此种情况不断发生,积累未回收对象到一定量,就会抛出OOM内存溢出异常。

Android内存泄漏常见场景

关键在于对象之间的生命周期长短。

  • 资源对象未关闭
    • BroadcastReceiver,ContentObserver、File、Cursor、Socket、Bitmap等
  • 静态对象
    • 集合类HashMap、Vector等,不及时setnull会一直持有对象
    • 静态的View、Activity。View默认持有当前Activity的引用
    • 键盘焦点,InputMethodManager
    • 当Activity有View获取键盘焦点
    • 在Activity销毁后View会被InputMethodManagerhold
    • View --hold--> Activity对象,造成泄漏
  • 监听器对象及时移除
    • addxxxListener 等,一般情况:静态对象 --hold--> listener实例 --hold--> Activity对象
  • 内部类
    • 匿名内部类持有外部类引用
    • 匿名对象进行异步任务时,可能产生泄漏
    • 当Activity回收时停止异步任务
    • 非静态内部类:持有外部类引用
    • Handler是串行处理任务的,当Activity回收时,Looper仍然有消息未处理完毕时会发生泄漏。
    • 因为Looper使用ThreadLocal实例保存,此实例对象是静态的。
    • Looper --hold--> MessageQueue --hold--> Handler (msg.target) --hold--> Activity
    • 建议使用WeakReference 弱引用
  • WebView在主线程中使用
    • 为Webview新开线程,通过AIDL与主线程通信

检测工具

  • MemoryMonitor:随时间变化,内存占用的变化情况

    • LeakCanary:实时监测内存泄漏的库
  • MAT:输入HRPOF文件,输出分析结果

    • Histogram:查看不同类型对象及其大小
    • DominateTree:对象占用内存及其引用关系
    • MAT使用教程

拓展知识

Java虚拟机内存分区

程序计数器
  • 线程私有
  • 没规定OOM情况

在虚拟机的概念模型中,字节码解释器工作就是通过改变计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

Java虚拟机内多线程是通过线程轮流切换并分配处理器时间的方式实现的,一个处理器(多核处理器一个内核)在一个确定的时刻只能执行一个线程的任务。线程反复切换并且恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。

如果线程执行的是Java代码,计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,计数器为空(Undefined)。

虚拟机栈
  • 线程私有
  • StackOverflowError
  • OutOfMemoryError

Java内存分区经常被程序员划分为栈内存堆内存,这种划分很粗躁,表明了程序员最关心的部分。

这常说的栈内存相当于虚拟机栈中的局部变量表部分。

局部变量表

存放了编译期可知的各种基本类型对象引用returnAddress类型

  • 基本类型:boolean、byte、char、int等
  • 对象引用:对象起始地址的引用指针、代表对象的句柄、其他与此对象相关的位置
  • returnAddress:字节码指令地址

线程请求栈深度大于虚拟机的允许深度,会抛出StackOverflowError。目前大多数虚拟机栈可以动态扩展,当扩展时无法申请到足够内存,会抛出OutOfMemoryError。

操作数栈

操作数栈用于存放JVM从局部变量表复制的常量或变量,提供读取、结果入栈,也用于存放调用方法所需要的参数以及接受方法返回的结果。

  • 后进先出LIFO
  • 最大深度由编译期确定
  • 可以存放JVM定义的任意数据类型的值
  • 基本类型long、double占用两个深度,其他占用一个深度
动态连接
  • 栈帧都持有运行时常量池中该栈帧所属方法的引用
  • 持有该引用是为了支持方法调用过程中的动态连接

Class文件的常量池存在大量符号引用,字节码(Java)中的方法调用指令

~
常量池中 指向方法的符号引用 作为参数
~

这些符号引用分为两个部分:

  • 静态解析:在类加载阶段或首次使用时 转化为直接引用如staitc/final
  • 动态连接:在运行期间转化为直接引用
本地方法栈
  • 线程私有

和虚拟机栈相似。虚拟机栈为虚拟机执行 Java方法(字节码) 服务,本地方法栈为虚拟机执行Native方法服务,也会抛出StackOverflowError、OutOfMemoryError异常。

虚拟机规范中强制规定本地方法栈使用的语言使用方法数据结构,也有虚拟机 Sun HotSpot直接将本地方法栈和虚拟机栈合并。

任何本地方法接口都会使用本地方法栈。

调用Java方法时,虚拟机会创建一个新的栈帧并压入虚拟机栈中。

此Java方法--调用-->本地方法时,虚拟机栈保持不变,只是简单的动态连接并直接调用指定的本地方法。

示例图

图中流程:

  1. 调用了两个Java方法
  2. 第二个Java方法调用了本地方法
    1. 假设本地方法栈是个C语言栈
  3. 第一个C函数调用第二个C函数
  4. 第二个C函数调用第三个Java方法
  5. 第三个Java方法又调用一个Java方法
JVM堆
  • 线程共有
  • 主要用于存储对象

在虚拟机启动时创建的内存区域,线程共享。唯一的目的就是存放对象实例。

Java堆是垃圾回收器管理的主要区域,很多时候被称为”GC堆”。

Java堆可以处于物理上不连续的内存空间。

Java堆内存可拓展主流实现方式,当堆中没有内存完成实例分配,且堆无法拓展时,抛出OOM

方法区
  • 线程共有
  • 存储类信息:全限定名、类型接口\类、访问修饰符
  • 存储常量、静态变量
  • 即时编译后的代码等数据

Java虚拟机规范中描述方法区为堆的逻辑部分,但它有个实际的名字非堆,用来区分Java堆。

方法区实现也肯能在堆区。

Java支持方法重载(符号毁灭)和方法重写(多肽、动态派发)

~
为了处理好动态派发,一种可能的实现就是专门开辟一个区,单独管理所有方法。
按照稳定性给对象方法进行排序,聚集类似方法。
调用方法时在方法区搜索一次定位到相同方法起始位置。
~

减少GC开销的措施

  • 不要显式调用System.gc()
    • 此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。
  • 尽量减少临时对象的使用
    • 临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。
  • 对象不用时最好显式置为Null
    • 一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。
  • 尽量使用StringBuffer,而不用String来累加字符串
    • 使用String进行大量繁琐字符串拼加时,会产生大量临时对象,使剩余内存空间碎片话。此时剩余内存总量不少,但可能无法分配出满足指定大小的剩余内存区间,产生内存抖动现象,频繁GC占用过多硬件资源,造成卡顿,甚至出现OOM。
  • 谨慎使用静态变量
    • 静态变量属全局变量,不进行GC回收,它持有的对象也不会回收。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值