漫谈内存泄漏

作者: sim_cai | 来源:发表于2017-09-24 21:49 被阅读227次

    前言

    最近看了同事整理的一份与内存泄漏相关思维导图。突然想从内存泄漏的角度探讨一下与内存相关的话题。
    什么是内存泄漏?然而我又问自己一个问题, malloc 的内存到底是什么?

    什么是内存

    在计算机系统中,我们谈论的内存通常是指DRAM

    虚拟内存空间

    而当我们程序在系统上运行起来时,操作系统为我们提供了一个假象,程序看起来是独占使用处理器、主存和I/O设备的。这种假象是通过进程的概念来实现的。

    虚拟内存,也为进程提供一个假象,每个进程都独占地使用主存。每个进程看到的内存都是一致的,称为虚拟内存空间

    虚拟内存的能力:

    • 使主存中只保存活动区域,根据需要在磁盘和主存之间来回传输数据。
    • 为每个内存提供一致的地址空间。
    • 保护了每个进程的地址空间不被其他进程破坏。
    一个 Linux 进程的虚拟内存.png

    上图是一个 Linux 进程的虚拟内存。也就是说我们平时 malloc 得到的内存地址,其实是虚拟内存的地址。

    动态内存分配

    其实系统为我们提供了 mmapmunmap 函数来创建和删除虚拟内存的区域。但很多时候直到程序实际运行才知道某些数据结构的大小。所以就有了动态内存分配器。

    动态内存分配器有两种基本类型:

    • 显式分配器,需要显式释放。例如 C 和 C++ 。
    • 隐式分配器,分配器检测已分配何时不再被程序所使用,那么就释放这个块。例如 Lisp、Java 等。

    什么是内存泄漏

    内存泄漏是常见的内存错误之一。我们知道 malloc 其实是从虚拟内存空间的堆中申请空闲的地址的。然而内存空间是有限的。程序在运行中 malloc 出来的内存空间使用完后,没有被 free 掉,这样我们就称之为内存泄漏

    ARC 机制

    iOS 上,不论是 Objective-C 还是 Swift 都是使用引用计数式的内存管理方式。
    ARC 就是 Automatic Reference Counting, 其实 ARC 很简单,我们只需要弄清楚对象之间的持有关系。

    举个简单的例子:

    场景一

    self.textField.text = @"Sim";
    

    下图简单的描述了,这段代码对象之间的持有关系。 self.textField.text 持有着 @"Sim"@"Sim" 对象的 retainCount = 1。

    1.png
    self.textField.text = @"SimCai";
    

    这时,对象 @"Sim" 不再被 self.textField.text 持有,所以 @"Sim" 对象的 retainCount = 0,对象会被释放掉。

    2.png

    场景二

    self.textField.text = @"Sim";
    NSString *firstName = self.textField.text;
    

    这段代码,@"Sim" 对象被 firstNameself.textField.text 同时持有。所以 @"Sim" 对象的 retainCount = 2 。

    3.png
    self.textField.text = @"SimCai";
    

    这时 self.textField.text 不再持有 @"Sim" 对象,但是 firstName 依然持有 @"Sim" ,所以 @"Sim" 的 retainCount = 1 ,不会被释放。

    4.png

    直到 firstName 也不再持有 @"Sim" 对象,@"Sim" 才会被释放。

    5.png

    循环引用

    ARC 整套机制看起来很简单,但会不会有什么特例,会造成内存无法被正常释放呢?
    有,循环引用

    ViewController 持有 TableView,同时 TableView 也持有 ViewController 。 他们相互有引用关系。这就是循环引用

    6.png

    为了打破这种引用的循环。我们可以通过 weak (弱引用) 来解决这个问题。

    一般情况下,ViewController 是会被个 UINavigationController 所持有。如果 TableView 也持有 ViewController ,这时 ViewController 的 retainCount = 2。

    而我们对 self.vc 使用了 weak 后,self.vc = ViewController ,这样的操作,不再会导致 retainCount 加1 。 这时 ViewController 依然还是 retainCount = 1。

    7.png

    而当 ViewController 被释放后 self.vc 建会指向 nil 。当然在这个例子,在 ViewController 释放后,TableView 自然也会别释放。

    8.png

    但在一些场景下使用 weak 需要比较注意的。例如,一个全局的定时器,如果持有了 ViewController 是弱引用。 那当 ViewController 被释放后,定时器再去访问 ViewController 就将引起 crash 。

    常见的内存泄漏场景

    • 使用时 block (需要格外小心)
    • NSTimer 没有销毁
    • KVO 没有移除
    • NSNotification 没有移除

    当了解了 ARC 后,再我看来这些本质都是循环引用问题(当然还有一些 CF 的API,还是需要手动内存管理的)。

    • block 会捕获变量
    • NSTimer 需要持有对象,进行通知回调
    • KVO 需要持有对象,进行通知回调
    • NSNotification 需要持有对象,进行通知回调

    所以这些操作都容易造成内存泄漏。

    要避免内存泄漏,更重要的是需要了解 ARC 的机制。实际上,可能造成内存泄漏的场景还有很多。

    总结

    • malloc 得到的内存地址,实际是虚拟内存空间中的堆地址。并不是实际的物理地址。
    • 内存泄漏是指,内存资源没用了,但内存资源没被 free。(用完了,就别占着坑)
    • 在 iOS ARC 时代,大部分内存泄漏问题,是由循环引用造成的。

    相关文章

      网友评论

        本文标题:漫谈内存泄漏

        本文链接:https://www.haomeiwen.com/subject/ujulextx.html