引用计数为主,标记清除和分代回收为辅。
标记清除是为了解决引用计数难以解决的循环引用问题。
分代回收使用空间换取时间,提高垃圾回收的效率。
1 内存分配
查看对象占用内存字节大小使用到sys模块的getsizeof()方法。
getsizeof方法只计算对象直接占用的内存,而不计算对象内所引用对象的内存。
空对象分配的内存并不为空!
import sys
a = None
b = ()
c = ""
d = []
e = set()
f = dict()
print(sys.getsizeof(a))
print(sys.getsizeof(b))
print(sys.getsizeof(c))
print(sys.getsizeof(d))
print(sys.getsizeof(e))
print(sys.getsizeof(f))
结果
16
48
51
64
224
240
除了None对象外,其他空对象都是容器,可以理解为创建这个容器本身就需要占用一定的内存,还有一部分内存是对象在初始化的时候预分配。这就是我们看到的空对象也占用这么大内存原因。
1.1 环状双向链表refchain
在python程序中创建的任何对象都会放在refchain链表中。
通用
-上一个对象
-下一个对象
-类型
-引用计数
由多个元素组成的对象:PyObject结构体+ob_size
int类型
value
list类型
items(列表元素),元素个数
2 引用计数
结构体中ob_refcnt,即引用计数器,值默认为1.
sys.getrefcount()
调用该函数会自身创建一个临时的引用,因此返回值会比期望多1.
记录所使用的对象各有多少引用。
当一个对象被创建,引用计数为1.
当对象的引用计数为0,则会被当做垃圾回收。
-对象从refchain中移除
-将对象销毁,内存归还
2.1 增加引用计数
-创建对象
-同一个对象被赋值给其他变量
-作为参数传递给函数、方法、类实例
-被赋值为一个容器对象的成员
容器对象
可以保留指向其他对象的应用的对象就是容器对象。
代表:元组,列表,字典。
非容器对象有字符串和数值等,这些对象不能保留指向其他对象的引用。
2.2 引用计数减少
-离开其作用范围,比如函数、方法、类实例结束
-对象别名被显式销毁(变量名对应的对象引用计数减1)
del a
-对象从一个容器对象中移除
pop(),remove()
-容器对象本身被销毁
del mylist
import sys
a = "mm"
print(sys.getrefcount("mm"))
b = a
print(sys.getrefcount("mm"))
t = id("mm")
print(sys.getrefcount("mm"))
tmp = [1,2,"mm"]
print(sys.getrefcount("mm"))
tmp.remove(("mm"))
print(sys.getrefcount("mm"))
del b
print(sys.getrefcount("mm"))
tmp = [1,2,"mm"]
print(sys.getrefcount("mm"))
del tmp
print(sys.getrefcount("mm"))
5
6
6
7
6
5
6
5
3 标记清除
3.1 循环引用
并不是所有的Python对象都会发生循环引用。有些对象可能保留了指向其他对象的引用,这些对象可能引起循环引用。
容器对象中都被分配了用于循环引用垃圾回收的头结构体。
由于各自引用计数器=1,所以不会被回收。
但是由于变量名被销毁,以后不会再有变量指向这块内存,所以这两个链表会一直存在内存中,永远不会被销毁,造成内存泄漏。
所以只用引用计数器是远远不够的,因此引入标记清除。
3.2 标记清除
在python底层再去维护一个链表,存放可能存在循环引用的对象(列表,字典,元组,集合)
在内部某种情况下扫描可能存在循环应用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器-1,如果是0,则垃圾回收。
4 分代回收
-什么时候扫描?
-可能存在循环引用链表的扫描代价比较大
将链表元素分成3代,也就维护3个链表。
0代:0代中对象个数达到700个扫描1次。
1代:0代扫描10次,则1代扫描1次。
2代:1代扫描10次,则2代扫描1次。
最开始都加入到0代,0代中对象个数达到700,对0代进行扫描。
存在循环引用则引用计数器-1,引用计数器=0的垃圾回收,剩下的对象升级作为1代。此时1代标记0代扫描1次。
重复上述过程。
是为了提升标记清除的效率。
5 缓存机制
优化机制。
5.1 池 (int)
为了避免重复的创建和销毁常见对象,维护池。
启动解释器的时候,python内部会帮我们创建-5~257的对象。
内部不会开辟内存,直接从池中获取。
5.2 free_list
当一个对象的引用计数器为0是,按理说应该回收。
但是在python内部不会直接回收,而是将对象添加到free_list链表中当缓存。
以后再去创建对象时,不再重新开辟内存,而是直接使用free_list。
把相同数据类型的对象内部数据重新初始化就行,再放到refchain.
free_list有个数的限制。