当前位置:首页 > 科技  > 软件

一个 Python 对象会在何时被销毁?

来源: 责编: 时间:2024-05-29 17:32:02 304观看
导读楔子如果对编程语言进行分类的话,一般可以分为静态语言和动态语言,也可以分为编译型语言和解释型语言。但个人觉得还可以有一种划分标准,就是是否自带垃圾回收。关于有没有垃圾回收,陈儒老师在《Python 2.5源码剖析》中,总

楔子

EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

如果对编程语言进行分类的话,一般可以分为静态语言和动态语言,也可以分为编译型语言和解释型语言。但个人觉得还可以有一种划分标准,就是是否自带垃圾回收。关于有没有垃圾回收,陈儒老师在《Python 2.5源码剖析》中,总结得非常好。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

对于像 C 和 C++ 这类语言,程序员被赋予了极大的自由,可以任意地申请内存。但权力的另一面对应着责任,程序员最后不使用的时候,必须负责将申请的内存释放掉,并把无效指针设置为空。可以说,这一点是万恶之源,大量内存泄漏、悬空指针、越界访问的 bug 由此产生。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

而现代的开发语言(比如 C#、Java)都带有垃圾回收机制,将开发人员从维护内存分配和清理的繁重工作中解放出来,开发者不用再担心内存泄漏的问题,但同时也剥夺了程序员和内存亲密接触的机会,并牺牲了一定的运行效率。不过好处就是提高了开发效率,并降低了 bug 发生的概率。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

由于现在的垃圾回收机制已经非常成熟了,把对性能的影响降到了最低,因此大部分场景选择的都是带垃圾回收的语言。EyQ28资讯网——每日最新资讯28at.com

而 Python 里面同样具有垃圾回收,只不过它是为引用计数机制服务的。所以解释器通过内部的引用计数和垃圾回收,代替程序员进行繁重的内存管理工作,关于垃圾回收我们后面会详细说,先来看一下引用计数。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

引用计数

EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

Python 一切皆对象,所有对象都有一个 ob_refcnt 字段,该字段维护着对象的引用计数,从而也决定对象的存在与消亡。下面来探讨一下引用计数,当然引用计数在介绍 PyObject 的时候说的很详细了,这里再回顾一下。EyQ28资讯网——每日最新资讯28at.com

但需要说明的是,比起类型对象,我们更关注实例对象的行为。引用计数也是如此,只有实例对象,我们探讨引用计数才是有意义的。EyQ28资讯网——每日最新资讯28at.com

因为内置的类型对象超越了引用计数规则,永远都不会被析构,或者销毁,因为它们在底层是被静态定义好的。EyQ28资讯网——每日最新资讯28at.com

图片图片EyQ28资讯网——每日最新资讯28at.com

很明显,内置的类型对象属于永恒对象。关于永恒对象之前解释过,指的是那些永远不会被回收的对象,像 None、小整数对象池里面的整数、以及内置的类型对象,它们都是永恒对象。EyQ28资讯网——每日最新资讯28at.com

如果对象是永恒对象,那么它的引用计数会直接被初始化为 uint32 最大值。当然,如果一个对象原本不是永恒对象,但它的引用计数之后达到了 uint32 最大值(有 2 ** 32 - 1 个变量在引用它),那么它也会被判定为永恒对象,但很明显这只是理论情况,现实不可能出现,因为一个对象不可能有这么多的变量在引用它。EyQ28资讯网——每日最新资讯28at.com

同理,我们自定义的类,虽然可以被回收,但是探讨它的引用计数也是没有价值的。我们举个栗子:EyQ28资讯网——每日最新资讯28at.com

class A:    passdel A

首先 del 关键字只能作用于变量,不可以作用于对象,比如 e = 2.71,可以 del e,但是不可以 del 2.71,这是不符合语法规则的。因为 del 的作用是删除变量,并让其指向对象的引用计数减 1,所以我们只能 del 变量,不可以 del 对象。EyQ28资讯网——每日最新资讯28at.com

同样的,使用 def、class 关键字定义完之后拿到的也是变量,比如上面代码中的 A,只要是变量,就可以被 del。但是 del 变量只是删除了该变量,换言之就是让该变量无法再被使用,至于变量指向的对象是否会被回收,就看是否还有其它的变量也指向它。EyQ28资讯网——每日最新资讯28at.com

总结:对象是否被回收完全由解释器判断它的引用计数是否为 0 所决定。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

永恒对象

EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

我们一直说对象的 ob_refcnt 字段负责维护引用计数,当然这是没问题的。但 Python 从 3.12 开始又引入了 ob_refcnt_split 字段,也负责维护引用计数。EyQ28资讯网——每日最新资讯28at.com

图片图片EyQ28资讯网——每日最新资讯28at.com

ob_refcnt_split 是一个长度为 2、类型为 uint32 的数组,但只会用其中一个元素来维护引用计数。如果达到了 uint32 最大值,那么判定为永恒对象,相关源码后续聊。EyQ28资讯网——每日最新资讯28at.com

我们来看看永恒对象的初始化过程,以 list 类型对象为例,看看它的引用计数是怎么设置的。EyQ28资讯网——每日最新资讯28at.com

// Objects/listobject.c// 引用计数和类型由宏 PyVarObject_HEAD_INIT 负责设置PyTypeObject PyList_Type = {    PyVarObject_HEAD_INIT(&PyType_Type, 0)    "list",    sizeof(PyListObject),    0,    ...};    // Include/object.h#define PyVarObject_HEAD_INIT(type, size) /    {                                     /        PyObject_HEAD_INIT(type)          /        (size)                            /    },#define PyObject_HEAD_INIT(type)    /    {                               /        { _Py_IMMORTAL_REFCNT },    /        (type)                      /    },    #define _Py_IMMORTAL_REFCNT UINT_MAX

我们看到类型对象在初始化的时候,引用计数直接被设置成了 uint32 最大值。当然啦,这并不是说有 2 ** 32 - 1 个变量在引用,而是通过将引用计数设置为 uint32 最大值,来表示这是一个不会被销毁的永恒对象。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

源码解密引用计数的相关操作

EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

操作引用计数无非就是将其加一或减一,至于什么时候加一、什么时候减一,在介绍 PyObject 的时候已经说的很详细了,可以看一下。这里我们通过源码,看看引用计数具体是怎么操作的。EyQ28资讯网——每日最新资讯28at.com

在底层,解释器会通过 Py_INCREF 和 Py_DECREF 两个函数来增加和减少对象的引用计数,而当对象的引用计数减少到 0 后,Py_DECREF 将调用对应的析构函数来释放该对象所占的内存和系统资源。这个析构函数由对象的类型对象中定义的函数指针来指定,也就是 tp_dealloc。EyQ28资讯网——每日最新资讯28at.com

下面我们来看看底层实现,不过在介绍 Py_INCREF 和 Py_DECREF 之前,先来看几个其它的函数,这些函数非常常见,有必要单独说一下。EyQ28资讯网——每日最新资讯28at.com

// Include/object.h// 返回对象的引用计数,说白了就是获取对象的 ob_refcnt 字段// 因为该字段负责维护引用计数static inline Py_ssize_t Py_REFCNT(PyObject *ob) {    return ob->ob_refcnt;}// 设置对象的引用计数static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {    // 如果对象是永恒对象,那么直接返回    // 不会再对永恒对象的引用计数做任何设置    if (_Py_IsImmortal(ob)) {        return;    }    ob->ob_refcnt = refcnt;}// 返回对象的类型,获取 ob_type 字段static inline PyTypeObject* Py_TYPE(PyObject *ob) {    return ob->ob_type;}// 设置对象的类型static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) {    ob->ob_type = type;}// 返回对象的 ob_sizestatic inline Py_ssize_t Py_SIZE(PyObject *ob) {    // _PyVarObject_CAST(ob) 等价于 (PyVarObject *)(ob)    return  _PyVarObject_CAST(ob)->ob_size;}// 设置对象的 ob_sizestatic inline void Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) {    ob->ob_size = size;}

这几个函数是用来设置引用计数、类型和 ob_size 的,比较简单,即使不看源码也能猜出内部都做了什么。需要注意的是,这些函数在之前的 Python 源码中都是以宏的形式存在,但在 3.12 里面变成内联函数了,本质上没有太大差异。EyQ28资讯网——每日最新资讯28at.com

然后来看看 Py_INCREF 和 Py_DECREF,它们负责对引用计数执行加一和减一操作。EyQ28资讯网——每日最新资讯28at.com

注意:这两个函数里面存在宏判断,我们这里只保留判断之后的结果。EyQ28资讯网——每日最新资讯28at.com

// Include/object.hstatic inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op){       // ob_refcnt_split 是长度为 2 的数组,但只会使用一个元素    // 至于使用哪一个,则取决于字节序,是大端存储还是小端存储    PY_UINT32_T cur_refcnt = op->ob_refcnt_split[PY_BIG_ENDIAN];    // 将当前引用计数加一    PY_UINT32_T new_refcnt = cur_refcnt + 1;    // 如果 cur_refcnt 已经达到了 uint32 最大值,那么加一之后会产生环绕,继续从零开始    // 所以如果 new_refcnt 为 0,证明当前对象的引用计数为 uint32 最大值    // 那么该对象就是永恒对象,而永恒对象不会被回收,引用计数也不再做处理,因此直接返回    if (new_refcnt == 0) {        return;    }    // 否则说明不是引用计数,那么进行更新    op->ob_refcnt_split[PY_BIG_ENDIAN] = new_refcnt;    // 稍后解释    _Py_INCREF_STAT_INC();}

这里估计有人发现了一个问题,就是当前只更新了 ob_refcnt_split,而没有更新 ob_refcnt。原因很简单,因为这两个字段组成的是共同体,它们占用同一份内存。EyQ28资讯网——每日最新资讯28at.com

ob_refcnt 是 int64 整数,ob_refcnt_split 是长度为 2 的 uint32 数组,它们都是 8 字节,并且占用的是同一份 8 字节的内存。所以 ob_refcnt_split 里面的两个元素正好对应 ob_refcnt 的低 32 位和高 32 位。EyQ28资讯网——每日最新资讯28at.com

因此在修改 ob_refcnt_split 的时候,同时也修改了 ob_refcnt,所以整个操作只进行了一次。并且从源码中也可以看出,对象的引用计数不会超过 uint32 最大值,因为当达到这个值的时候会被判定为永恒对象,而永恒对象的引用计数不会再做任何操作,因为永恒对象会永远存在。EyQ28资讯网——每日最新资讯28at.com

但还是那句话,除非一开始就将引用计数设置为 uint32 最大值,让对象成为永恒对象,否则单靠创建变量是不可能让对象的引用计数达到这一限制的,因为不管再复杂的项目,也不会出现一个对象被 2 ** 32 - 1 个变量指向的情况,所以 uint32 是完全够用的。EyQ28资讯网——每日最新资讯28at.com

然后在函数的最后出现了一个 _Py_INCREF_STAT_INC 函数,它负责对一些全局统计信息进行更新,目前无需关注。EyQ28资讯网——每日最新资讯28at.com

以上是 Py_INCREF,负责将引用计数加一,再来看看 Py_DECREF,它负责将引用计数减一。EyQ28资讯网——每日最新资讯28at.com

// Include/object.hstatic inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op){    // 如果对象是永恒对象,那么直接返回,因为永恒对象不会被回收    // 它的引用计数不会再发生变化,始终保持 uint32 最大值    if (_Py_IsImmortal(op)) {        return;    }    // 更新一些全局统计信息,和 _Py_INCREF_STAT_INC 作用一样    _Py_DECREF_STAT_INC();    // 重点来了,首先将 ob_refcnt 减一,然后判断它是否等于 0    // 如果为 0,说明对象已经不被任何变量引用了,那么应该被销毁    if (--op->ob_refcnt == 0) {        // 调用 _Py_Dealloc 将对象销毁,这个函数内部的逻辑很简单        // 虽然里面存在很多宏判断,导致代码看起来很复杂        // 但如果只看编译后的最终结果,那么代码就只有下面三行        /*        PyTypeObject *type = Py_TYPE(op);        destructor dealloc = type->tp_dealloc;        (*dealloc)(op);        */        // 会获取类型对象的 tp_dealloc,然后调用,销毁实例对象        _Py_Dealloc(op);    }}

以上就是 Py_INCREF 和 Py_DECREF 两个函数的具体实现,但是它们不能接收空指针,如果希望能接收空指针,那么可以使用另外两个函数。EyQ28资讯网——每日最新资讯28at.com

图片图片EyQ28资讯网——每日最新资讯28at.com

Py_XINCREF 和 Py_XDECREF 会额外对指针做一次判断,如果为空则什么也不做,不为空再调用 Py_INCREF 和 Py_DECREF。EyQ28资讯网——每日最新资讯28at.com

在一个对象的引用计数为 0 时,与该对象对应的析构函数就会被调用。但是要特别注意的是,我们之前说调用析构函数之后会回收对象,或者销毁对象、删除对象等等,意思是将这个对象从内存中抹去,但并不意味着要释放空间。换句话说就是对象没了,但对象占用的内存却有可能还在。EyQ28资讯网——每日最新资讯28at.com

如果对象没了,占用的内存也要释放的话,那么频繁申请、释放内存空间会使 Python 的执行效率大打折扣,更何况 Python 已经背负了人们对其执行效率的不满这么多年。EyQ28资讯网——每日最新资讯28at.com

所以 Python 底层大量采用了缓存池的技术,使用这种技术可以避免频繁地申请和释放内存空间。因此在析构的时候,只是将对象占用的空间归还到缓存池中,并没有真的释放。EyQ28资讯网——每日最新资讯28at.com

这一点,在后面剖析内置实例对象的实现中,将会看得一清二楚,因为大部分内置的实例对象都会有自己的缓存池。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

小结

EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

到此我们的基础概念就算说完了,从下一篇文章开始就要详细剖析内置对象的底层实现了,比如浮点数、复数、整数、布尔值、None、bytes 对象、bytearray 对象、字符串、元组、列表、字典、集合等等,所有的内置对象都会详细地剖析一遍,看看它是如何实现的。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

有了目前为止的这些基础,我们后面就会轻松很多,先把对象、变量等概念梳理清楚,然后再来搞这些数据结构的底层实现。EyQ28资讯网——每日最新资讯28at.com

EyQ28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-91531-0.html一个 Python 对象会在何时被销毁?

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: JDK并发编程类库,有坑!!!

下一篇: 开 1000 个 Web Worker!我的页面能起飞吗?

标签:
  • 热门焦点
  • vivo TWS Air开箱体验:真轻 臻好听

    在vivo S15系列新机的发布会上,vivo的最新款真无线蓝牙耳机vivo TWS Air也一同发布,本次就这款耳机新品给大家带来一个简单的分享。外包装盒上,vivo TWS Air保持了vivo自家产
  • 容量越大越不坏?24万块硬盘故障率报告公布 这些产品零故障

    8月5日消息,云存储服务商Backblaze发布了最新的硬盘故障率报告,年故障率有所上升。Backblaze发布的硬盘季度统计数据,其中包括故障率等重要方面。这些结
  • 之家push系统迭代之路

    前言在这个信息爆炸的互联网时代,能够及时准确获取信息是当今社会要解决的关键问题之一。随着之家用户体量和内容规模的不断增大,传统的靠"主动拉"获取信息的方式已不能满足用
  • 在线图片编辑器,支持PSD解析、AI抠图等

    自从我上次分享一个人开发仿造稿定设计的图片编辑器到现在,不知不觉已过去一年时间了,期间我经历了裁员失业、面试找工作碰壁,寒冬下一直没有很好地履行计划.....这些就放在日
  • JVM优化:实战OutOfMemoryError异常

    一、Java堆溢出堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证 GC Roots 到对象之间有可达路径来避免垃 圾收集回收机制清除这些对象,当这些对象所占空间超过
  • 为什么你不应该使用Div作为可点击元素

    按钮是为任何网络应用程序提供交互性的最常见方式。但我们经常倾向于使用其他HTML元素,如 div span 等作为 clickable 元素。但通过这样做,我们错过了许多内置浏览器的功能。
  • OPPO、vivo、小米等国内厂商Q2在印度智能手机市场份额依旧高达55%

    7月20日消息,据外媒报道,研究机构的报告显示,在全球智能手机出货量同比仍在下滑的大背景下,印度这一有潜力的市场也未能幸免,出货量同比也有下滑,多家厂
  • OPPO K11采用全方位护眼屏:三大护眼能力减轻视觉疲劳

    日前OPPO官方宣布,全新的OPPO K11将于7月25日正式发布,将主打旗舰影像,和同档位竞品相比,其最大的卖点就是将配备索尼IMX890主摄,堪称是2000档位影像表
  • 北京:科技教育体验基地开始登记

      北京“科技馆之城”科技教育体验基地登记和认证工作日前启动。首批北京科技教育体验基地拟于2023年全国科普日期间挂牌,后续还将开展常态化登记。  北京科技教育体验基
Top