ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

为什么 del 一个对象有时立刻消失,有时却纹丝不动?一文讲透引用计数与垃圾回收

2026/9/2 6:32:29 拓冰建站 浏览量
为什么 del 一个对象有时立刻消失,有时却纹丝不动?一文讲透引用计数与垃圾回收 「Python 进阶之路」系列 Day22写在前面模块四把并发编程讲完了从今天开始进入模块五——内存管理与性能。第一篇先解决一个基础但很多人说不清楚的问题Python 的对象到底是怎么被回收的为什么有时候del一个对象立刻就触发了销毁有时候明明del了却好像什么都没发生答案就在引用计数和垃圾回收这两套机制的分工里。一、是什么引用计数是主力分代GC是兜底CPython 的内存管理机制由两部分组成引用计数Reference Counting每个对象内部维护一个计数器记录有多少个地方引用着它。每多一个引用计数加一每少一个引用计数减一计数归零的瞬间对象立即被销毁这是内存回收的主力机制分代垃圾回收Generational Garbage Collector引用计数解决不了循环引用问题两个对象互相引用即使外部都不再需要它们彼此的计数也不会归零需要一个专门的回收器定期扫描、找出这类死循环对象并清理掉这是引用计数的兜底方案Day23 细讲怎么解决循环引用二、为什么Python选择引用计数及时、可预测用引用计数做主力回收机制的好处是内存释放及时、行为可预测——对象一旦没人用了立刻就被回收不需要像纯粹的标记清除式 GC 那样攒到某个时机才统一扫描回收程序不会因为等 GC 而突然卡顿。代价是每一次赋值、传参、放入容器都要维护那个计数器有一定的性能开销而且引用计数天生处理不了循环引用需要额外的机制兜底——这就是为什么 Python 同时具备引用计数和分代 GC 两套机制各自负责一部分工作。三、怎么用1. 实测引用计数什么时候增加、减少importsys aobject()print(sys.getrefcount(a))# 2 —— 注意getrefcount本身传参时也会临时多算一次引用baprint(sys.getrefcount(a))# 3 —— b a 增加了一次引用caprint(sys.getrefcount(a))# 4 —— c a 又增加了一次delbprint(sys.getrefcount(a))# 3 —— del b 减少了一次delcprint(sys.getrefcount(a))# 2 —— 回到最初的值函数传参会临时增加一次引用计数函数返回后这个临时引用消失defshow_count_inside(obj):print(sys.getrefcount(obj))# 3 —— 函数参数多算了一次xobject()print(sys.getrefcount(x))# 2show_count_inside(x)print(sys.getrefcount(x))# 2 —— 函数返回后恢复原值容器持有元素同样会增加被持有对象的引用计数yobject()print(sys.getrefcount(y))# 2lst[y,y,y]# 同一个对象放进列表3次print(sys.getrefcount(y))# 5 —— 增加了3lst.clear()print(sys.getrefcount(y))# 2 —— 清空后恢复2. 引用计数归零对象立即被销毁用__del__方法对象被销毁时自动调用验证销毁的时机classWatched:def__init__(self,name):self.namenamedef__del__(self):print(f{self.name}被销毁了)wWatched(w对象)delw# w对象 被销毁了 ← 这行紧跟着 del w 立刻打印出来del w执行完__del__就立刻打印了销毁信息——不需要等待任何形式的 GC 扫描引用计数归零的那一刻对象就被销毁了这正是引用计数及时这个特点最直接的体现。3. 引用计数处理不了循环引用del之后只剩循环引用NodeANodeBdel之前有外部引用外部变量n1NodeA外部变量n2NodeBclassNode:def__init__(self,name):self.namename self.otherNonedef__del__(self):print(fNode{self.name}被销毁了)n1Node(n1)n2Node(n2)n1.othern2# n1 引用 n2n2.othern1# n2 引用 n1形成循环引用deln1deln2# 这里什么都没打印n1、n2 互相引用着对方各自的引用计数都没有归零importgc collectedgc.collect()# Node n1 被销毁了# Node n2 被销毁了print(collected)# 2 —— gc.collect() 手动触发后才真正清理掉del n1、del n2只是删掉了外部变量这两个引用但n1和n2内部还互相持有着对方n1.other指向n2n2.other指向n1它们的引用计数都还是 1永远不会自然归零——单靠引用计数这两个对象会一直卡在内存里直到gc.collect()分代垃圾回收器介入识别出这个循环引用的孤岛并清理掉。这也就回答了开头的问题del一个普通对象通常会立刻销毁但如果这个对象牵涉在循环引用里del只是拿走了外部引用对象本身要等分代 GC 扫描到才会被真正清理。4. gc模块分代回收基础用法importgcprint(gc.get_count())# (1, 0, 0) —— 三代对象数量年轻代、中年代、老年代print(gc.get_threshold())# (700, 10, 10) —— 三代各自的回收触发阈值Python 的分代 GC 基于分代假设大部分对象存活时间很短活得越久的对象越不容易变成垃圾。所以把对象分成三代新创建的对象放进第 0 代年轻代扫描得勤阈值低700 个新对象就触发一次扛过扫描没被回收的对象晋升到下一代老年代扫描得少阈值高这样能在保证正确性的同时尽量减少扫描的开销只重点盯着最可能变成垃圾的那批年轻对象。四、面试追问Q1Python 的内存管理机制主要是什么以引用计数为主力每个对象内部维护一个引用计数器计数归零时对象立即被销毁以分代垃圾回收为兜底定期扫描找出引用计数解决不了的循环引用并清理掉两套机制配合工作。Q2引用计数的优缺点分别是什么优点是内存释放及时、行为可预测对象一旦没人用了立刻就被回收不会因为等待 GC 扫描而导致程序卡顿缺点是每次赋值、传参、放入容器都要维护这个计数器带来一定的性能开销而且天生处理不了循环引用问题。Q3什么时候一个对象的引用计数会增加/减少增加的场景变量赋值指向它b a、作为参数传给函数、被放进容器list、dict 等减少的场景变量被del或重新赋值指向别的对象、变量离开作用域、从容器里被移除。Q4为什么需要分代垃圾回收它解决了什么引用计数解决不了的问题解决循环引用问题——两个或多个对象互相引用即使外部不再有任何引用指向它们彼此之间的引用计数也不会归零单靠引用计数永远不会主动回收这类对象需要分代垃圾回收器定期扫描识别并清理这些孤立的引用环。Q5怎么手动触发垃圾回收什么场景需要调用gc.collect()可以手动触发一次完整扫描。一般场景不需要手动干预Python 会按各代的阈值自动触发但在明确知道刚产生了大量循环引用、并且希望立刻释放这部分内存的场景比如处理完一批带有循环引用的大对象之后可以主动调用一次。下一篇预告Day23 讲循环引用具体是怎么被处理的——gc模块底层的检测原理以及用weakref弱引用从源头上避免循环引用产生的正确姿势。