内存管理

参考:链接1链接2链接3链接4

Python 内部使用引用计数,来保持追踪内存中的对象,Python 内部记录了对象有多少个引用,即引用计数,当对象被创建时就创建了一个引用计数,当对象不再需要时,这个对象的引用计数为0时,它被垃圾回收。

测试占用内存的例子:

import os
import psutil
# 显示当前 python 程序占用的内存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))

def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    show_memory_info('after a created')
    func()
    show_memory_info('finished')

>>> #输出结果
initial memory used: 47.19140625 MB
after a created memory used: 433.91015625 MB
finished memory used: 48.109375 MB

引用计数

查看一个对象的引用计数

import sys
a = 'Hello World'
sys.getrefcount(a)

可以查看a对象的引用计数,但是比正常计数大1,因为调用函数的时候传入a,这会让a的引用计数+1。

引用计数加1

  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,或者窗口对象本身离开了作用域。

引用计数机制

python 里每一个东西都是对象,它们的核心就是一个结构体:PyObject

typedef struct_object {
    int ob_refcnt;
    struct_typeobject *ob_type;
} PyObject;

PyObject 是每个对象必有的内容,其中 ob_refcnt 就是做为引用计数。当一个对象有新的引用时,它的 ob_refcnt 就会增加,当引用它的对象被删除,它的 ob_refcnt 就会减少

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数
#define Py_DECREF(op) \ //减少计数
    if (--(op)->ob_refcnt != 0) 
        ; 
    else
        __Py_Dealloc((PyObject *)(op))

当引用计数为0时,该对象生命就结束了。

优点
  • 简单
  • 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
缺点
  • 维护引用计数消耗资源
  • 循环引用
list1,list2 = [], []
list1.append(list2)
list2.append(list1)

list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。 对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(标记清除和分代收集)

  • 标记清除:此方式主要用来处理循环引用的情况,只有容器对象(list、dict、tuple,instance)才会出现循环引用的情况。

垃圾回收

1、当内存中有不再使用的部分时,垃圾收集器就会把他们清理掉。它会去检查那些引用计数为0的对象,然后清除其在内存的空间。当然除了引用计数为0的会被清除,还有一种情况也会被垃圾收集器清掉:当两个对象相互引用时,他们本身其他的引用已经为0了。

2、垃圾回收机制还有一个循环垃圾回收器, 确保释放循环引用对象(a引用b, b引用a, 导致其引用计数永远不为0)。

在Python中,许多时候申请的内存都是小块的内存,这些小块内存在申请后,很快又会被释放,由于这些内存的申请并不是为了创建对象,所以并没有对象一级的内存池机制。这就意味着Python在运行期间会大量地执行malloc和free的操作,频繁地在用户态和核心态之间进行切换,这将严重影响Python的执行效率。为了加速Python的执行效率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。

小整数对象池

整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间。

Python 对小整数的定义是 [-5, 257) 这些整数对象是提前建立好的,不会被垃圾回收。在一个 Python 的程序中,所有位于这个范围内的整数使用的都是同一个对象.

同理,单个字母也是这样的。

但是当定义2个相同的字符串时,引用计数为0,触发垃圾回收。

大整数对象池

每一个大整数,均创建一个新的对象。

>>> a,b = 300,300
>>> id(a),id(b)
(1991892034320, 1991892034416)
>>> a,b = 200,200
>>> id(a),id(b)
(140722755439088, 140722755439088)

intern 机制

  • 小整数[-5,257)共用对象,常驻内存
  • 单个字符共用对象,常驻内存
  • 单个单词,不可修改,默认开启intern机制,共用对象,引用计数为0,则销毁

  • 字符串(含有空格),不可修改,没开启intern机制,不共用对象,引用计数为0,销毁

>>> a,b = 'Hello World','Hello World'
>>> id(a),id(b)
(1991892047600, 1991892047088)
>>> a,b = 'HelloWorld','HelloWorld'
>>> id(a),id(b)
(1991892047216, 1991892047216)
  • 大整数不共用内存,引用计数为0,销毁
  • 数值类型和字符串类型在 Python 中都是不可变的,这意味着你无法修改这个对象的值,每次对变量的修改,实际上是创建一个新的对象

内存池机制

Python提供了对内存的垃圾收集机制,但是它将不用的内存放到内存池而不是返回给操作系统。

Python中所有小于256个字节的对象都使用 pymalloc 实现的分配器,而大的对象则使用系统的 malloc。另外Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。