点击关注上方蓝字,阅读更多干货~
01
为什么要内存管理
在Python中,变量是无需事先声明、无需指定类型的。Python的内存管理是一个包含所有Python对象和数据结构的私有堆,它是由python的解释器负责,可以帮助减少开发的程序错误,让程序更加稳定。那么我们为什么要进行内存管理,内存管理的好处有哪些?下面我们先引入一个内存碎片的概念。
内存碎片
内部碎片:是指被明确分配出去用于某个进程使用,但却不能被进程充分利用的内存空间,直到进程释放或者进程结束,系统才能再次利用该内存块。
外部碎片:是指没有被系统分配给某个进程,但由于太小导致无法分配给申请内存空间的新进程使用的内存空闲区域。
了解完内存碎片的概念后,我们一起看下面这个简单的例子。
假设有一个100个单位的连续空闲内存段,范围是0-99。当进程1申请了一块10个单位的内存空间,那么此时0-9范围内的内存空间将会分配给进程1使用,直到进程1释放空间或者进程1结束。接着另一个进程2申请了15个内存空间,范围是10-24。
图2 内存空间状态图
Python内存管理
通过上述的说明,我们了解了内存管理的部分知识。那么通过python来管理内存的好处是什么呢?和其他高级语言一样,通过其自身的内存管理,可以减少系统内存的开辟以及释放,避免产生内存碎片,也可以进行快速的内存分配。
02
Python中内存的管理形式
下面一起看看,在python中编译器进行内存管理时的代码和流程:
a = 100
b = a
b = 1
del a
图3 编译器进行内存管理时的流程图
我们简单的描述了python的几段代码在进行整个编译器内存管理时的大致流程。同时,引出了引用计数机制、垃圾回收机制、内存池机制的概念。那么这些机制在内存管理中具体担任什么角色,又承担什么作用呢?我们继续往下看。
03
引用计数机制
我们一起来看一个案例:
import sys
a = 100 # type is int
print(sys.getrefcount(a)) # return 2
b = a
print(sys.getrefcount(a)) # return 3
print(id(a)) # return 4365588224 变量a在内存中的地址
print(id(b)) # return 4365588224 变量b在内存中的地址
print(a is b) # return True is是python身份运算符,其判断两个对象中的内存地址是否一样
当有新的引用指向的时候,引用计数+1
当有有效的引用发生的时候,引用计数-1
这里我们也给大家提供查看对象的引用计数方法:
import sys
sys.getrefcount(obj)) # 其传入对象名,获取对象引用计数值。
# 注意,作为getrefcount这个函数的参数,每次引用计数会多1
引用计数并不一定时刻准确,下面我们来看看会导致引用计数发生改变的情况。
导致引用计数增加的情况:
那么,如果当一个对象的引用计数为0时,此时python解释器会做如何处理?一起继续往下看。
04
垃圾回收机制
a = [100,200,300]
del a
那么当对象x和对象y相互作用时,del语句会减少x和y的引用计数,且销毁引用底层对象的名称。然而由于每个对象都包含了一个其他对象的引用,因此引用计数不会归0,对象也不会消除,从而导致内存泄漏。
x = [1]
y = [2]
x.append(y)
y.append(x)
del x # 此时销毁了x的对象名称,且引用计数-1,正常情况下x所引用的对象应该是0,但这里是1
del y # 此时销毁了y的对象名称,且引用计数-1,正常情况下y所引用的对象应该是0,但这里是1
那么它有什么影响呢?它会降低计算机可用内存地址,从而降低计算机性能。最坏的情况下,它可能导致应用程序崩溃。
此时,python为了解决循环引用的问题,避免造成内存泄漏,会在垃圾回收机制运行过程中,引入另外两个机制:标记清除、分代回收。
标记清除
标记清除是指不改变真实的引用计数,而是将集合对象的引用计数复制一份副本,再改动该对象引用的副本。这里对副本做任何的改动,都不会影响到原本对象。需要注意的是,只有容器对象(列表、字段、集合等)才会产生循环引用的情况,而像字符串、整数类型的是不会发生循环引用的。
图6 标记清除流程图
通过标记清除机制就可以成功解决上述代码中循环引用的问题,那么我们应该在什么时候执行标记清除机制呢?
分代回收
分代回收是指建立在标记清除机制基础之上,以空间换取时间来提高垃圾回收效率的机制。
那么编译器会在什么时候进行分代回收呢?
import gc
print(gc.get_threshold()) # return (700, 10, 10)
上述代码中,返回输出元组类型的(700,10,10),表示着分代回收中的各个阈值,它们有着下面的3种对应关系:
第0代:当新生的对象个数减去已回收的对象个数的差大于700时,产生一次回收,本次回收的是第0、1、2代。
第1代:10次0代回收会产生一次1代回收,本次回收的是第1、2代。
第2代:10次1代回收会产生一次2代回收。
图8 触发垃圾回收的情况导图
05
内存池机制
内存池机制是指Python提供了内存的垃圾回收机制,且python编译器将不用的内存放到内存池而不是返回给操作系统。
第1层是python内存分配器,统一管理所有内存分配,包含了如PyMem_RawMalloc等一系列的内存API
第2层在第1层的接口上实现了统一分配对象内存,当对象小于512字节时会进行内存的分配和释放
第3层是我们对python对象的直接操作层
那么,我们为什么要引入pymalloc内存池机制,而不直接使用malloc来分配内存?因为使用malloc来分配内存需要在第0层使用c库函数调用。如果在c中频繁的调用malloc,会对性能产生影响,且多次进行内存的开辟分配和释放会产生大量的内存碎片。因此需要提前引入内存池机制,来提高python的执行效率。
06
与其他语言的比较
内存的生命周期
手动管理内存空间:c和c++等低级编程语言需要手动申请和释放内存空间
自动管理内存空间:python、java和js等高级编程语言可以自动申请和释放内存空间
C++的内存管理
在开辟动态内存空间时,需要手动通过new运算符来申请新内存空间,通过delete来释放内存空间。
JS的内存管理
对于基本数据类型(string、number、undefined等)的内存分配,会在执行时直接在栈空间进行分配。
var a = '100';
var b = 100;
var c = {
'a': '200',
'b': 200
};
在JS的垃圾回收中,还进行着引用计数、标记清除、标记整理等操作。
function dome(){
var a = '100'; // 当前内存a的引用计数为0
var b = a ; // a的引用计数+1 总计数为1
var c = a ; // a的引用计数+1 总计数为2
var b = 100 ; // a的引用计数-1 总计数为1
}
dome();
JS标记清除会给内存中的变量做上标记。一旦JS执行垃圾回收,或者当引用计数为0时,会回收这些变量所占的内存空间。
function dome(){
var a = '100';
var b = 100 ; // 将内存中的变量a和b标记
}
dome(); // 执行完毕后,变量a和变量b被标记清除
通过上述说明,我们可以发现js和python语言的内存管理机制相差不大。其主要区别在于python的分代回收和JS的标记整理。但c++的内存管理相较于python的内存管理却略有不同,c++的内存管理在进行动态内存分配时,需要手动申请释放,否则就会造成内存泄漏。
07
总结
为了帮助大家更好的进行python内存管理,我们可以通过使用内存池机制减少内存碎片化去提高执行效率;通过引用计数机制来完成垃圾回收机制,再采用标记清除来解决循环引用造成的内存泄漏问题,最后使用分代回收来提高垃圾回收的效率。
本文作者
白肖,来自缦图互联网中心数据团队。
--------END--------
也许你还想看