赋值语句内存分析
我们可以通过使用id()函数来查看某个对象的内存地址,每个人的电脑内存地址不一样。
a = 1 id(a) # Output: 4566652048 b = 2 id(b) # Output: 4566652080 c = 8 id(c) # Output: 4566652272 d = 8 id(d) # Output: 4566652272
使用 ==来查看对象的值是否相等,is判断对象是否是同一个对象
c == d # Output: True c is d # Output: True e = 888888888 id(e) # Output: 4569828784 f = 888888888 id(f) # Output: 4569828880 e == f # Output: True e is f # Output: False
解释:我们可以看到,
c == d输出 True 和c is d也输出 True,这是因为,对一个小一点的 int 变量赋值,Python 在内存池(Pool)中分配给 c 和 d 同一块内存地址,- 而
e == f为 True,值相同;e is f输出 False,并不少同一个对象。
这是因为 Python 内存池中分配空间,赋予对象的类别并赋予其初始的值。从-5 到 256 这些小的整数,在 Python 脚本中使用的非常频繁,又因为他们是不可更改的,因此只创建一次,重复使用就可以了。
e 和 f 数字比较大,所以只能重新分配地址来。其实-5 到 256 之间的数字,Python 都已经给我安排好了。
>>> i = 256 >>> j = 256 >>> i is j True >>> i = 257 >>> j = 257 >>> i is j False >>> i = -5 >>> j = -5 >>> i is j True >>> i = -6 >>> j = -6 >>> i is j False
接着,看对象的内存分析:
li1 = [] li2 = [] li1 == li2 # Output: True li1 is li2 # Output: False x = 1 y = x id(x) # Output: 4566652048 id(y) # Output: 4566652048 y = 2 id(y) # Output: 4566652080 x == y # Output: False x is y # Output: False
再来看看垃圾回收
垃圾回收机制
来看一下 Python 中的垃圾回收技术:
- 引用计数为主
- 标记清除和分代回收为辅
如果一个对象的引用计数为 0,Python 解释器就会回收这个对象的内存,但引用计数的缺点是不能解决循环引用的问题,所以我们需要标记清除和分代回收。
什么是引用计数
- 每个对象都有存有指向该对象的引用总数
- 查看某个对象的引用计数
sys.getrefcount() - 可以使用 del 关键字删除某个引用
import sys l = [] print(sys.getrefcount(l)) # Output: 2 l2 = l l3 = l l4 = l3 print(sys.getrefcount(l)) # Output: 5 del l2 print(sys.getrefcount(l)) # Output: 4 i = 1 print(sys.getrefcount(i)) # Output: 140 a = i print(sys.getrefcount(i)) # Output: 141
当对象的引用计数达到零时,解释器会暂停,来取消分配它以及仅可从该对象访问的所有对象。即满足引用计数为 0 的时候,会启动垃圾回收。
但是引用计数不能解决循环引用的问题,就如下的代码不停跑就能把电脑内存跑满:
>>> a = [] >>> b = [] >>> while True: ... a.append(b) ... b.append(a) ... [1] 31962 killed python
标记清除
标记清除算法作为 Python 的辅助垃圾收集技术主要处理的是一些容器对象,比如 list、dict、tuple,instance 等,因为对于字符串、数值对象是不可能造成循环引用问题。标记清除和分代回收就是为了解决循环引用而生的。
它分为两个阶段:第一阶段是标记阶段,GC 会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
在上图中,可以从程序变量直接访问块 1,并且可以间接访问块 2 和 3。程序无法访问块 4 和 5。第一步将标记块 1,并记住块 2 和 3 以供稍后处理。第二步将标记块 2,第三步将标记块 3,但不记得块 2,因为它已被标记。扫描阶段将忽略块 1,2 和 3,因为它们已被标记,但会回收块 4 和 5。
标记清除算法作为 Python 的辅助垃圾收集技术,主要处理的是一些容器对象,比如 list、dict、tuple 等,因为对于字符串、数值对象是不可能造成循环引用问题。
Python 使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
分代回收(自动)
分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。
- Python 将所有的对象分为 0,1,2 三代
- 所有的新建的对象都是 0 代对象
- 当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。
同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为 Python 的辅助垃圾收集技术处理那些容器对象。
Python 运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。
当两者的差值高于某个阈值时,垃圾回收才会启动
- 查看阈值 gc.get_threshold()
import gc print(gc.get_threshold()) # Output: (700, 10, 10)
get_threshold()返回的(700, 10, 10)返回的两个 10。也就是说,每 10 次 0 代垃圾回收,会配合 1 次 1 代的垃圾回收;而每 10 次 1 代的垃圾回收,才会有 1 次的 2 代垃圾回收。理论上,存活时间久的对象,使用的越多,越不容易被回收,这也是分代回收设计的思想。
手动回收
- gc.collect()手动回收
- objgraph 模块中的 count()记录当前类产生的实例对象的个数
import gc result = gc.collect() print(result)
import objgraph
class Person(Object): pass class Cat(object): pass p = Person() c = Cat() p.name ='yuzhou1su' c.master = p print(sys.getrefcount(p)) print(sys.getfefcount(c)) del p del c gc.collect() print(objgraph.count('Person')) print(objgraph.count('Cat'))
当定位到哪个对象存在内存泄漏,就可以用 show_backrefs 查看这个对象的引用链。
内存池(memory pool)机制
频繁 申请、消耗 会导致大量的内存碎片,致使效率变低。
内存池的概念就是在内存中申请一定数量的,大小相等的内存块留作备用。
内存池池由单个大小类的块组成。每个池维护一个到相同大小类的其他池的双向链接列表。这样,即使在不同的池中,该算法也可以轻松找到给定块大小的可用空间。
当有新的内存需求时,就会先从内存池中分配内存留给这个需求。内存不够再申请新的内存。
内存池本身必须处于以下三种状态之一:
- 已使用
- 已满
- 或为空。
优点:减少内存碎片,提高效率。
总结
内存管理是计算机的一个非常重要的组成部分。 Python 跟 Java、Go 一样,帮助开发者从语言设计层面解决了这个问题,使得我们不用手动分配和释放内存,这也是这类语言的优势。
本文主要解释了:
- 什么是内存管理,管理方式的方式
- Cpython 的内存管理方式
- 垃圾回收机制
- Python 的引用计数、标记清楚和分代回收的垃圾自动回收方法。
- 最后介绍了手动回收的包和为了提高内存有效使用的内存池机制

