持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情
前言
我们都知道,Python是面向对象的语言。在Python里万物皆对象,那么Python是如何给对象分配内存空间呢?Python又有哪些内存管理机制呢?带着这2个问题,本文揭开Python内存管理机制的面纱。
Python三大内存管理机制
首先来个结论,Python有三大内存管理机制:引用计数、垃圾回收、内存池机制。 下面我们一一来看。
引用计数
引用计数在Python使用中很常见,比如我们函数中会有传参,这就是对参数的一个引用。当一个对象被引用时,引用次数会+1,同样的,对象减少一个引用时,引用计数-1。而当引用计数=0时则代表对象被删除了。
用个小例子来说说:
In [1]: from sys import getrefcount
In [2]: a = "juejin"
In [3]: getrefcount(a)
Out[3]: 2
In [4]: b = a
In [5]: getrefcount(a)
Out[5]: 3
In [6]: getrefcount(b)
Out[6]: 3
In [7]: id(a)
Out[7]: 140372839359216
In [8]: id(b)
Out[8]: 140372839359216
分析一下例子:
- 首先创建一个对象a,此时a的引用计数为1;
- getrefcount函数引用了对象a作为参数,引用计数+1,因此引用计数打印是为2;
- b=a,这里b引用了a,所以In[5]、In[6]中打印a和b的引用次数都为3。
- In[7]、In[8]分别打印了a和b对象的地址,都指向同一个地址。
通过引用计数,可以减少内存的创建和开销。比如a和b值相同,就不必要重新开辟空间给b了。
垃圾回收
同样先看结论,Python中的垃圾回收机制有引用计数、标记清除、分代回收。
引用计数的垃圾回收
首先引用计数在上面已经介绍过了,它也属于一种垃圾回收机制。它体现在当一个对象被引用次数为0时,说明没有引用指向该对象了,这时候对象就要被回收了。
这个机制很好理解,也是最直观的一种垃圾回收机制。但需要注意的是:如果对象出现了循环引用,引用计数就不起作用了。什么是循环引用呢?用2个对象来说,就是A对象和B对象相互引用,而且没有其他引用A或B。说起来不好理解,用个例子解释一下:
m, n = {}, {}
m['n'] = n
n['m'] = m
del m
del n
例子中,m和n定义时引用计数都为1,但2、3行各自引用了一次,这样一来m、n计数都+1=2。最后分别del m、del n,引用计数-1,最后就是m和n的引用计数都还是1。按照我们的本意m和n对象最后都要被清除了,但它们的引用计数都>0。这种情况下,引用计数就失效了,标记清除和分代回收就要派上用场了。
标记清除
标记清除可解决循环引用的问题,它是基于追踪回收实现的垃圾回收算法。简单点来说,这个算法分2个步骤:
- 标记:GC将所有的活动对象标记一下;
- 清除:出现无标记的对象,认定为非活动对象,然后回收它。
判断对象是否活动,主要通过指针方向来看。如下图,从跟对象出发,沿着指针方向向对象遍历,能遍历到的对象就是活动对象,不能遍历的则是非活动对象。下图中1、2、3都可遍历到,所以是活动对象,而4没有指针可到,因此4是非活动对象。
标记清除主要是处理Python的容器对象,比如字典、列表、元组等。
分代回收
分代回收相对来说不直观,它是一种时间换空间的做法。在Python中将内存分为3块,分别是0、1、2块,它们都有各自对应的链表。
新创建的对象会分到第0块,当第0块的链表总数达到一个阈值时,Python垃圾回收机制会触发。可被回收的对象会处理掉,不用回收的对象就会移到第1块去。而第1块的对象处理和第0块也类似。第2块的对象是存活最久的。
内存池机制
Python的内存机制呈金字塔状,不同层级对于不同的操作:
- -1、-2层:由操作系统直接进行操作;
- 0层:当请求分配的内存>256k,使用C中的malloc、free 等函数分配和释放内存。
- 1、2层:当请求分配的内存<=256k,由Python的接口函数Pymem_Malloc去操作。
- 3层:用户对Python对象的操作。
Python通过引入内存池机制,主要用在管理小内存(<256k)的申请及释放。因为大量的执行0层的动作,也就是malloc或free会频繁在用户与操作系统切换影响效率。当然,我们也可根据自身需要修改256k这个阈值来调整0层和1、2层的临界点。
小结
本文解答了Python中有哪些内存管理机制,了解这些内存机制,我们在使用Python时可以避免内存泄漏的问题,比如循环引用的问题。