内存泄漏/内存溢出
一、内存泄漏 & 内存溢出
在软件开发与运行过程中,内存管理是一个至关重要的环节。然而,由于各种原因,如程序设计的缺陷或资源管理的不当,可能会导致两种常见的内存问题:内存泄漏(Memory Leak)和内存溢出(Memory Overflow)。各种语言/操作系统上预防和解决也有不同方式方案,以下是关于两者概念的详细介绍:
内存泄漏:
内存泄漏指程序中已经不再需要使用的内存没有被释放,从而造成内存资源浪费和程序性能下降。其特征是程序使用内存总量持续增加,直到程序崩溃或者系统强制关闭。
原因:
- 资源未被正确释放:程序动态分配了内存资源,但没有及时释放,导致内存泄漏。
- 垃圾回收机制失效:在使用Java等高级语言编写的程序中,垃圾回收机制负责自动释放不再使用的内存资源。但如果程序员在编写代码时存在逻辑错误,就有可能导致垃圾回收机制失效,进而导致内存泄漏。
- 循环引用:在使用面向对象编程语言时,两个对象之间可能会发生相互引用的情况。如果这种引用形成了一个环路,就会导致这些对象永远无法被释放,从而产生内存泄漏。
影响:
- 系统崩溃:内存泄漏会导致系统可用内存不足,从而造成系统崩溃或者异常退出。
- 程序性能下降:内存泄漏会让程序占用更多的内存,导致程序性能下降,响应速度变慢。
- 安全问题:黑客可能会利用内存泄漏漏洞实现攻击,比如利用堆栈溢出漏洞进行缓冲区溢出攻击。
检测与避免方法:
- 使用内存监控工具:
- Windows Task Manager、
- Linux top命令、Valgrind
- Android Liko KOOM Leakcanary
- 代码审查:通过仔细阅读代码,找出可能导致内存泄漏的逻辑错误,发现和解决问题。
- 显式地释放资源:在程序中动态分配了内存后,一定要在不再需要使用这些资源时显式地释放它们。例如,在C语言中使用free函数进行内存释放。
- 使用智能指针:C++等语言中提供了智能指针的概念,可以自动管理内存,避免手动释放资源时遗漏。
- 避免循环引用:在使用面向对象编程时,尽量避免对象之间相互引用形成环路的情况。如果无法避免,可以使用弱引用等技术来解决。
内存溢出:
内存溢出是指程序在申请内存时,所需的内存空间超过了系统所分配的内存空间,使得程序无法正常运行.
原因:
- 分配的内存过于庞大,在内存池中没有足够的连续空间满足需求。
- 内存泄漏:应用程序持续向系统申请内存空间,但没有及时释放,导致内存池被消耗殆尽。
- 代码编写问题:如未正确释放已经申请的内存、指针操作错误等。
- 操作系统本身限制了进程所能申请的最大内存。
影响:
内存溢出会导致程序崩溃或者无法正常运行,因为它直接涉及到程序的运行空间不足。
解决办法:
- 优化代码:避免出现无效的内存分配和内存泄漏。
- 使用内存池技术:尽可能地重复利用已经申请的内存空间。
- 增大操作系统对程序可用的最大内存限制(前提是操作系统支持)。
- 将需要处理的数据划分为更小的块,分批进行处理,这样可以使每个块需要分配的内存量变小。
二、关联/区别
关系:
内存泄漏会造成内存溢出, 内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积起来后果很严重,无论多少内存,迟早会被占光,最终会导致内存溢出。
区别:
发生时机:内存溢出通常发生在程序运行时,当数据结构的大小超过预设限制或者递归调用栈过深时,就会发生内存溢出。而内存泄漏则是在程序持续运行过程中逐渐累积的,当不再使用的内存没有及时释放时,就会产生泄漏。
表现方式: 内存溢出会导致程序崩溃或者无法正常运行,因为它直接涉及到程序的运行空间不足。而内存泄漏在初期可能不会对程序产生明显影响,但随着时间的推移,未释放的内存不断累积,最终会导致系统资源耗尽,程序性能下降。
解决方案: 对于内存溢出,解决方法通常涉及到优化数据结构和算法,减少内存消耗,或者增加系统可用内存。而解决内存泄漏则需要定位泄漏源头,修复代码中的内存管理问题,确保不再使用的内存能够被及时释放。
三、Android
内存泄漏场景:
1. 非静态内部类/匿名内部类
造成原因:非静态内部类默认会持有外部类的引用,如果内部类的生命周期超过了外部类,就会造成内存泄漏。
场景:当Activity销毁后,由于内部类中存在异步耗时任务还在执行,导致Activity实例一直被内部类持有无法被回收,造成内存泄漏。
1 | public class TestActivity extends AppCompatActivity { |
解决办法:使用静态内部类,并通过弱引用的方式持有外部类。
1 | public class TestActivity extends AppCompatActivity { |
2. 静态成员变量造成的内存泄漏
造成原因:静态成员变量的生命周期等于应用程序的生命周期,如果该静态成员引用的变量生命周期小于该静态变量,就会造成内存泄漏。
场景:静态成员变量持有了一个耗费资源过多的实例(如Activity、Fragment)。
1 | public class Person { |
解决办法:尽量避免使用Static成员变量引用资源耗费过多的实例,如果必须使用,可以使用Application的Context。这样可以随着应用生命周期创建销毁.
3. 单例模式造成的内存泄漏
造成原因:单例模式由于其具有静态特性,导致其生命周期等于应用程序生命周期,如果单例中持有别的类的实例,就会造成内存泄漏。
场景:单例模式中持有一个耗费资源过多的实例(如Context)。
1 | public class SingleInstance { |
解决办法:使用Application的Context代替Activity的Context。
4. Handler造成的内存泄漏
造成原因:Handler会持有外部类的对象(如Activity),如果Handler中还有消息没执行完,此时创建Handler的Activity关闭就会造成内存泄漏。
场景:Activity中通过一个子线程异步请求网络数据,请求成功后更新当前页面。
1 | public class MainActivity extends AppCompatActivity { |
解决办法:
- 使用静态内部类+弱引用。
- 在Activity销毁时,及时清理消息队列中的消息。
1 | public class MainActivity extends AppCompatActivity { |
5. 资源性对象未关闭造成的内存泄漏
造成原因:对于资源性对象(如BroadcastReceiver、EventBus等),在不再使用时,应该立即调用其close()函数将其关闭,然后再置为null。否则会造成内存泄漏。
场景:Activity中注册了BroadcastReceiver,但在onDestroy方法中没有注销该接收器。
1 | public class MainActivity extends AppCompatActivity { |
解决办法:在onDestroy方法中注销广播接收器。
1 |
|
6. 容器中的对象未清理造成的内存泄漏
造成原因:在使用集合类(如ArrayList、HashMap等)时,如果集合中的对象不再使用,但没有及时从集合中移除,就会导致这些对象无法被垃圾回收器回收,从而造成内存泄漏。
场景:在Activity中创建了一个ArrayList来存储一些对象,但在Activity销毁时,没有清空这个ArrayList。
解决办法:在Activity销毁时,清空集合中的对象,并将其置为null。
1 |
|
Liko
在 OOM 和内存触顶时通过用户无感知 dump 来获取 HPROF 文件,当 App 退出到后台且内存 充足的情况进行分析,裁剪 HPROF 回传进行分析
KOOM
利用系统内核COW(Copy-on-write,写时复制)机制,每次dump内存镜像前先暂停虚拟机,然后fork子进程来执行dump操作,父进程在fork成功后立刻恢复虚拟机运行,整个过程对于父进程来讲总耗时只有几毫秒。内存镜像于闲时进行独立进程单线程本地分析,分析完即删除。
LeakCanary
原理分析
分为以下几步:
- 监测Activity 的生命周期的 onDestroy() 的调用。
- 当某个 Activity 的 onDestroy() 调用后,便对这个 activity 创建一个带 ReferenceQueue 的弱引用,并且给这个弱引用创建了一个 key 保存在 Set集合 中。
- 如果这个 activity 可以被回收,那么弱引用就会被添加到 ReferenceQueue 中。
- 等待主线程进入 idle(即空闲)后,通过一次遍历,在 ReferenceQueue 中的弱引用所对应的 key 将从 retainedKeys 中移除,说明其没有内存泄漏。
- 如果 activity 没有被回收,先强制进行一次 gc,再来检查,如果 key 还存在 retainedKeys 中,说明 activity 不可回收,同时也说明了出现了内存泄漏。
- 发生内存泄露之后,dump内存快照,分析 hprof 文件,找到泄露路径(使用 haha 库分析),发送到通知栏