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
,例如以下图所看到的:
通过名字就能够看出来,大部分对象创建以后就会被分配在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
为了解决引用
循环的问题,会复制每一个对象的引用计数
。而且遍历每一个对象。比方对于对象m
,会找到全部它引用的对象n
然后将n
的引用计数
减1,这样。当全部对象都遍历完之后对于引用计数不为0的对象以及这些对象所引用的子孙对象都会被保留。剩余的对象会被清除。例如以下图所看到的:
总结
本文主要作为一篇科普文章,没有深入python
代码。或是其它GC
的代码来解说。主要解说实现原理,水平不高,有疑问还可共同探讨。
备注
由于作者水平有限,难免出现纰漏,如有问题还请指教。