内存泄漏

Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同时防止未释放的内存把堆栈(heap)挤爆而导致程序崩溃的可能,所以写出来的代码更为安全。

不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑可能(logical leak)。如果不小心,你的Android应用很容易浪费掉未释放的内存,最终导致抛出内存不足的错误(out-of-memory)。

说到内存泄漏,就不得不提到内存溢出,这两个是比较容易混淆的概念。

  • 内存溢出:程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现OOM。
  • 内存泄漏:程序向系统申请分配内存空间后,在使用完毕后未释放内存。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这就是内存泄漏。

大量的内存泄漏会导致内存溢出(OOM)。

内存泄漏原因分析

如果持有对象的强引用,垃圾回收无法在内存中回收这个对象。

内存泄漏的真正原因是:持有对象的强引用,且没有及时释放,进而造成内存单元一直被占用,从而导致内存溢出。

堆内存中存放着对象和数组的实例,在Java中堆内存不会随着方法的结束而清空,所以在方法中定义了局部变量,在方法结束后变量依然存活在堆中。如果我们不停地创建新对象,堆(heap)的内存就会被耗尽。所以Java引入了垃圾回收(garbage collection,简称GC)去处理堆内存,但如果对象一直被引用无法被回收,造成内存的浪费,无法再被使用。所以对象无法被GC回收就是造成内存泄漏的原因。

  • 一般内存泄漏(traditional memory leak):由忘记释放分配的内存导致的。(例如:Cursor忘记关闭等)。
  • 逻辑内存泄漏(logical memory leak):当应用不再需要这个对象,但仍未释放该对象的所有引用。

如果持有对象的强引用,垃圾回收器是无法在内存中回收这个对象的。

典型的内存泄漏及其原因如下

内容摘自《Java程序员面试宝典》

  1. 全局集合
    在大的应用程序中有某种全局的数据储存库是很常见的,所以必须有某种机制从储存库中移除不再需要的数据。

  2. 缓存
    缓存是一种数据结构,用于快速查找已经执行的操作结果。缓存通常都是以动态方式实现的,其中新的结果是在执行时添加到缓存中的,但如果添加的缓存数量过多,又没有进行清理的方法,就会出现占用内存过多的情况。所以需要有一种机制计算并移除缓存中的数据,比如LRU算法,或者使用软引用/弱引用加快内存回收。

  3. ClassLoader
    Java中ClassLoader结构的使用为内存泄漏提供了许多可乘之机。ClassLoader的特别之处在于它不仅涉及“常规”的对象引用,还涉及元对象引用,比如字段、方法和类。这意味着只要有对字段、方法、类或ClassLoader对象的引用,ClassLoader就会驻留在JVM中,因为ClassLoader本身可以关联许多类及其静态字段,所以就会有许多内存被泄漏。

Android中可能发生内存泄漏的情况

在Android开发中,最容易引发的内存泄漏问题的是Context。比如Activity的Context,就包含大量的内存引用,例如View hierarchies和其他资源。一旦泄漏了Context,也意味着泄露了它指向的所有对象。Android机器内存有限,太多的内存泄漏容易导致OOM。

检测逻辑内存泄漏需要主观判断,特别是对象的生命周期并不清晰。幸运的是,Activity有着明确的生命周期,很容易发现泄漏的原因。Activity.onDestroy()被视为一个Activity生命周期的结束。从程序上看,它应该被销毁了,或者Android系统需要回收这些内存。当内存不足时,Android也会回收优先级比较低的Activity。如果这个方法执行完,在堆栈中仍存在持有该Activity的强引用,垃圾回收器就无法把它标记成已回收的内存,而我们本来目的就是要回收它!结果就是Activity存活在它的生命周期之外。

Activity是重量级对象,应该让Android系统来处理它。然而,逻辑内存泄漏总是在不经意间发生。在Android中,导致内存泄漏的陷阱不外乎两种:

  • 全局进程(process-global)的static变量;
  • 活在Activity生命周期之外的线程,没有清空对Activity的强引用。

检查一下你有没有遇到下列的情况。

Static Activity

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。

如果这个静态变量在Activity生命周期结束后没有清空,就导致内存泄漏。因为static变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。

    static Activity activity;void setStaticActivity() {activity = this;}View saButton = findViewById(R.id.sa_button);saButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {setStaticActivity();nextActivity();}});

Static View

类似的情况会发生在单例模式中,如果Activity经常被用到,那么在内存中保存一个实例是很实用的。正如之前所述,强制延长Activity的生命周期是相当危险而且不必要的,无论如何都不能这样做。

特殊情况:如果一个View初始化耗费大量资源,而且在一个Activity生命周期内保持不变,那可以把它变成static,加载到视图树上(View Hierachy),像这样,当Activity被销毁时,应当释放资源。(译者注:示例代码中并没有释放内存,把这个static view置null即可,但是还是不建议用这个static view的方法)

    static view;void setStaticView() {view = findViewById(R.id.sv_button);}View svButton = findViewById(R.id.sv_button);svButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {setStaticView();nextActivity();}});

Inner Class

继续,假设Activity中有个内部类,这样做可以提高可读性和封装性。将如我们创建一个内部类,而且持有一个静态变量的引用,恭喜,内存泄漏就离你不远了(译者注:销毁的时候置空,嗯)。

    private static Object inner;void createInnerClass() {class InnerClass {}inner = new InnerClass();}View icButton = findViewById(R.id.ic_button);icButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {createInnerClass();nextActivity();}});

内部类的优势之一就是可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。

Anonymous Class

相似地,匿名类也维护了外部类的引用。所以内存泄漏很容易发生,当你在Activity中定义了匿名的AsyncTsk。当异步任务在后台执行耗时任务期间,Activity不幸被销毁了(译者注:用户退出,系统回收),这个被AsyncTask持有的Activity实例就不会被垃圾回收器回收,直到异步任务结束。

    void startAsyncTask() {new AsyncTask<Void, Void, Void>() {@Override protected Void doInBackground(Void... params) {while(true);}}.execute();}super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);View aicButton = findViewById(R.id.at_button);aicButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {startAsyncTask();nextActivity();}});

Handler

同样道理,定义匿名的Runnable,用匿名类Handler执行。Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity实例不会被销毁了,于是导致内存泄漏。

    void createHandler() {new Handler() {@Override public void handleMessage(Message message) {super.handleMessage(message);}}.postDelayed(new Runnable() {@Override public void run() {while(true);}}, Long.MAX_VALUE >> 1);}View hButton = findViewById(R.id.h_button);hButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {createHandler();nextActivity();}});

Thread

通过Thread来展现内存泄漏。

    void spawnThread() {new Thread() {@Override public void run() {while(true);}}.start();}View tButton = findViewById(R.id.t_button);tButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {spawnThread();nextActivity();}});

TimerTask

只要是匿名类的实例,不管是不是在工作线程,都会持有Activity的引用,导致内存泄漏。

    void scheduleTimer() {new Timer().schedule(new TimerTask() {@Overridepublic void run() {while(true);}}, Long.MAX_VALUE >> 1);}View ttButton = findViewById(R.id.tt_button);ttButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {scheduleTimer();nextActivity();}});

Sensor Manager

最后,通过Context.getSystemService(int name)可以获取系统服务。这些服务工作在各自的进程中,帮助应用处理后台任务,处理硬件交互。如果需要使用这些服务,可以注册监听器,这会导致服务持有了Context的引用,如果在Activity销毁的时候没有注销这些监听器,会导致内存泄漏。

        void registerListener() {SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);}View smButton = findViewById(R.id.sm_button);smButton.setOnClickListener(new View.OnClickListener() {@Override public void onClick(View v) {registerListener();nextActivity();}});

总结

看过那么多会导致内存泄漏的例子,容易导致吃光手机的内存使垃圾回收处理更为频发,甚至最坏的情况会导致OOM。垃圾回收的操作是很昂贵的开销,会导致肉眼可见的卡顿。所以,实例化的时候注意持有的引用链,并经常进行内存泄漏检查。

相应的解决方案

使用弱引用代替强引用

在生命周期结束时清除引用释放内存

少用非静态内存类,改用静态内部类

静态内存类不持有外部类的引用,避免链式引用

尽量避免使用静态变量

在Activity结束时注销监听器

使用leakcanary分析内存泄漏

《Java程序员面试宝典》中的方法

由于生产环境和测试环境的设备不同,所以内存泄漏通常发生在无法测试的生产环境中。

  1. 使用一些开销较低的工具来监控和查找内存泄漏,还需要能够重启系统或修改代码就可以将这些工具连接到正在运行的系统上。可能最重要的是,当进行分析时,需要能够断开工具而保持系统不受干扰。
  2. 不间断地监控GC的活动,确定内存使用量是否随着时间增加,如果确实如此,就可能发生了内存泄漏。

Android中的内存使用

  • Android程序有内存限制。
  • 频繁的GC容易造成程序响应问题。
  • 进程自动回收:运行在后台的程序,拥有的内存越大,越容易被回收——任务栈和进程的关系——做好数据持久化、程序状态连续性和恢复。

对象使用的建议


Android程序偏向更轻量级的对象,更少的内存占用时间(除去必要的内存缓存),重用避免重复创建。


  • 尽量避免使用枚举
    使用static final int代替
  • 多使用final修饰
    除非业务需要,首选final修饰,编译器会优化
  • 图片
    成熟的库(Android-Universal-Image-Loader),用多少取多少,及时释放缓存。
  • 软引用和弱引用
  • 池和对象复用
    避免对象创建,引起内存抖动。例如知道一个集合是固定大小的话,那么每次网络请求结束后更新对象字段值,而不是clear又创建一批新对象。
    线程池——好处不多说。使用时注意因为run持久不结束,线程对象对应的字段和局部变量注意泄漏。
    Adapter中数据对象和View的复用。
  • 避免不必要的getter和setter
    仅仅是简单的POJO,完全没必要访问控制器。
  • 合并handler
    handler不要离开Activity,最好的一个Activity使用一个就够了。不要使用Handler代替回调来通信,使用第三方库,如EventBus来解耦,handler传递数据很低效(不及时-它不是同步的,对象序列化)。
    handler是用来完成跨线程的通信的。
  • 及时释放引用
    能使用局部变量的,就不要使用字段。方法中,释放那些不使用又继续占有的对象引用。
    四大组件对象不是由我们new的,有其明确的生命周期,在“销毁”动作时从对象引用层面释放该释放的。
  • 优先使用静态内部类
    匿名内部类总是默认持有外部类对象的引用。
  • 在保证速度的前提下使用文件缓存
    一些情况下甚至是必须的,如登录状态。
  • 使用ApplicationContext
    仅在必要的时候——如dialog——使用Activity,而且注意Activity的Context的及时释放。
  • 使用具体类而不是接口
    例如,HashMap,变量不需要声明为Map,这会有更好的执行速度。
    没必要为“不存在”的扩展性做牺牲。
  • 在onDestory()中做好清理工作
    主要是引用的释放,广播的取消注册,回调/监听对象的解除,handler的取消投递的消息、网络请求的取消、动画的停止,线程、其它异步任务和处理等。

对于泄漏问题,只有一点,不使用就及时把保持引用的成员变量和局部变量设置为null。重点注意回调和静态字段。

Android内存泄露——全解析和处理办法
Android内存泄漏的八种可能(上)
Android内存泄漏分享

Android内存泄漏的分析和避免相关推荐

  1. android内存泄漏原因分析,Android Studio3.6的内存泄漏检测功能 VS LeakCanary

    2020年2月,谷歌发布了Android Studio 3.6版.它包括一个新的"内存泄漏检测"功能.这是否意味着我们不再需要流行的内存泄漏检测库"Leak Canary ...

  2. android内存泄漏原因分析,android 内存泄漏问题

    内存泄露问题在一些压力测试的场景很容易暴露,例如一些常用应用场景反复操作(eg:反复切换前后摄像头,反复进入退出相机应用.压力拍照等等). 内存泄露一般表现为: ①内存分配释放,导致进程空间虚拟地址被 ...

  3. Android 内存泄漏问题分析 指南

    内存异常的情况: 我们看到一直在上涨~ 内存正常的情况: 内存有涨有跌,这样才说明内存能够被回收. Mat 工具的使用: 使用mat 打开hprof文件,查看你认为可能泄露的类的引用. 如果是java ...

  4. android内存泄漏原因分析,Android 内存泄漏案例分析总结(Handler)

    在Android开发开发中,操作不当很容易引起内存泄漏,这里主要记录下平时遇到问题,包括:静态变量(也包含集合).非静态的内部类.Handler.监听器,尤其是 Handler 在开发中要格外的注意. ...

  5. android释放acitity内存,Android 内存泄漏分析与解决方法

    在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...

  6. Android内存泄漏分析及调试

    2019独角兽企业重金招聘Python工程师标准>>> Android内存泄漏分析及调试 分类: Android2013-10-25 11:31 5290人阅读 评论(5) 收藏 举 ...

  7. Android 内存泄漏分析指北

    android 内存泄漏分析指北 简单来说内存泄漏就是当对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用 java 垃圾回收介绍: Java 虚拟机运行所管理的内存包括以下几个 ...

  8. Android内存泄漏的检测流程、捕捉以及分析

    https://blog.csdn.net/qq_20280683/article/details/77964208 Android内存泄漏的检测流程.捕捉以及分析 简述: 一个APP的性能,重度关乎 ...

  9. Android 内存泄漏分析与解决方法

    Android 内存泄漏分析与解决方法 参考文章: (1)Android 内存泄漏分析与解决方法 (2)https://www.cnblogs.com/start1225/p/6903419.html ...

最新文章

  1. 学习java得一些笔记(5)
  2. leetcode307. Range Sum Query - Mutable
  3. hdu1058(dp||优先队列)
  4. 小白如何入门Mybatis?这里有答案
  5. JSON数据从OSS迁移到MaxCompute最佳实践
  6. CoreCLR文档翻译 - GC的设计
  7. 从PeopleEditor控件中取出多用户并更新到列表
  8. 面试官系统精讲Java源码及大厂真题 - 04 Arrays、Collections、Objects 常用方法源码解析
  9. X命名空间-标记扩展
  10. android studio gradle 打jar 包 (混淆+第三方库包)
  11. java程序知识_java的基本知识点
  12. (转载)make的-j命令(加速Linux程序编译)
  13. 修改 tomcat 内存
  14. graphviz安装
  15. linux 取消分区,如何在 Linux 中删除分区 | Linux 中国
  16. nginx 之安全配置
  17. Android显示——一帧的渲染过程(VSYNC)
  18. 教大家如何去做外链才是最好的
  19. 除了Micrsoft Office和WPS,有没有免费好用的office软件?
  20. 华为服务器安装Ubuntu 18.04.2 详细步骤(附图文介绍)

热门文章

  1. 实探临沂网戒中心原址 杨永信仍任副院长坐诊
  2. 用python进行五角星的绘制
  3. go依赖包出现版本不兼容的问题
  4. linux中nginx.conf的文件路径以及重启nginx的方法
  5. 使用正则匹配去掉SQL文本中的注释
  6. update和upgrade的区别
  7. 冒充最高检网络电信诈骗之追溯
  8. 绝对值得收藏的十位电影配乐大师 (中)
  9. 通过Boomerang按计划在Gmail中发送或接收电子邮件(并且我们有邀请)
  10. 服务器怎样进行维护 服务器维护教程