Python的内存管理

Python的内存管理

在 Python 中,内存管理涉及到一个包含所有 Python 对象和数据结构的私有堆(heap)。这个私有堆的管理由内部的 Python 内存管理器(Python memory manager) 保证。Python 内存管理器有不同的组件来处理各种动态存储管理方面的问题,如共享、分割、预分配或缓存。
内存管理机制

  1. 动态内存分配: Python使用动态内存分配,这意味着它在运行时动态分配和管理内存,而不需要显式的内存分配或释放操作。
  2. 引用计数: Python使用引用计数来跟踪对象的引用数量。每当一个对象被引用一次,其引用计数增加一次。当引用计数降至零时,Python会自动回收对象的内存。
  3. 循环引用: 引用计数机制可以很好地处理循环引用的情况。当两个或多个对象相互引用时,只有当它们不再可达时(无法通过任何路径访问),它们的引用计数才会减少。
  4. 垃圾回收器: 除了引用计数,Python还使用垃圾回收器来检测和回收循环引用中的对象。垃圾回收器可以找到不可达的对象,并将它们释放以回收内存。
  5. 内存池: Python使用内存池来管理小型对象的内存分配。内存池将内存分成小块,以减少内存碎片,并加速对象的分配和释放。
  6. C扩展模块: 由于Python的内核部分是用C语言编写的,因此可以使用C扩展模块来管理内存。这允许Python与底层系统和库进行交互,并有效地管理内存。
  7. 内存优化工具: Python提供了一些内存优化工具,如gc模块,允许你手动控制垃圾回收器的行为,以及sys模块中的函数,用于查看和管理内存使用情况。
  8. 引用和复制: 在Python中,对象的传递通常是通过引用而不是复制来实现的。这意味着当你将一个对象传递给函数或赋值给另一个变量时,实际上是传递了对该对象的引用,而不是对象的复本。这可以减少内存使用和提高性能。

python3.6.9 内存管理的官方文档 https://docs.python.org/zh-cn/3.6/c-api/memory.html

对象,Object

Python的内存管理主要是针对“一切皆对象”这个设计理念设计的(这里我们仅针对CPython实现展开说明。)。什么是对象,Object?在Python的世界里对象就是任何分配在堆中的值。

在CPython实现中,对象是struct, PyObject,包含四个部分:

  • 指向其他对象的指针(用来找到其他的对象)
  • 对象的引用计数器
  • 类型指针
  • 对象的大小(对于大小可变的对象)
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

typedef struct {

python对象在内存中的存储,主要在栈与堆中。
Python的内存管理_第1张图片

1.python中的内存机制

Python的内存管理_第2张图片


Python的内存机制以金字塔行,-1,-2层主要有操作系统进行操作,

  • 第0层是C中的malloc,free等内存分配和释放函数进行操作;
  • 第1层和第2层是内存池,有Python的接口函数PyMem_Malloc函数实现,当对象小于256K时有该层直接分配内存;
  • 第3层是最上层,也就是我们对Python对象的直接操作;

Python中所有小于256个字节的对象都使用pymalloc实现的分配器,而大的对象则使用系统的 malloc。另外Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。
扩展:在 C 中如果频繁的调用 malloc 与 free 时,是会产生性能问题的.再加上频繁的分配与释放小块的内存会产生内存碎片. Python 在这里主要干的工作有:
如果请求分配的内存在1~256字节之间就使用自己的内存管理系统,否则直接使用 malloc.
这里还是会调用 malloc 分配内存,但每次会分配一块大小为256k的大块内存.
经由内存池登记的内存到最后还是会回收到内存池,并不会调用 C 的 free 释放掉.以便下次使用.对于简单的Python对象,例如数值、字符串,元组(tuple不允许被更改)采用的是复制的方式(深拷贝?),也就是说当将另一个变量B赋值给变量A时,虽然A和B的内存空间仍然相同,但当A的值发生变化时,会重新给A分配空间,A和B的地址变得不再相同

2.python中的内存池

当创建大量消耗小内存的对象时,频繁调用new/malloc会导致大量的内存碎片,致使效率降低。内存池的作用就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。
python中的内存管理机制为Pymalloc
1.内存池的工作方式:

  • 内存池将内存分成固定大小的块。
  • 当需要分配小型对象时,内存池会从这些块中分配一块合适大小的内存。
  • 当对象不再被引用时,内存池将内存块标记为空闲,以备将来再次使用。

2.内存池的优势

  • 减少内存碎片:内存池可以有效减少内存碎片,因为它们只分配固定大小的块,而不需要动态分配内存。
  • 快速分配和释放:内存池可以迅速分配和释放对象,因为它们不需要执行复杂的内存分配和释放操作。
  1. 内置类型的内存池:
  • Python中的一些内置类型,如整数、浮点数和短字符串,使用内存池来管理内存。
  • 这些类型的对象在创建时会从内存池中分配,并在不再被引用时返回给内存池。

2.1内存池中对象的内存管理

对于小对象内存,Python主要提供了三类对象进行管理:arena, pool, block。

  • block, 最小内存分配单位,每一个block的大小以8byte为单位,划分为64个组。
  • pool, 是相同大小block的集合
  • arena, 每一个大小为256kb,包含64个pool

2.2 Block

Block是Python对象内存分配的最小单位,从8byte(或者16byte)到512byte不等

 * Request in bytes     Size of allocated block      Size class idx
 * ----------------------------------------------------------------
 *        1-8                     8                       0
 *        9-16                   16                       1
 *       17-24                   24                       2
 *       25-32                   32                       3
 *       33-40                   40                       4
 *       41-48                   48                       5
 *       49-56                   56                       6
 *       57-64                   64                       7
 *       65-72                   72                       8
 *        ...                   ...                     ...
 *      497-504                 504                      62
 *      505-512                 512                      63

2.3 pool

pool 是相同分组(大小) block 的集合。一般,pool的大小为4kb(内存分页的大小),这主要是为了方便处理内存的fragmentation。如果一个对象被回收了,内存管理器可以再次利用这部分内存存储其他合适大小的对象。pool的结构如下:

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};

可以看到,每一个pool通过双链表连接在一起,szidx 记录了这个pool下面的block的size,ref.count 记录了已经被使用的blocks,arenaindex 则记录了这个pool所属的 arena 地址。freeblock 是当前可用的第一个block的地址。值得注意的是,如果一个block是空的,那么它会存储下一个空block的地址,这样方便寻址。

每一个pool包含三个状态:

  • used,部分被使用
  • full,全部被使用
  • empty,空

为了提高寻址效率,Python还维护一个array usedpools, 存储不同分组的pool的头地址。如下:

Python的内存管理_第3张图片

另外,block和pool不会直接分配内存,他们只是维护内存的数据结构,内存是由 arena 进行分配的。

3.3Arena

Arena 代表了一片大小为256kb的在堆上的内存空间,每一个 arena 包含 64 个pool。arena的结构如下:

struct arena_object {
    uintptr_t address;
    block* pool_address;
    uint nfreepools;
    uint ntotalpools;
    struct pool_header* freepools;
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

arena也是被双链表连接在一起,ntotalpools和nfreepool记录了目前可用的pool的信息。

3.python的垃圾回收机制

高级语言一般都有垃圾回收机制,其中c、c++使用的是用户自己管维护内存的方式,这种方式比较自由,但如果回收不当也会引起垃内存泄露等问题。而python采用的是引用计数机制为主,标记-清理和分代收集两种机制为辅的策略。
从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的表:

a = [1, 2, 3]
del a

解析del
del a后,已经没有任何引用指向之前建立的[1,2,3],该表引用计数变为0,用户不可能通过任何方式接触或者动用这个对象,当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。
(1)、垃圾回收时,Python不能进行其它的任务,频繁的垃圾回收将大大降低Python的工作效率;
(2)、Python只会在特定条件下,自动启动垃圾回收(垃圾对象少就没必要回收)
(3)、当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(objectdeallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。
我们可以通过gc模块的get_threshold()方法,查看该阈值:

import gc
print(gc.get_threshold())

返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。
我们也可以手动启动垃圾回收,即使用gc.collect()。

1、引用计数

python中一切皆对象,所以python底层计数结构地就可以抽象为:

pyobject{
    引用计数;  reference count 
	引用的对象类型; object type
	引用对象的值 value
}

是不是简单明了。现在我们先去考虑一下,什么情况下引用计数+1,什么情况下-1,当引用次数为0时,肯定就是需要进行回收的时刻。

引用计数+1的情况
1、对象被创建时,例如 mark="bill"
2、对象被copy引用时,例如 mark2=mark,此时mark引用计数+1
3、对象被作为参数,传入到一个函数中时
4、对象作为一个子元素,存储到容器中时,例如 list=[mark,mark2]

引用计数-1的情况
1、对象别名被显示销毁,例如 del mark
2、对象引用被赋予新的对象,例如mark2=mark3,此时mark引用计数-1(对照引用计数+1的情况下的第二点来看)
3、一个函数离开他的作用域,例如函数执行完成,它的引用参数的引用计数-1
4、对象所在容器被销毁,或者从容器中删除。

产看引用计数

实例:

import sys
a = "mark"
print(sys.getrefcount(a))

结果:
4
备注:如果实际结果与上面不符,跟使用的编辑器有很大关系,重点是理解计数引用原理,不要太在意为啥不是1.
想理解原因可以转看:https://stackoverflow.com/questions/45021901/why-does-a-newly-created-variable-in-python-have-a-ref-count-of-four

引用计数机制优点

1、简单、直观
2、实时性,只要没有了引用就释放资源。

引用计数机制缺点

1、维护引用计数需要消耗一定的资源
2、循环应用时,无法回收。也正是因为这个原因,才需要通过标记-清理和分代收集机制来辅助引用计数机制。

2、标记-清理

由上面内容我们可以知道,引用计数机制有两个缺点,缺点1还可以勉强让人接受,缺点2如果不解决,肯定会引起内存泄露,为了解决这个问题,引入了标记删除。

Python的内存管理_第4张图片

Python的内存管理_第5张图片

Theory of the mark-and-sweep

在标记扫描集合中,收集器首先检查 程序变量;指向的任何内存块都将添加到列表中 要检查的块数。对于该列表中的每个块,它都会设置一个标志 (标记)以表明它仍然是必需的,并且 它已被处理。它还会将任何块添加到列表中 由尚未标记的块指向。这样, 程序可以访问的所有块都被标记。

在第二阶段,收集器扫描所有分配的内存, 搜索尚未标记的块。如果找到任何内容,则 将它们返回到分配器以供重用。

Python的内存管理_第6张图片

五个内存块,其中三个可从程序变量访问。

在上图中,块 1 可以直接从程序访问 变量,块 2 和 3 是间接可访问的。第4和第5座 程序无法访问。第一步将标记块 1, 并记住块 2 和 3 以供以后处理。第二步 将标记块 2。第三步将标记块 3,但不会 请记住块 2,因为它已经标记。扫描阶段将忽略 块 1、2 和 3,因为它们被标记,但会回收块 4 和 5.

简单标记扫描收集的两个缺点是:

  • 它必须先扫描正在使用的整个内存,然后才能释放任何内存; stop the world collection
  • 它必须运行到完成,或者,如果中断,则从头开始。

如果系统需要实时或交互式响应,那么简单 标记扫描系列可能不适合目前的情况,但还有更多 复杂的垃圾收集算法由此派生技术。

看个实例,从实例中领会标记删除:

a=[1,2]#假设此时a的引用为1
b=[3,4]#假设此时b的引用为1
#循环引用
a.append(b)#b的引用+1=2
b.append(a)#a的引用+1=2


#假如现在需要删除a,应该如何回收呢?
#  手动触发垃圾回收
# gc.collect()

c=[5,6]#假设此时c的引用为1
d=[7,8]#假设此时d的引用为1
#循环引用
c.append(d)#c的引用+1=2
d.append(c)#d的引用+1=2
#假如现在需要同时删除c、d,应该如何回收呢?

首先我们应该已经知道,不管上面两种情况的哪一个都无法只通过计数来完成回收,因为随便删除一个变量,它的引用只会-1,变成1,还是大于0,不会回收,为了解决这个问题,开始看标记删除来大展神威吧。

python标记删除时通过l两个容器来完成的:死亡容器、存活容器。

首先,我们先来分析情况2,删除c、d
删除后,c的引用为1,d的引用为1,根据引用计数,还无法删除

标记删除第一步:对执行删除操作后的每个引用-1,此时c的引用为0,d的引用为0,
把他们都放到死亡容器内。把那些引用仍然大于0的放到存活容器内。

标记删除第二步:遍历存活容器,查看是否有的存活容器引用了死亡容器内的对象,
如果有就把该对象从死亡容器内取出,放到存活容器内。
由于c、d都没有对象引用他们了,所以经过这一步骤,他们还是在死亡组。

标记删除第三部:将死亡组所有对象删除。
这样就完成了对从c、d的删除。

同样道理,我们来分析:只删除a的过程:

  • 标记删除第一步:对执行删除(-1)后的每个引用-1,那么a的引用就是0,b的引用为1,将a放到死亡容器,将b放到存活容器。
  • 标记删除第二步:循环存活容器,发现b引用a,复活a:将a放到存活容器内。
  • 标记删除第三步:删除死亡容器内的所有对象。

综上所说,发现对于循环引用,必须将循环引用的双发对象都删除,才可以被回收。

3、分代收集

经过上面的【标记-清理】方法,已经可以保证对垃圾的回收了,但还有一个问题,【标记-清理】什么时候执行比较好呢,是对所有对象都同时执行吗?

同时执行很显然不合理,我们知道,存活越久的对象,说明他的引用更持久(好像是个屁话,引用不持久就被删除了),为了更合理的进行【标记-删除】,就需要对对象进行分代处理,思路很简单:

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

分代(generation)回收的策略
新创建的对象做为0代 
2、执行一个【标记-删除】,存活的对象代数就+1 
3、代数越高的对象(存活越持久的对象),进行【标记-删除】的时间间隔就越长。这个间隔,江湖人称阀值。

gc扫描次数(第0代>第1代>第2代)
当某一代中被分配的对象与被释放的对象之差达到某一阈值时,就会触发当前一代的gc扫描。当某一代被扫描时,比它年轻的一代也会被扫描,因此,第2代的gc扫描发生时,第0,1代的gc扫描也会发生,即为全代扫描。

>>> import gc 
>>> gc.get_threshold() ## 分代回收机制的参数阈值设置
(700, 10, 10)
  • 700=新分配的对象数量-释放的对象数量,第0代gc扫描被触发
  • 第一个10:第0代gc扫描发生10次,则第1代的gc扫描被触发
  • 第二个10:第1代的gc扫描发生10次,则第2代的gc扫描被触发

同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。

import gc
gc.set_threshold(700, 10, 5)

4、三种情况触发垃圾回收

1、调用gc.collect() 2、GC达到阀值时 3、程序退出时

5、小整数对象池与intern机制

由于整数使用广泛,为了避免为整数频繁销毁、申请内存空间,引入了小整数对象池。[-5,257)是提前定义好的,不会销毁,单个字母也是。
那对于其他整数,或者其他字符串的不可变类型,如果存在重复的多个,例如:
a1=“mark” a2=“mark” a3=“mark” a4=“mark” … a1000=“mark”
如果每次声明都开辟出一段空间,很显然不合理,这个时候python就会使用intern机制,靠引用计数来维护。
总计:
1、小整数[-5,257):共用对象,常驻内存
2、单个字符:共用对象,常驻内存
3、单个单词等不可变类型,默认开启intern机制,共用对象,引用计数为0时销毁。

4.GC

gc.collect 源码解读

collect 是垃圾回收的核心函数,理解的这个函数,也就理解了整个垃圾回收机制流程。
static Py_ssize_t collect(int generation)
{
    int i;
    Py_ssize_t m = 0; /* # objects collected */
    Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
    PyGC_Head *young; /* the generation we are examining */
    PyGC_Head *old; /* next older generation */
    PyGC_Head unreachable; /* non-problematic unreachable trash */
    PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */
    PyGC_Head *gc;
    double t1 = 0.0;

    ...

    /* update collection and allocation counters */
    if (generation+1 < NUM_GENERATIONS)
        generations[generation+1].count += 1;
    for (i = 0; i <= generation; i++)
        generations[i].count = 0;

    /* 上面提到的,如过回收的不是最幼代,会把比它年轻的代都合并过来 */
    for (i = 0; i < generation; i++) {
        gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
    }

    /* young 指向当前要回收的代
     *   old 指向比 young 老一代的代,如果 yound 已经是最老,那么和 yound 相等 */
    young = GEN_HEAD(generation);
    if (generation < NUM_GENERATIONS-1)
        old = GEN_HEAD(generation+1);
    else
        old = young;

   /* 垃圾回收流程的第一步:设置对象的 gc_refs 为 ob_refcnt */
    update_refs(young);
    /* 垃圾回收流程的第二步 1:将每个 reachable 的对象的 gc_refs 减 1 */
    subtract_refs(young);

   /* 初始化 unreachable 链表 */
    gc_list_init(&unreachable);
    /* 垃圾回收流程的第二步 2:将所有 unreachable 的对象移动到 unreachable 链表
     * 注意:流程中是说将 reachable 的对象移动单独集合,这是老的实现方式,并没有错,只是后来发现
     *      unreachable 的对象往往对于 unreachable 的,移动 unreachable 更有效率 */
    move_unreachable(young, &unreachable);

    /* Move reachable objects to next generation. */
    if (young != old) {
        if (generation == NUM_GENERATIONS - 2) {
            long_lived_pending += gc_list_size(young);
        }
        gc_list_merge(young, old);
    }
    else {
        /* We only untrack dicts in full collections, to avoid quadratic
           dict build-up. See issue #14775. */
        untrack_dicts(young);
        long_lived_pending = 0;
        long_lived_total = gc_list_size(young);
    }

    /* 初始化 finalizers 链表,用于存放带有析构方法的对象 */
    gc_list_init(&finalizers);
    /* unreachable 的对象本来是可以回收的,但是如果对象定义了析构方法,就要放弃回收
     * 并将对象移到 finalizers 链表 */
    move_finalizers(&unreachable, &finalizers);
    /* finalizers 现在链接了所有 unreachable 但是带有析构方法的对象,被这些对象引用的
     * 其它 unreachable 对象也不能回收,将它们也移动 finalizers 链表 */
    move_finalizer_reachable(&finalizers);

    /* 统计可回收对象的数量,打印 debug 信息 */
    for (gc = unreachable.gc.gc_next; gc != &unreachable; gc = gc->gc.gc_next) {
        m++;
        if (debug & DEBUG_COLLECTABLE) {
            debug_cycle("collectable", FROM_GC(gc));
        }
    }

    /* 处理弱引用对象,这里暂不深入 */
    m += handle_weakrefs(&unreachable, old);

    /* unreachable 链表剩下的对象都是可回收的了,调用这些对象的 tp_clear 方法进行垃圾回收,
     * 循环引用在这里被打破 */
    delete_garbage(&unreachable, old);

    /* Collect statistics on uncollectable objects found and print
     * debugging information. */
    for (gc = finalizers.gc.gc_next;
         gc != &finalizers;
         gc = gc->gc.gc_next) {
        n++;
        if (debug & DEBUG_UNCOLLECTABLE)
            debug_cycle("uncollectable", FROM_GC(gc));
    }

    /* debug */
    ...

    /* finalizers 里的 unreachable 对象往上移一代 */
    (void)handle_finalizers(&finalizers, old);

    /* freelist 是一些类型下面(如 tupe )为了避免每次创建新对象都要申请内存而缓存的一些
     * 空对象,当 GC 回收的是最老的代时,这些类型下的 freelist 里的对象会也被释放(不是全部
     * 释放,tuple 下的逻辑是保留一个)*/
    if (generation == NUM_GENERATIONS-1) {
        clear_freelists();
    }

    ...

    return n+m;
}

参考文章:

https://zhuanlan.zhihu.com/p/150032487

https://docs.python.org/zh-cn/3.6/c-api/memory.html

https://www.cnblogs.com/TMesh/p/11731010.html

https://zhuanlan.zhihu.com/p/65839740

https://www.cnblogs.com/shengulong/p/10143856.html

https://www.zhihu.com/question/32373436/answer/549698608

https://www.memorymanagement.org/mmref/index.html#mmref-intro

https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/

https://www.cnblogs.com/workherd/p/14199648.html

https://zhuanlan.zhihu.com/p/65838548

https://github.com/python/cpython/blob/ad051cbce1360ad3055a048506c09bc2a5442474/Objects/obmalloc.c#L534

你可能感兴趣的:(Python,python,pycharm)