python的内存管理分为三个方面:
- 引用计数
- 垃圾回收
- 内存池机制
浅析引用计数
python内部使用引用计数,来保持追踪内存中的对象,Python内部记录了对象有多少个引用,即引用计数,当对象被创建时就创建了一个引用计数,当对象不再需要时,这个对象的引用计数为0时,它被垃圾回收。
引用计数增加的情况:
1.对象被创建:x=4
2.另外的别人被创建:y=x
3.被作为参数传递给函数:foo(x)
4.作为容器对象的一个元素:a=[1,x,’33’]引用计数减少情况
1.一个本地引用离开了它的作用域。比如上面的foo(x)函数结束时,x指向的对象引用减1。
2.对象的别名被显式的销毁:del x ;或者del y
3.对象的一个别名被赋值给其他对象:x=789
4.对象从一个窗口对象中移除:myList.remove(x)
5.窗口对象本身被销毁:del myList,或者窗口对象本身离开了作用域。如何获取一个变量的引用计数
>> import sys
>> x = 1
>> sys.getrefcount(x)
599
>> y = x
>> sys.getrefcount(x)
600
>> del y
>> sys.getrefcount(x)
599
垃圾回收
python的垃圾回收机制以引用计数为主,标记-清除和分代收集为辅。
- 引用计数
优点:“实时性”,任何内存,一旦没有指向它的引用,就会立即被回收。
缺点:
(1). 效率底下:引用计数机制带来的计数操作,与引用赋值成正比。频繁的技术操作,会给CPU带来大量消耗。
(2). 循环引用:也就是两个对象相互引用,这样的话,两个对象的引用计数永远不会为0,及它们永远不被清除。 - 标记-清除
标记-清除是为了解决循环引用的问题。可以包含其他对象引用的容器对象(比如:list,set,dict,class,instance)都可能产生循环引用。
2.1 假设
如果两个对象的引用计数都为1的话,但仅仅存在它们之间的相互引用,那么,我们可以认为这两个对象的实际引用计数为0.如果我们将这个引用循环去掉,那么它们的实际引用计数才会显现。
案例:有循环引用的A,B两个对象,从A出发,因为它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,因为B有一个对A的引用,同样将A的引用减1,这样,就完成了循环引用对象间环摘除。
问题:如果A,B间没有循环引用,但A引用了B,B没有以用A,贸然的将B计数引用减1,而A没有被回收,这将导致在未来的某个时刻出现一个对B的悬空引用,类似与C的空指针异常。这就要求我们必须在A没有被删除的情况下复原B的引用计数,那么维护引用计数的复杂度将成倍增加。
2.2 标记-清除的原理
原理:
我们并不改动真实的引用计数,而是将集合中对象的引用计数复制一份副本,改动该对象引用的副本。对于副本做任何的改动,都不会影响到对象生命走起的维护。
这个计数副本的唯一作用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合之后,首先将现在的内存链表一分为二,一条链表中维护root object集合,成为root链表,而另外一条链表中维护剩下的对象,成为unreachable链表。之所以要剖成两个链表,是基于这样的一种考虑:现在的unreachable可能存在被root链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的所有对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中即可。
效率分析:
从垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。 - 分代回收
3.1 理论:
无论使用何种语言开发,无论开发的是何种类型,何种规模的程序,都存在这样一点相同之处。即:一定比例的内存块的生存周期都比较短,通常是几百万条机器指令的时间,而剩下的内存块,起生存周期比较长,甚至会从程序开始一直持续到程序结束。
3.2 原理:
将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减小。也就是说,活得越长的对象,就越不可能是垃圾,就应该减少对它的垃圾收集频率。那么如何来衡量这个存活时间:通常是利用几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,可以得出:该对象存活时间就越长。也就是符合马太福音,存活久的让它继续存活下去。
内存池机制
Python的内存机制以金字塔层次:
-1,-2层主要有操作系统进行操作, 第0层是C中的malloc,free等内存分配和释放函数进行操作;
第1层和第2层是内存池,有Python的接口函数PyMem_Malloc函数实现,当对象小于256K时有该层直接分配内存;
- 在 C 中如果频繁的调用 malloc 与 free 时,是会产生性能问题的.再加上频繁的分配与释放小块的内存会产生内存碎片。
- 具现化
Python提供了对内存的垃圾收集机制,但是它将不用的内存放到内存池而不是返回给操作系统。
Python中所有小于256个字节的对象都使用pymalloc实现的分配器,而大的对象则使用系统的 malloc。另外Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。
在Python中,许多时候申请的内存都是小块的内存,这些小块内存在申请后,很快又会被释放,由于这些内存的申请并不是为了创建对象,所以并没有对象一级的内存池机制。这就意味着Python在运行期间会大量地执行malloc和free的操作,频繁地在用户态和核心态之间进行切换,这将严重影响 Python的执行效率。为了加速Python的执行效率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。这也就是之前提到的 Pymalloc机制。
Pymalloc 关于释放内存方面,当一个对象的 引用计数变为0时,python就会调用它的析构函数。在析构时,也采用了内存池机制,从内存池来的内存会被归还到内存池中,以避免频繁地释放动作。
Pymalloc分配一系列256KB的内存块,称之为arena。每个arena分割为4KB大小的内存池Pool,每个Pool再切分为固定大小的Block。在内存分配时,分配给进程的就是这些Blocks。