python内存管理
内存分配
python中的内存分配采用内存池的方式。( 内存池:当创建大量消耗小内存的对象时,频繁调用new/malloc会导致大量的内存碎片,致使效率降低。内存池的作用就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。)
如图,为cpython(python解释器的内存架构图):
- level 0 以下为系统层内存管理;python实现的内存管理机制主要位于level 1~level 3;
- level 3: python对于每一类内置的对象(int、float、list、dict、string)设置对象间独立的私有内存池,即内存池不共享,int释放的内存,不会被分配给float使用;
- level 2: 当申请的内存大小小于256KB时,内存分配主要由 Python 对象分配器(Python’s object allocator)实施;
- level 1: 当申请的内存大小大于256KB时,由Python原生的内存分配器进行分配,本质上是调用C标准库中的malloc/realloc等函数;
内存回收
在内存回收方面,python同样使用内存池机制释放内存,当对象引用计数变为0,调用析构函数,从内存池申请到的内存会被归还到内存池中,以避免频繁地申请和释放动作。
标准的 CPython GC 有两个组件:
- **引用计数收集器(reference counting collector)**:主要的、基础模块、不可控,不能禁用。
- **分代垃圾收集器(generational garbage collector)**:辅助的、可以手动触发和禁用,即 gc module。
CPython GC 的策略是:
- 对每个对象维护引用计数
- 通过一个辅助算法来定期检测循环引用,释放无用对象
- 引入分代策略来优化此检测,提高性能
引用计数
顾名思义,引用计数即记录对象被其他使用的对象引用的次数。python设置内部跟踪变量即引用计数器,记录每个变量有多少个引用。当某个对象的引用计数为0时,就进入垃圾回收队列。
1 |
|
注:当把a作为参数传递给getrefcount时,会产生一个临时的引用,因此得出来的结果比真实情况+1
引用计数增加:
- 赋值运算符(例如:a=[1,2])
- 参数传递
- 将对象(作为元素)append 到容器中(例如:c.append(a))
引用计数减少:
- 使用del语句对对象别名显式的销毁(例如:del b)
- 对象所在的容器被销毁或从容器中删除对象(例如:del c )
- 引用超出作用域或被重新赋值(例如:a=[3,4])
全局变量:
全局变量会一直存在直到 Python 进程结束为止。因此,由全局变量引用的对象的引用计数永远不会降为零,所有全局变量都存储在字典中,你可以通过调用 globals() 函数来获取它们。
在某一个『块』中定义的局部变量,如 function、class 或 with(context’s enter/exit) 语句具有局部作用域,当解释器从该块中退出时,会释放在该块内部创建的局部变量及其引用。
循环引用
1 |
|
注意 :
- a引用b, b引用a, 此时两个对象各自被引用了2次(去除getrefcout()的临时引用)
- 执行del之后,对象a,b的引用次数都-1,此时各自的引用计数器都为1,陷入循环引用
- 标记:找到其中的一端a, 因为它有一个对b的引用,则将b的引用计数-1。再沿着引用到b, b有一个a的引用, 将a的引用计数-1,此时对象a和b的引用次数全部为0,被标记为不可达(Unreachable)
- 清除:被标记为不可达的对象就是真正需要被释放的对象
分代回收
*标记清除过程会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行。为了减少应用程序暂停的时间,Python 通过“**分代回收”(Generational Collection)*以空间换时间的方法提高垃圾回收效率。
分代回收基于这样的一个统计事实:对于程序,存在一定比例的内存块的生存周期比较短;而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。生存期较短对象的比例通常在 80%~90%之间。 因此,简单地认为:对象存在时间越长,越可能不是垃圾,应该越少去收集。这样在执行标记清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度,是一种以空间换时间的方法策略。
Python将所有的对象分为年轻代(第0代)、中年代(第1代)、老年代(第2代)三代。所有的新建对象默认是第0代对象。当在第0代的GC扫描中存活下来的对象将被移至第1代,在第1代的GC扫描中存活下来的对象将被移至第2代。
当某一代中被分配的对象与被释放的对象之差达到某一阈值时,就会触发当前一代的GC扫描。当某一代被扫描时,比它年轻的一代也会被扫描,因此,第2代的GC扫描发生时,第0,1代的GC扫描也会发生,即为全代扫描。
1 |
|
标记清除(PyPy)
引用计数能够解决大多数垃圾回收的问题,但是遇到两个对象相互引用的情况,del语句可以减少引用次数,但是引用计数不会归0,对象也就不会被销毁,从而造成了内存泄漏问题。
标记清除用来解决引用计数机制产生的循环引用,进而导致内存泄漏的问题 。( 循环引用只有在容器对象才会产生,比如字典,元组,列表等。)
- 标记阶段,遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达。
- 清除阶段,再次遍历对象,如果发现某个对象没有标记为可达(即为Unreachable),则就将其回收。
总结
CPython 的大部分垃圾收集是通过引用计数完成的,我们无法对其进行干涉调整,通过辅助的分代 GC 来处理循环引用,其可控,参考 gc module。
参考: