​​

Python GC 与 Objective-C ARC

提起​​GC(Garbage Collector)​​我们首先想到的应该是​​JVM​​的​​GC​​,可是作者水平有限,​​Java​​使用的不多,了解的也不够深入。所以本文的重点将放在对​​python gc​​的解说。以及对照​​OC​​使用的​​ARC(Automatic Reference Counting)​​。

本文须要读者有​​Python​​或​​OC​​的基础,假设遇到没有解说清楚的地方。烦请自行查阅。

引用计数

由于​​Python​​和​​OC​​都使用了​​引用计数​​作为内存管理的一种手段,所以先介绍一下​​引用计数​​。

​引用计数​​是一种非常easy的追踪内存中对象的技术,能够这样想象。每一个对象都有一个内部的变量称为​​引用计数器​​。这个​​引用计数器​​记录了每一个对象有多少个引用,我们称为​​引用计数​​。当一个对象创建或者被赋值给其它变量时就会添加​​引用计数​​,当对象不再被使用或手动释放时就会降低​​引用计数​​。当​​引用计数​​为0时也就表示没有变量指向该对象,程序也无法使用该对象。因此须要被回收。

在介绍​​Python​​的引用计数之前先普及一下常识。​​python​​中一切都是对象,对象赋值、函数參数传递都採用传引用而不是传值(也能够理解为传值,可是这个值不是对象的内容值而是对象的地址值),有些读者可能受到一些博客的影响会觉得在传递数字类型或字符串类型时是传值而不是传址。看例如以下代码:

def swap(x, y):
temp = x
x = y
y = temp

if __name__ == '__main__':
a = 1
b = 2
swap(a, b)
print(a, b)

x = 'Jiaming Chen'
y = 'Zhouhang Wan'
swap(x, y)
print(x, y)

m = (1, 2)
n = (3, 4)
swap(m, n)
print(m, n)

python2.7 output:
(1, 2)
('Jiaming Chen', 'Zhouhang Wan')
((1, 2), (3, 4))

python3.5 output:
1, 2
'Jiaming Chen' 'Zhouhang Wan'
(1, 2) (3, 4)


非常多读者觉得上述代码执行了​​swap​​函数以后并没有交换实參的值,因此觉得​​python​​在对数字类型、字符串类型或元组类型这种參数是採用传值的方式进行的,实际上这是错误的理解。要记住​​python​​中一切都是对象。全部的參数传递也都是传递引用即传址而不是传值,再看例如以下代码:

def swap(x, y):
print('2: ', id(x), id(y))
temp = x
x = y
y = temp
print('3: ', id(x), id(y))

if __name__ == '__main__':
a = 1
b = 2
print('1: ', id(a), id(b))
swap(a, b)
print(a, b)
print('4: ', id(a), id(b))

python2.7 output:
('1: ', 140256869373448, 140256869373424)
('2: ', 140256869373448, 140256869373424)
('3: ', 140256869373424, 140256869373448)
(1, 2)
('4: ', 140256869373448, 140256869373424)

python3.5 output:
1: 4449926112 4449926144
2: 4449926112 4449926144
3: 4449926144 4449926112
1 2
4: 4449926112 4449926144


​id​​函数能够输出一串数字。能够理解为对象在内存中的地址,我们发如今调用​​swap​​函数之前、调用以后以及在进入​​swap​​函数时实參和形參的地址都是一致的,可是在交换以后地址变了,这就牵扯到​​python​​的​​更新模型​​,​​python​​的​​更新模型​​分为两种。​​可更新​​与​​不可更新​​,​​可更新​​顾名思义就是指这个对象的值是能够改动的,而​​不可更新​​则是对象的值不能够改动,假设确实要改动​​python​​会为你创建一个新的对象,这样就解释上述代码,在​​swap​​函数中。数字类型的变量是​​不可更新​​的,因此在交换数值的时候​​python​​发现你改动了不可更新对象的值就会创建一个新的对象供你使用。​​不可更新​​的类型包含:数字类型(整型、浮点型)、字符串类型、元祖类型。那​​可更新模型​​就是列表和字典类型,当你改动​​可更新模型​​对象的值时​​python​​不会为你创建新的对象,有兴趣的读者能够自行实验一下。

上面讲了这么多就是为了阐述一条:​​python中一切都是对象,传參都是传递引用​​。

再回过头介绍​​引用计数​​,能够添加引用计数的情况就包含了:创建新的对象、将对象赋给还有一个变量、函数传參、作为列表、元组的成员或是作为字典的key或value。这些情况下就会添加​​引用计数​​。

降低​​引用计数​​的情况就包含了:使用​​del​​关键字显示销毁一个对象、其它对象赋值给一个变量、函数执行结束、从列表、元祖中删除或是该列表、元祖总体被删除、从字典中被删除或key被替换或是整个字典被删除。

​OC​​的​​引用计数​​与​​python​​相似,由于​​OC​​是​​C语言​​的超集,我们能够在​​OC​​中使用C语言基本数据类型比方:​​int​​、​​float​​等,还包含一些​​Foundation框架​​中定义的结构体如:​​CGRect​​、​​CGPoint​​等,这些类型都是值类型因此在赋值或传參的时候都会拷贝一份来传递就不涉及​​引用计数​​。而其它的类类型在声明或定义时都是声明一个指针如​​NSString *s;​​这种对象就会採用​​引用计数​​来管理内存,添加或降低​​引用计数​​的情况与​​python​​的相似,由于篇幅问题就不展开解说。

自己主动引用计数 Automatic Reference Counting

自己主动引用计数​​ARC​​是由苹果开发的,实际是在​​MRC(Manual Reference Counting)​​的基础上通过编译器来实现的,在​​MRC​​时代我们须要使用​​retain​​方法来保留一个对象从而添加对象的引用计数,使用​​release​​方法来释放一个对象从而降低对象的引用计数。而且使用​​NSAutoreleasePool​​来管理,可是在​​ARC​​来到以后我们能够全然忽略这些方法,​​LLVM​​会在编译的时候帮我们完毕上述操作。​​LLVM​​会自己主动在须要的地方插入上述代码,因此程序猿全然解放了。

下面是官方的一段解释:

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C 
objects. Rather than having to think about retain and release operations, ARC allows you to concentrate on the
interesting code, theobject graphs, and the relationships between objects in your application


通过对​​ARC​​原理的简要分析我们能够发现:

1、​​ARC​​是在编译期实现的技术,在​​编译期​​就已经将​​retain​​、​​release​​这种代码插入到了源代码中进行编译。而不是在执行时​​runtime​​开辟一个单独的线程来实现。

2、程序猿不再像​​MRC​​时代那样须要手动管理引用计数,不须要自行编写​​retain​​、​​release​​方法的调用,而全然交由​​LLVM​​管理。

3、全部的属性​​property​​不再使用​​retain​​这种修饰符来修饰,取而代之的则是​​strong​​和​​weak​​。

4、不再使用​​NSAutoreleasePool​​改用​​@autoreleasepool​​。

通过分析能够发现​​ARC​​的下面优点:

1、​​ARC​​是编译期技术而不是执行时,因此程序会稳定执行,当对象没有被使用时会马上释放,不会像​​GC​​那样执行时间长了以后内存占满了须要停下整个程序来清理内存,这也是为什么Android比iOS卡顿的原因吧。

2、不须要手动编写​​retain​​、​​release​​这个方案。彻底解放了程序猿。降低发生野指针错误,也降低了没有释放内存的可能。

相同的编写过​​OC​​的同学也应该知道​​ARC​​最大的缺点就是须要自己解决​​引用循环​​的问题,因此採用​​GC​​解决内存管理的语言学习上更加简单,比方​​python​​尽管也使用了​​引用计数​​但同一时候也使用了​​GC​​从而有效的攻克了​​引用循环​​的问题(下文会介绍)因此全然不须要考虑内存管理的问题,​​Java​​也是如此。程序猿全然不须要考虑这种问题。而编写​​OC​​时程序猿须要时刻小心​​引用循环​​的产生。

关于​​OC​​循环引用的详细形式以及解决方式本文不再赘述了。有兴趣的读者能够自行查阅或​

垃圾回收器

通过前面的介绍能够看出​​OC​​採用的​​ARC​​尽管在原理上非常简洁明了。可是在实际使用中仍然会出现​​引用循环​​的问题,​​引用循环​​处理的不好会导致内存泄露以及野指针错误直接导致程序崩溃,因此,使用​​ARC​​时一定要防止​​引用循环​​的产生。

​Garbage Collection​​则是还有一种内存管理的方式,​​GC​​在原理上就比較复杂了。可是在使用中,程序猿差点儿不须要知道它的不论什么细节,由于它会自己主动帮你处理好一切。

与​​ARC​​不同的是,​​GC​​并不是在编译期实现,而是在执行期​​runtime​​单独开辟一个线程来处理的,​​GC​​实际就是一个代码段,在它觉得须要执行的时候就会去执行这段代码。这就要求​​GC​​回收内存的时候一定要速度非常快。尽可能少的去影响程序正常执行,因此须要在时间、空间以及执行频率上进行一个折中的处理,还有就是对于回收的内存可能会产生内存碎片,对内存碎片的处理也非常重要。

GC的特点

concurrency VS stop-the-world

​GC​​发展的非常快,对于各种性能瓶颈也有了非常多的解决方式,比方​​GC​​通常採用​​stop-the-world​​的方式来执行。也就是当​​GC​​须要回收内存时就会停下正常执行的程序来处理内存回收,这就导致程序卡顿,可是这种优点就是处理起来更便捷。由于整个程序被停止了,堆区和栈区的变量也不会发生不论什么改变。对于内存回收来说更加简单了。也有​​GC​​採用并发的方式来执行内存回收的操作。可是并发时堆区和栈区的变量有可能会发生变化。这对​​GC​​来说就非常复杂了。

compact VS not compact and copy

​GC​​在将不再使用的对象所占内存清理之后就会将内存进行压缩处理,相似于文件系统压缩硬盘存储一样。​​GC​​会将全部仍在使用的对象放在一起,将剩下的内存进行清除处理,这样就能够节约内存,而且再次分配内存时能够更快,当然缺点也非常明显,就是须要进行内存的移动操作,假设不进行压缩而是直接分配不使用的内存尽管回收速度会快可是分配速度相比会慢,而且也会浪费一部分内存。还有一种方法就是使用​​copy​​操作,将仍然须要使用的对象都拷贝到还有一个内存块,这样之前的内存块就能够整块进行清除处理。有点同压缩处理一样,可是缺点也非常明显就是会占用太多内存。

分代回收

​分代回收​​就是指,将内存分为多个代(generation),比方最常见的就是分为​​young区​​和​​old区​​事实上还有一个永久区,比方​​python​​使用的​​分代回收​​就分为了​​0 1 2​​三代,依照对象的生存期把对象分配在不同的​​代​​中,而且每一个​​代​​的回收策略也不同。之所以这样做是由于经过大量研究发现了一个事实:大部分对象的生存期都非常短,也就是说大部分的对象在创建不久以后就 不再使用了。因此,较小对象最初被分配在​​young区​​,假设是非常大的对象可能初次创建就直接被分配在​​old区​​,而且​​young区​​的​​GC​​执行频率更高。而且​​young区​​的对象相比​​old区​​更小。假设经过几轮的​​GC​​操作​​young区​​的对象仍然存在就会被分配到​​old区​​了。​​old区​​的​​GC​​执行频率相对较低。而且​​old区​​的对象通常比較大,当真正须要回收的时候就会导致回收效率较低。

前面介绍了​​young区​​的大部分对象由于生存期短而且对象较小,经过数次​​GC​​内存回收操作以后大部分对象都会被销毁。因此在​​young区​​採用的回收算法通常採用​​Copying​​算法。​​young区​​的一般被分为三个部分。一个​​Eden​​,两个​​Survivor​​部分即​​From​​和​​To​​,例如以下图所看到的:

Python Garbage Collection 与 Objective-C ARC_内存碎片

通过名字就能够看出来,大部分对象创建以后就会被分配在​​young区​​的​​Eden​​部分,毕竟是叫伊甸园嘛,小对象的天堂,大对象就直接被分配在​​old区​​了,而​​Copying​​算法就是当​​young区​​进行​​GC​​操作时会将​​Eden​​部分中须要销毁的对象销毁掉。然后将​​Eden​​和​​From​​中仍存活的对象拷贝到​​To​​部分中,然后将​​From​​和​​To​​交换地址,也就是​​From​​变成了​​To​​。​​To​​变成了​​From​​。

前面也讲了​​old区​​中存放的都是较大的对象而且常常须要使用的,假设还採用​​Copying​​算法可能每次须要复制一大半的对象,这样明显会导致性能下降。因此​​old区​​採用了​​标记-清除(Mark-Sweep)​​算法,基本原理就是将不再使用的对象先标记(Mark)然后再回收(Sweep),仍然须要使用的对象就不会被马克。可是这样会产生一个问题,前面​​young区​​採用复制的方式进行清理就不会产生内存碎片,而​​old区​​就会产生内存碎片。因此须要使用到前文介绍的​​Compact​​方法进行内存压缩处理。这也就导致了​​old区​​效率低的原因。

为了解决​​ARC​​存在的​​引用循环​​问题。​​GC​​中有一个​​可达(reachable)​​和​​不可达(unreachable)​​的概念。由于​​堆​​中的内存须要依赖​​栈​​中存储的指针才干够訪问,因此​​GC​​觉得​​栈​​区的变量以及全局变量的变量都是有效的。通过这些变量去寻找其它对象,假设找到了就是​​可达reachable​​的,那就说明这个对象仍然有引用是须要被保留下来的,假设没有找到就标记是​​不可达unreachable​​的,当递归的遍历完了全部的有效变量就能够标记出全部的​​不可达unreachable​​对象进行回收,这样就完美的攻克了​​引用循环​​的问题,​​Java​​和​​C#​​就採用相似这种策略。

Python的GC

​python​​使用​​引用计数​​以及​​分代回收​​来管理内存,可是在解决​​引用循环​​的问题上并没有採用​​可达性​​的方式来解决。

考虑例如以下代码:

if __name__ == '__main__':
x = []
y = []
x.append(y)
y.append(x)

a = []
b = []
c = []
d = 'Jiaming Chen'
c.append(d)
b.append(c)
a.append(b)
a.append(c)


非常明显的上述代码中​​x​​与​​y​​两个​​list​​构成了​​引用循环​​环,详细的引用关系例如以下图所看到的:

Python Garbage Collection 与 Objective-C ARC_内存碎片_02

​python​​为了解决​​引用​​循环的问题,会复制每一个对象的​​引用计数​​。而且遍历每一个对象。比方对于对象​​m​​,会找到全部它引用的对象​​n​​然后将​​n​​的​​引用计数​​减1,这样。当全部对象都遍历完之后对于引用计数不为0的对象以及这些对象所引用的子孙对象都会被保留。剩余的对象会被清除。例如以下图所看到的:

Python Garbage Collection 与 Objective-C ARC_内存碎片_03

总结

本文主要作为一篇科普文章,没有深入​​python​​代码。或是其它​​GC​​的代码来解说。主要解说实现原理,水平不高,有疑问还可共同探讨。

备注

由于作者水平有限,难免出现纰漏,如有问题还请指教。