[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 = ;
// 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 = ;
dict也为80:
#ifndef PyDict_MAXFREELIST
#define PyDict_MAXFREELIST 80
#endif
static PyDictObject *free_list[PyDict_MAXFREELIST];
static int numfree = ;
static PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST];
static int numfreekeys = ;
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
[Python之路] 内存管理&垃圾回收的更多相关文章
- python内存管理&垃圾回收
python内存管理&垃圾回收 引用计数器 环装双向列表refchain 在python程序中创建的任何对象都会放在refchain连表中 name = '张三' age = 18 hobby ...
- Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC
[转载] :http://my.oschina.net/rouchongzi/blog/171046 Java之类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指 ...
- 内存管理 垃圾回收 C语言内存分配 垃圾回收3大算法 引用计数3个缺点
小结: 1.垃圾回收的本质:找到并回收不再被使用的内存空间: 2.标记清除方式和复制收集方式的对比: 3.复制收集方式的局部性优点: https://en.wikipedia.org/wiki/C_( ...
- [CLR via C#]21. 自动内存管理(垃圾回收机制)
目录 理解垃圾回收平台的基本工作原理 垃圾回收算法 垃圾回收与调试 使用终结操作来释放本地资源 对托管资源使用终结操作 是什么导致Finalize方法被调用 终结操作揭秘 Dispose模式:强制对象 ...
- 【python进阶】Garbage collection垃圾回收2
前言 在上一篇文章[python进阶]Garbage collection垃圾回收1,我们讲述了Garbage collection(GC垃圾回收),画说Ruby与Python垃圾回收,Python中 ...
- python变量的内存管理
python变量的内存管理 一.变量存在了哪里? 先让我们来看一段代码: height = 100 # 定义变量 # print(100) # print会自动帮你创建一个变量100,打印完之后,马上 ...
- Java进阶 JVM 内存与垃圾回收篇(一)
JVM 1. 引言 1.1 什么是JVM? 定义 Java Vritual Machine - java 程序的运行环境(Java二进制字节码的运行环境) 好处 一次编译 ,到处运行 自动内存管理,垃 ...
- Java内存与垃圾回收调优
Java(JVM)内存模型 正如你从上面的图片看到的,JVM内存被分成多个独立的部分.广泛地说,JVM堆内存被分为两部分——年轻代(Young Generation)和老年代(Old Generat ...
- JVM内存管理------垃圾搜集器参数精解
本文是GC相关的最后一篇,这次LZ只是罗列一下hotspot JVM中垃圾搜集器相关的重点参数,以及各个参数的解释.废话不多说,这就开始. 垃圾搜集器文章传送门 JVM内存管理------JAVA语言 ...
随机推荐
- poj 1845 【数论:逆元,二分(乘法),拓展欧几里得,费马小定理】
POJ 1845 题意不说了,网上一大堆.此题做了一天,必须要整理一下了. 刚开始用费马小定理做,WA.(poj敢说我代码WA???)(以下代码其实都不严谨,按照数据要求A是可以等于0的,那么结果自然 ...
- SDUT-2121_数据结构实验之链表六:有序链表的建立
数据结构实验之链表六:有序链表的建立 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 输入N个无序的整数,建立一个有序链 ...
- SDUT-2122_数据结构实验之链表七:单链表中重复元素的删除
数据结构实验之链表七:单链表中重复元素的删除 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 按照数据输入的相反顺序(逆 ...
- 终端安装opencv
安装 要想在 notebook 中安装和使用 OpenCV,请打开终端窗口(也被称为 Windows 用户的命令提示符窗口),并使用以下命令通过 conda 安装最新版本 (v3): conda in ...
- Facebook F8|闲鱼高级技术专家参会分享
笔者代表闲鱼参加了Facebook在4月30日举行的为期二天的F8大会,地点加州.将会议概括和一些收获分享给大家.对国内开发者而言,Facebook的产品设计.社区.VR/AR等有一些借鉴意义:对海外 ...
- 模板—BSGS
#include<iostream> #include<cstdio> #include<cmath> #include<map> #define LL ...
- python项目管理
Python 通常没有对应 Java 的 Ant / Maven 这样的 build tool,有一个用于打包的 setuptools / distutils 但也并不完全等价.如果是用来管理依赖包, ...
- hdu 3374 String Problem (字符串最小最大表示 + KMP求循环节)
Problem - 3374 KMP求循环节. http://www.cnblogs.com/wuyiqi/archive/2012/01/06/2314078.html 循环节推导的证明相当 ...
- Python--day69--ORM的F查询和Q查询
F查询和Q查询 F查询 在上面所有的例子中,我们构造的过滤器都只是将字段值与某个常量做比较.如果我们要对两个字段的值做比较,那该怎么做呢? Django 提供 F() 来做这样的比较.F() 的实例可 ...
- poj2632 累死了
题意: 给定A*B的格子,放入N个机器人,每个机器人初始位置及朝向给定.给定M条指令.指令类型有三种: 1.L:左转90° 2.R:右转90° 3.F:前进一格 问执行指令过程中 ...