转载请注明出处 https://cloud.tencent.com/developer/user/1605429
提起GC(Garbage Collector)
我们首先想到的应该是JVM
的GC
,但是作者水平有限,Java
使用的不多,了解的也不够深入,所以本文的重点将放在对python gc
的讲解,以及对比OC
使用的ARC(Automatic Reference Counting)
。
本文需要读者有Python
或OC
的基础,如果遇到没有讲解清楚的地方,烦请自行查阅。
因为Python
和OC
都使用了引用计数
作为内存管理的一种手段,所以先介绍一下引用计数
。
引用计数
是一种非常简单的追踪内存中对象的技术,可以这样想象,每一个对象都有一个内部的变量称为引用计数器
,这个引用计数器
记录了每个对象有多少个引用,我们称为引用计数
。当一个对象创建或者被赋值给其他变量时就会增加引用计数
,当对象不再被使用或手动释放时就会减少引用计数
,当引用计数
为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
的类似,由于篇幅问题就不展开讲解。
自动引用计数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
循环引用的具体形式以及解决方案本文不再赘述了,有兴趣的读者可以自行查阅或者参考文章iOS block探究(一): 基础详解
通过前面的介绍可以看出OC
采用的ARC
虽然在原理上很简洁明了,但是在实际使用中仍然会出现引用循环
的问题,引用循环
处理的不好会导致内存泄露以及野指针错误直接导致程序崩溃,因此,使用ARC
时一定要防止引用循环
的产生。
Garbage Collection
则是另一种内存管理的方式,GC
在原理上就比较复杂了,但是在使用中,程序员几乎不需要知道它的任何细节,因为它会自动帮你处理好一切。与ARC
不同的是,GC
并非在编译期实现,而是在运行期runtime
单独开辟一个线程来处理的,GC
实际就是一个代码段,在它认为需要执行的时候就会去执行这段代码,这就要求GC
回收内存的时候一定要速度很快,尽可能少的去影响程序正常运行,因此需要在时间、空间以及运行频率上进行一个折中的处理,还有就是对于回收的内存可能会产生内存碎片,对内存碎片的处理也很重要。
GC
发展的很快,对于各种性能瓶颈也有了很多的解决方案,比如GC
通常采用stop-the-world
的方式来执行,也就是当GC
需要回收内存时就会停下正常运行的程序来处理内存回收,这就导致程序卡顿,但是这样的好处就是处理起来更便捷,因为整个程序被停止了,堆区和栈区的变量也不会发生任何改变,对于内存回收来说更加简单了。也有GC
采用并发的方式来执行内存回收的操作,但是并发时堆区和栈区的变量有可能会发生变化,这对GC
来说就很复杂了。
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区结构
通过名字就可以看出来,大部分对象创建以后就会被分配在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
使用引用计数
以及分代回收
来管理内存,但是在解决引用循环
的问题上并没有采用可达性
的方式来解决。考虑如下代码:
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的对象以及这些对象所引用的子孙对象都会被保留,剩余的对象会被清除。如下图所示:
GC后引用关系
本文主要作为一篇科普文章,没有深入python
代码,或是其他GC
的代码来讲解,主要讲解实现原理,水平不高,有疑问还可共同探讨。
由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。