转载请注明出处 http://blog.csdn.net/u014205968/article/details/69389363

Python GC 与 Objective-C ARC

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

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

引用计数

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

引用计数是一种非常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相似,由于OCC语言的超集,我们能够在OC中使用C语言基本数据类型比方:intfloat等,还包含一些Foundation框架中定义的结构体如:CGRectCGPoint等,这些类型都是值类型因此在赋值或传參的时候都会拷贝一份来传递就不涉及引用计数。而其它的类类型在声明或定义时都是声明一个指针如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是在编译期实现的技术,在编译期就已经将retainrelease这种代码插入到了源代码中进行编译。而不是在执行时runtime开辟一个单独的线程来实现。
2、程序猿不再像MRC时代那样须要手动管理引用计数,不须要自行编写retainrelease方法的调用,而全然交由LLVM管理。
3、全部的属性property不再使用retain这种修饰符来修饰,取而代之的则是strongweak
4、不再使用NSAutoreleasePool改用@autoreleasepool

通过分析能够发现ARC的下面优点:
1、ARC是编译期技术而不是执行时,因此程序会稳定执行,当对象没有被使用时会马上释放,不会像GC那样执行时间长了以后内存占满了须要停下整个程序来清理内存,这也是为什么Android比iOS卡顿的原因吧。
2、不须要手动编写retainrelease这个方案。彻底解放了程序猿。降低发生野指针错误,也降低了没有释放内存的可能。

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

关于OC循环引用的详细形式以及解决方式本文不再赘述了。有兴趣的读者能够自行查阅或者參考文章iOS block探究(一): 基础详细解释

垃圾回收器

通过前面的介绍能够看出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部分即FromTo,例如以下图所看到的:

Young区结构

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

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

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

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)

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

初始引用关系

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

GC后引用关系

总结

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

备注

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