[Python之路] 内存管理&垃圾回收

一、python源码

1.准备源码

下载Python源码:https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz

解压得到文件夹:

我们主要关注Include中的".h"文件以及Objects目录中的".c"文件。

我们从Include和Objects中的文件类型就可以看出Python解释器是C语言编写的。

2.object.h

在Include文件夹中,全部都是".h"文件。

这些C语言头文件中主要存放着宏、函数声明、结构体声明、全局变量等。

我们在Python中所有的类都继承自Object,所以在这个C语言的object.h中,我们可以看看是如何实现的。

我们首先看object.h文件内容(小部分):

#define _PyObject_HEAD_EXTRA                struct _object *_ob_next;               struct _object *_ob_prev;

typedef struct _object {
    // 维护双向链表refchain
    _PyObject_HEAD_EXTRA
    // 引用计数
    Py_ssize_t ob_refcnt;
    // 数据的类型
    struct _typeobject *ob_type;
} PyObject;

typedef struct {
    PyObject ob_base;
    // 数据类型为多元素时,维护一个容量个数
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

我们可以从上面的源码中看到,两个结构体PyObject和PyVarObject,区别是PyVarObject多一个ob_size属性,这个属性代表的是元素的个数(例如list、dict中元素的个数)。

所以,这两个结构体,分别对应不同类型的数据的头(Python中任何数据的定义,都会有这个头):

PyObject:float

PyVarObject:list、dict、tuple、set、int、str、bool

因为Python中的int是不限制长度的,所以底层实现是用的str,所以int也属于PyVarObject阵营。Python中的bool实际上是0和1,所以也是int,也属于PyVarObject阵营。

3.floatobject.h

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

我们以float类型为例,可以看到创建一个float类型的数据,实际上是创建了一个PyFloatObject结构体的实例。

PyFloatObject结构体中包含了一个PyObject_HEAD(这就是object.h中的PyObject),以及一个double ob_fval,这个double变量就是我们存放的值。

我们以Python中的实际操作,来看源码中的过程:

1)python中定义变量v = 0.3:

源码流程:

    a.开辟内存(内存大小,是sizeof(PyFloatObject))

    b.初始化

      ob_fval=0.3

      ob_type=float

      ob_refcnt=1

    c.将对象加入双向链表refchain中

2)python执行操作name=v:

源码流程:

    ob_refcnt+=1

3)python执行操作del v:

源码流程:

    ob_refcnt-=1

4)python执行

def func(arg):
    print(arg)

func(name)

源码流程:

    执行时开辟栈:ob_refcnt+=1

    结束时销毁栈:ob_refcnt-=1

5)python执行del name:

源码流程:

    ob_refcnt-=1

在这几次操作中,每次进行ob_refcnt-=1的时候都会判断ob_refcnt是否等于0。如果是0,这将其归为垃圾,按理说GC回收器应该将其回收,请看第二节。

二、缓存机制

在第一节中,如果float变量的引用都被删除,引用计数为0以后,按理说GC回收器应该对其进行回收。

1.free_list缓存链表

但编译器认为,用户经常都要定义float类型的变量,所以他将该PyFloatObject对象从refchain链表中拿出来,并且放到另一个单向链表中,这个单向链表就是缓存(叫free_list)

我们做个验证:

>>> v = 8.9
>>> name = v
>>> del v
>>> id(name)
1706304905888
>>> del name
>>> xx = 9.0
>>> id(xx)
1706304905888
>>>

可以看到,name的id为1706304905888,删除name后,由创建了一个float变量xx,结果xx的id还是为170630490588。这就验证了缓存的机制。

为什么要使用缓存(free_list)?

  因为回收内存空间和开辟内存空间都要消耗时间,所以,如果将空间放到缓存中,有新的float变量被定义的话,直接从缓存中拿到地址,重新进行一次初始化,并将新的值赋给ob_fval即可。

2.free_list最大长度

注意,这里的单向链表(free_list)只是针对PyFloatObject类型的。而且这个链表有最大长度100。可以在floatobject.c中看到相关定义:

#ifndef PyFloat_MAXFREELIST
// 定义free_list的最大长度
#define PyFloat_MAXFREELIST    100
#endif
// 用numfree来表示当前free_list有多长
static int numfree = 0;
// free_list指针
static PyFloatObject *free_list = NULL;

例如同时有1000个float变量的引用计数变为0,则归入free_list的只有100个,其余900个可能会被回收。

在float中,free_list的最大长度是100,而在其他的数据类型中,最大长度可能不一样。

例如list的free_list的最大长度为80:

#ifndef PyList_MAXFREELIST
#define PyList_MAXFREELIST 80
#endif
static PyListObject *free_list[PyList_MAXFREELIST];
static int numfree = 0;

dict也为80:

#ifndef PyDict_MAXFREELIST
#define PyDict_MAXFREELIST 80
#endif
static PyDictObject *free_list[PyDict_MAXFREELIST];
static int numfree = 0;
static PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST];
static int numfreekeys = 0;

3.其他优化机制

也不是所有的数据类型都使用free_list缓存机制,例如int用的是小数据池进行优化:

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

三、垃圾回收机制

Python的GC主要遵循以下原则:

  引用计数器为主,标记清除和分代回收为辅。

1.引用计数器(同上,略)

2.循环引用

循环引用一般发生在列表、字典、对象等容器类对象,他们之间可以互相嵌套,例如:

a = [1, 2]
b = [4, 5]
# b的引用计数会加1,变为2
a.append(b)

# a的引用计数变为0
del a
# b的引用计数变为1,但是已经无法访问b,所以就形成了内存泄漏
del b

在这种情况下, 内存发生了泄漏,就要利用标记清除来解决循环引用的问题。

2.标记清除

针对那些容器类的对象,在Python中会将他们单独放到一个双向链表(非refchain)中,做定期扫描。

参考:https://www.cnblogs.com/saolv/p/8411993.html

#第一组循环引用#
a = [1,2]
b = [3,4]
a.append(b)
b.append(a)
del a

##

#第二组循环引用#

c = [4,5]
d = [5,6]
c.append(d)
d.append(c)
del c
del d
#至此,原a和原c和原d所引用的对象的引用计数都为1,b所引用的对象的引用计数为2,
e [7,8]
del e

现在说明一下标记清除:代码运行到上面这块了,此时,我们的本意是想清除掉c和d和e所引用的对象,而保留a和b所引用的对象。但是c和d所引用对象的引用计数都是非零,原来的简单的方法只能清除掉e,c和d所引用对象目前还在内存中。

  假设,此时我们预先设定的周期时间到了,此时该标记清除大显身手了。他的任务就是,在a,b,c,d四个可变对象中,找出真正需要清理的c和d,而保留a和b。

  首先,他先划分出两拨,一拨叫root object(存活组),一拨叫unreachable(死亡组)。然后,他把各个对象的引用计数复制出来,对这个副本进行引用环的摘除。

  环的摘除:假设两个对象为A、B,我们从A出发,因为它有一个对B的引用,则将B的引用计数减1;然后顺着引用达到B,因为B有一个对A的引用,同样将A的引用减1,这样,就完成了循环引用对象间环摘除。

  摘除完毕,此时a的引用计数的副本是0,b的引用计数的副本是1,c和d的引用计数的副本都是0。那么先把副本为非0的放到存活组,副本为0的打入死亡组。如果就这样结束的话,就错杀了a了,因为b还要用,我们把a所引用的对象在内存中清除了b还能用吗?显然还得在审一遍,别把无辜的人也给杀了,于是他就在存活组里,对每个对象都分析一遍,由于目前存活组只有b,那么他只对b分析,因为b要存活,所以b里的元素也要存活,于是在b中就发现了原a所指向的对象,于是就把他从死亡组中解救出来。至此,进过了一审和二审,最终把所有的任然在死亡组中的对象通通杀掉,而root object继续存活。b所指向的对象引用计数任然是2,原a所指向的对象的引用计数仍然是1

扫描后存活组的对象,将放到另外一个链表中去,一共有3个这样的链表,代表3代。

3.分代回收

分代回收就是指维护容器类对象的三个链表,3个链表对应三层。对最底层的链表扫描10次,才对上层的链表扫描一次。

这其实是为了节省性能,尽量少扫描对象。

认为没有问题经常使用的对象放入上一层,减少扫描次数。

所以,在Python的内存管理中,一共维护着4个链表,其中一个链表refchain用来管理一般的数据类型,例如float等。而另外3个链表组成分代,管理容器类数据类型。

参考博客:https://www.cnblogs.com/wupeiqi/articles/11507404.html

原文地址:https://www.cnblogs.com/leokale-zz/p/12113559.html

时间: 2024-10-01 11:10:42

[Python之路] 内存管理&垃圾回收的相关文章

[CLR via C#]21. 自动内存管理(垃圾回收机制)

目录 理解垃圾回收平台的基本工作原理 垃圾回收算法 垃圾回收与调试 使用终结操作来释放本地资源 对托管资源使用终结操作 是什么导致Finalize方法被调用 终结操作揭秘 Dispose模式:强制对象清理资源 使用实现了Dispose模式的类型 C#的using语句 手动监视和控制对象的生存期 对象复活 代 线程劫持 大对象 一.理解垃圾回收平台的基本工作原理 值类型(含所有枚举类型).集合类型.String.Attribute.Delegate和Event所代表的资源无需执行特殊的清理操作.

Java内存与垃圾回收调优

本文由 ImportNew - 进林 翻译自 journaldev.欢迎加入翻译小组.转载请参见文章末尾的要求. 要了解Java垃圾收集机制,先理解JVM内存模式是非常重要的.今天我们将会了解JVM内存的各个部分.如何监控以及垃圾收集调优. Java(JVM)内存模型 正如你从上面的图片看到的,JVM内存被分成多个独立的部分.广泛地说,JVM堆内存被分为两部分--年轻代(Young Generation)和老年代(Old Generation). 年轻代 年轻代是所有新对象产生的地方.当年轻代内

014 Python变量的内存管理

Python变量内存管理 1.变量存在哪里 1.如果我们定义了一个变量,而我们没有用python解释器取运行的时候,这个变量其实就是很普通的几个字符而已.而当我们用Python解释器取运行它的时候,那字符进入了内存,才会有变量这个概念.也就是说变量是存放在内存当中的. 2.但是说变量只是存在内存中并没有很具体,实际上在每定义一个变量就会在这个内存的大空间中开辟一个小空间 2.引用计数 1.引用计数是针对变量值的 2.比如定义一个变量 height = 180 x = height # x是在引用

python的优化机制与垃圾回收与gc模块

python属于动态语言,我们可以随意的创建和销毁变量,那么如果频繁的创建和销毁则会浪费cpu,那么python内部是如何优化的呢? python和其他很多高级语言一样,都自带垃圾回收机制,不用我们去维护,也避免了出现内存泄漏,悬空指针等bug,那么python内部如何进行垃圾回收的呢? python的垃圾回收,我们用gc模块去开启或者关闭它,那么gc模块又是什么呢? python的优化机制 垃圾回收 关于循环引用进行一个测试 import gc # python的一个垃圾回收模块,garbag

Python程序运行流程与垃圾回收机制

Python程序运行流程 Python解释器首先将程序将py文件编译成一个字节码对象PyCodeObject(只存在于内存中).(当这个模块的 Python 代码执行完后,就会将编译结果保存到了pyc文件中,这样下次就不用编译,直接加载到内存中.pyc文件只是PyCodeObject对象在硬盘上的表现形式.) py文件被编译后,接下来的工作就交由 Python虚拟机来执行字节码指令.Python虚拟机会从编译得到的PyCodeObject对象中依次读入每一条字节码指令,并在当前的上下文环境中执行

第七章python对象引用、可变性和垃圾回收

1.python变量是什么 python的变量实质是一个指针,而java普通变量是一个容器直接存入值. 为什么b也变了呢,由于a,b同时指向同一个地址,导致a指向的内容改变也会让b改变,id()获得对象所指向的内存中的地址,如果是对象本身的地址的话a,b应该是不相同的. 1 a = [1, 2, 3] 2 b = a 3 # a和b实质是指针都指向同一个地址,修改a同时会修改b 4 print(id(a), id(b)) # 54866344 54866344 5 print(a is b) #

java内存报警 垃圾回收

jdk6和7服务器端(-server) 默认的新生的垃圾回收器为:PS Scavenge,老年代默认的垃圾回收器为:PS MarkSweep 目前项目使用jdk7,tomcat7,经常出现内存堆使用量200s持续超过堆总内存80%,触发报警. 由于项目最近的更新为jdk和tomcat升级,从6升级到7,而之前使用tomcat6时并未报警,据查是由于tomcat的一个监听器行为模式变更造成的 <Listener className="org.apache.catalina.core.JreM

Python中的内存管理机制

Python是如何进行内存管理的 python引用了一个内存池(memory pool)机制,即pymalloc机制,用于管理对小块内存的申请和释放 1.介绍 python和其他高级语言一样,会进行自动的内存管理.它使用引用计数机制检测为对象分配的内存是否可以被释放.然后,在Python中内存永远不会返还给操作系统,Python会持有这些内存并在需要时重新使用它们.在很多场景下,这个特性可以减少内存申请和释放所带来的性能损耗:但对于需要长时间运行的Python进程来讲,Python将会占用大量的

Java内存管理和回收

转载自http://blog.csdn.net/cutesource/article/details/5906705 JVM内存组成结构 JVM栈由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Space和To Space组成,结构图如下所示: 新生代.新建的对象都是用新生代分配内存,E