Python Garbage Collection 与 Objective-C ARCPython GC 与 Objective-C ARC

2018-04-10 11:42:48 浏览数 (1)

转载请注明出处 https://cloud.tencent.com/developer/user/1605429

Python GC 与 Objective-C ARC

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

本文需要读者有PythonOC的基础,如果遇到没有讲解清楚的地方,烦请自行查阅。

引用计数

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

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

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

代码语言:javascript复制
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中一切都是对象,所有的参数传递也都是传递引用即传址而不是传值,再看如下代码:

代码语言:javascript复制
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会自动在需要的地方插入上述代码,因此程序员完全解放了。

以下是官方的一段解释:

代码语言:javascript复制
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使用引用计数以及分代回收来管理内存,但是在解决引用循环的问题上并没有采用可达性的方式来解决。考虑如下代码:

代码语言:javascript复制
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的代码来讲解,主要讲解实现原理,水平不高,有疑问还可共同探讨。

备注

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

0 人点赞