• 深入理解Python中的内存管理和垃圾回收机制
  • 发布于 2个月前
  • 77 热度
    0 评论
  • 林顺忍
  • 3 粉丝 40 篇博客
  •   
当一门语言应用熟练以后,就要向底层钻研,搞懂其原理。如果面试大公司,比如阿里巴巴这种。他们比较喜欢问你这些底层的东西。因为应用这层,他们很难筛出人来。
不管何种语言,程序运行中都会耗费很多资源,如果有些应用完成以后,资源还不释放,运行性能就会越来越低,甚至发生OOM(out of memery)问题
无论何种垃圾收集机制, 一般都是两阶段: 垃圾检测和垃圾回收.

在Python中, 大多数对象的生命周期都是通过对象的引用计数来管理的.

问题: 但是存在循环引用的问题: a 引用 b, b 引用 a, 导致每一个对象的引用计数都不为0, 所占用的内存永远不会被回收

要解决循环引用: 必需引入其他垃圾收集技术来打破循环引用. Python中使用了标记-清除以及分代收集即, Python 中垃圾回收机制: 引用计数(主要), 标记清除, 分代收集(辅助)

引用计数, 意味着必须在每次分配和释放内存的时候, 加入管理引用计数的动作。
引用计数的优点: 最直观最简单, 实时性, 任何内存, 一旦没有指向它的引用, 就会立即被回收。

Python中,主要依靠gc(garbage collector)模块的引用计数技术来进行垃圾回收。所谓引用计数,就是考虑到Python中变量的本质不是内存中一块存储数据的区域,而是对一块内存数据区域的引用。所以python可以给所有的对象(内存中的区域)维护一个引用计数的属性,在一个引用被创建或复制的时候,让python,把相关对象的引用计数+1;相反当引用被销毁的时候就把相关对象的引用计数-1。当对象的引用计数减到0时,自然就可以认为整个python中不会再有变量引用这个对象,所以就可以把这个对象所占据的内存空间释放出来了。

引用计数技术在每次引用创建和销毁时都要多做一些操作,这可能是一个小缺点,当创建和销毁很频繁的时候难免带来一些效率上的不足。但是其最大的好处就是实时性,其他语言当中,垃圾回收可能只能在一些固定的时间点上进行,比如当内存分配失败的时候进行垃圾回收,而引用计数技术可以动态地进行内存的管理。

一、变量与对象

关系图如下:

1、变量,通过变量指针引用对象

变量指针指向具体对象的内存空间,取对象的值。

2、对象,类型已知,每个对象都包含一个头部信息(头部信息:类型标识符和引用计数器)

注意:变量名没有类型,类型属于对象(因为变量引用对象,所以类型随对象),变量引用什么类型的对象,变量就是什么类型的。
>>> a=123
>>> b=a
>>> id(a)
4372738752
>>> id(b)
4372738752
>>> a=456
>>> id(a)
4374261616
>>> id(b)
4372738752
PS:id()是python的内置函数,用于返回对象的身份,即对象的内存地址。
>>> var1=object
>>> var2=var1
>>> id(var1)
4372473808
>>> id(var2)
4372473808
3、引用所指判断

通过is进行引用所指判断,is是用来判断两个引用所指的对象是否相同。

整数
>>> a=1
>>> b=1
>>> print(a is b)
True
短字符串
>>> c="good"
>>> d="good"
>>> print(c is d)
True
长字符串
>>> e="very good"
>>> f="very good"
>>> print(e is f)
False
列表
>>> g=[]
>>> h=[]
>>> print(g is h)
False
由运行结果可知:

1、Python缓存了整数和短字符串,因此每个对象在内存中只存有一份,引用所指对象就是相同的,即使使用赋值语句,也只是创造新的引用,而不是对象本身;
2、Python没有缓存长字符串、列表及其他对象,可以由多个相同的对象,可以使用赋值语句创建出新的对象。

二、引用计数

在Python中,每个对象都有指向该对象的引用总数---引用计数

查看对象的引用计数:sys.getrefcount()
In [2]: import sys
In [3]: a=[1,2,3]
In [4]: getrefcount(a)
Out[4]: 2
In [5]: b=a
In [6]: getrefcount(a)
Out[6]: 3
In [7]: getrefcount(b)
Out[7]: 3

Python的一个容器对象(比如:表、词典等),可以包含多个对象。
In [12]: a=[1,2,3,4,5]
In [13]: b=a

In [14]: a is b
Out[14]: True
In [15]: a[0]=6   
In [16]: a
Out[16]: [6, 2, 3, 4, 5]

In [17]: a is b
Out[17]: True
In [18]: b
Out[18]: [6, 2, 3, 4, 5]


由上可见,实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

3、引用计数增加
1、对象被创建
2、另外的别人被创建
3、作为容器对象的一个元素
4、被作为参数传递给函数:foo(x)

4、引用计数减少
1、对象的别名被显式的销毁
2、对象的一个别名被赋值给其他对象
3、对象从一个窗口对象中移除,或,窗口对象本身被销毁
4、一个本地引用离开了它的作用域,比如上面的foo(x)函数结束时,x指向的对象引用减1。

引用计数法有很明显的优点:
高效
运行期没有停顿 可以类比一下Ruby的垃圾回收机制,也就是 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
对象有确定的生命周期

易于实现


原始的引用计数法也有明显的缺点:

维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比,而不像mark and sweep等基本与回收的内存数量有关。
无法解决循环引用的问题。A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收。
循环引用的示例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)

当Python中的对象越来越多,占据越来越大的内存,启动垃圾回收(garbage collection),将没用的对象清除。
为了解决这两个致命弱点,Python又引入了以下两种GC机制。

标记-清除的回收机制
针对循环引用这个问题,比如有两个对象互相引用了对方,当外界没有对他们有任何引用,也就是说他们各自的引用计数都只有1的时候,如果可以识别出这个循环引用,把它们属于循环的计数减掉的话,就可以看到他们的真实引用计数了。基于这样一种考虑,有一种方法,比如从对象A出发,沿着引用寻找到对象B,把对象B的引用计数减去1;然后沿着B对A的引用回到A,把A的引用计数减1,这样就可以把这层循环引用关系给去掉了。

不过这么做还有一个考虑不周的地方。假如A对B的引用是单向的, 在到达B之前我不知道B是否也引用了A,这样子先给B减1的话就会使得B称为不可达的对象了。为了解决这个问题,python中常常把内存块一分为二,将一部分用于保存真的引用计数,另一部分拿来做为一个引用计数的副本,在这个副本上做一些实验。比如在副本中维护两张链表,一张里面放不可被回收的对象合集,另一张里面放被标记为可以被回收(计数经过上面所说的操作减为0)的对象,然后再到后者中找一些被前者表中一些对象直接或间接单向引用的对象,把这些移动到前面的表里面。这样就可以让不应该被回收的对象不会被回收,应该被回收的对象都被回收了。

分代回收
分代回收策略着眼于提升垃圾回收的效率。研究表明,任何语言,任何环境的编程中,对于变量在内存中的创建/销毁,总有频繁和不那么频繁的。比如任何程序中总有生命周期是全局的、部分的变量。
Python将所有的对象分为0,1,2三代;
所有的新建对象都是0代对象;
当某一代对象经历过垃圾回收,依然存活,就被归入下一代对象。

gc模块提供一个接口给开发者设置垃圾回收的选项。上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能就是解决循环引用的问题。

常用函数:
gc.set_debug(flags)
设置gc的debug日志,一般设置为gc.DEBUG_LEAK
gc.collect([generation])
显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,
2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。
返回不可达(unreachable objects)对象的数目
gc.set_threshold(threshold0[, threshold1[, threshold2])
设置自动执行垃圾回收的频率。
gc.get_count()
获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表
gc模块的自动垃圾回收机制
必须要import gc模块,并且is_enable()=True 才会启动自动垃圾回收。
这个机制的主要作用就是发现并处理不可达的垃圾对象。
垃圾回收=垃圾检查+垃圾回收
在Python中,采用分代收集的方法。把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾检查中,改对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中。

gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取。

现在电脑的资源已经没有那么紧张了,很多语言不像C语言那样,要手动释放资源,不然很容易出现野指针。Java, Python内存都是自动管理,自动回收垃圾。
如果我们知道这些原理,在写敲代码的时候,就会避免一些陷阱,能够提高程序性能。

如果垃圾回收不是那么好理解,我们可以用现实生活中的例子来联想。
又提到了开发商来拍地。
每拍一快地,房管局都要登记一下,这个开发商捂地的数量又加一。如果这块地使用完了,开发商捂地的数目减一。这便是引用计数。
如果这块地,一女二嫁,有纠纷。陷入循环死结了。政府就各个击破,请他们喝茶,请他们都退出。这个就是标记-清除。
如果这块地的开发商后台特别强大,捂着就是不开发。地方政府就只能采取一贯的做法,“让后代去解决”。这就是分代回收。
用户评论