粗谈ARC自动引用计数和GC垃圾回收
1. ARC 自动引用计数
自动引用计数(Automatic Reference Count
简称 ARC
),是苹果在 WWDC 2011 年大会上提出的用于内存管理的技术。虽然 ARC
极大地简化了我们的内存管理工作,但是引用计数这种内存管理方案如果不被理解,那么就无法处理好那些棘手的循环引用问题。
引用计数(Reference Count
)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1
,当有一个新的指针指向这个对象时,我们将其引用计数加 1
,当某个指针不再指向这个对象是,我们将其引用计数减 1
,当对象的引用计数变为 0
时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C
语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt)等语言也提供了基于引用计数的内存管理方式。
引用计数这种内存管理方式虽然简单,但是手工写大量的操作引用计数的代码不但繁琐,而且容易被遗漏。于是苹果在 2011 年引入了 ARC
。ARC
顾名思义,是自动帮我们填写引用计数代码的一项功能。
ARC
的想法来源于苹果在早期设计 Xcode
的 Analyzer
的时候,发现编译器在编译时可以帮助大家发现很多内存管理中的问题。后来苹果就想,能不能干脆编译器在编译的时候,把内存管理的代码都自动补上,带着这种想法,苹果修改了一些内存管理代码的书写方式(例如引入了 @autoreleasepool
关键字)后,在 Xcode
中实现了这个想法。
ARC
的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC
是工作在编译期的一种技术方案,这样的好处是:
编译之后,ARC
与非 ARC
代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc
来关闭部分源代码的 ARC
特性。
相对于垃圾回收这类内存管理方案,ARC
不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC
能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1
的操作,之后又紧接着有一个 -1
的操作,那么编译器就可以把这两个操作都优化掉。
但是也有人认为,ARC
也附带有运行期的一些机制来使 ARC
能够更好的工作,他们主要是指 weak
关键字。weak
变量能够在引用计数为 0
时被自动设置成 nil
,显然是有运行时逻辑在工作的。我通常并没有把这个算在 ARC
的概念当中,当然,这更多是一个概念或定义上的分歧,因为除开 weak
逻辑之外,ARC
核心的代码都是在编译期填充的。
2. GC 垃圾回收
Android
手机通常使用 Java
来开发,而 Java
是使用垃圾回收这种内存管理方式。 那么,ARC
和垃圾回收对比,有什么优点和缺点?
虽然做 iOS
开发并不需要用到垃圾回收这种内存管理机制。但是垃圾回收被使用得非常普遍,不但有 Java
,还包括 JavaScript
, C#
,Go
等语言。
垃圾回收简介
垃圾回收(Garbage Collection
,简称 GC
)这种内存管理机制最早由图灵奖获得者 John McCarthy 在 1959 年提出,垃圾回收的理论主要基于一个事实:大部分的对象的生命期都很短。
所以,GC
将内存中的对象主要分成两个区域:Young
区和 Old
区。对象先在 Young
区被创建,然后如果经过一段时间还存活着,则被移动到 Old
区。(其实还有一个 Perm
区,但是内存回收算法通常不涉及这个区域)
Young
区和 Old
区因为对象的特点不一样,所以采用了两种完全不同的内存回收算法。
Young
区的对象因为大部分生命期都很短,每次回收之后只有少部分能够存活,所以采用的算法叫 Copying
算法,简单说来就是直接把活着的对象复制到另一个地方。Young
区内部又分成了三块区域:Eden
区 , From
区 , To
区。每次执行 Copying
算法时,即将存活的对象从 Eden
区和 From
区复制到 To
区,然后交换 From
区和 To
区的名字(即 From
区变成 To
区,To
区变成 From
区)。
Old
区的对象因为都是存活下来的老司机了,所以如果用 Copying
算法的话,很可能 90%
的对象都得复制一遍了,不划算啊!所以 Old
区的回收算法叫 Mark-Sweep
算法。简单来说,就是只是把不用的对象先标记(Mark
)出来,然后回收(Sweep
),活着的对象就不动它了。因为大部分对象都活着,所以回收下来的对象并不多。但是这个算法会有一个问题:它会产生内存碎片,所以它一般还会带有整理内存碎片的逻辑,在算法中叫做 Compact
。如何整理呢?早年用过 Windows
的硬盘碎片整理程序的朋友可能能理解,其实就是把对象插到这些空的位置里。这里面还涉及很多优化的细节,我就不一一展开了。
讲完主要的算法,接下来 GC
需要解决的问题就只剩下如何找出需要回收的垃圾对象了。为了避免 ARC
解决不了的循环引用问题,GC
引入了一个叫做「可达性」的概念,应用这个概念,即使是有循环引用的垃圾对象,也可以被回收掉。下面就给大家介绍一下这个概念。
当 GC
工作时,GC
认为当前的一些对象是有效的,这些对象包括:全局变量,栈里面的变量等,然后 GC
从这些变量出发,去标记这些变量「可达」的其它变量,这个标记是一个递归的过程,最后就像从树根的内存对象开始,把所有的树枝和树叶都记成可达的了。那除了这些「可达」的变量,别的变量就都需要被回收了。
听起来很牛逼对不对?那为什么苹果不用呢?实际上苹果在 OS X 10.5 的时候还真用了,不过在 10.7 的时候把 GC
换成了 ARC
。那么,GC
有什么问题让苹果不能忍,这就是:垃圾回收的时候,整个程序需要暂停,英文把这个过程叫做:Stop the World
。所以说,你知道 Android
手机有时候为什么会卡吧,GC
就相当于春运的最后一天返城高峰。当所有的对象都需要一起回收时,那种体验肯定是当时还在世的乔布斯忍受不了的。
看看下面这幅漫画,真实地展现出 GC
最尴尬的情况(漫画中提到的 Full GC
,就是指执行 Old
区的内存回收):
当然,事实上经过多年的发展,GC
的回收算法一直在被优化,人们想了各种办法来优化暂停的时间,所以情况并没有那么糟糕。
ARC
相对于 GC
的优点:
ARC
工作在编译期,在运行时没有额外开销。
ARC
的内存回收是平稳进行的,对象不被使用时会立即被回收。而 GC
的内存回收是一阵一阵的,回收时需要暂停程序,会有一定的卡顿。
ARC
相对于 GC
的缺点:
GC
真的是太简单了,基本上完全不用处理内存管理问题,而 ARC
还是需要处理类似循环引用这种内存管理问题。
GC
一类的语言相对来说学习起来更简单。