安迪_963

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

内存管理机制

python中垃圾回收机制主要有三方面:引用记数主,标记清除,分代回收为辅

引用计数(没有人记得你时,才是真正的死亡)

在python中一切皆为对象,每个对象都维护一个引用次数,如果次数为零,即没有任何引用,它将被回收机制无情的收割(没有人赢得你时,才是真正的死亡.鲁迅也曾说:有的人死了,但他仍活着,我想也有此意思).下面看看具体代码:

import sys


class Person:
    pass


p = Person() # p被创建,指向Person对象,记数 +1

print("p ref count:", sys.getrefcount(p)) # p作为实参传给函数,记数 +1,总次数为 2
p1 = p  # p1引用 ,记数 +1 总次数为 3
print("p ref count", sys.getrefcount(p)) 
del p1 # 删除 p1对p的引用,次数-1, 总次数为2
print("p ref count", sys.getrefcount(p)) # 2

#输出:
p ref count: 2
p ref count 3
p ref count 2

第一次打印为什么是2?

我们来看源码

def getrefcount(): # real signature unknown; restored from __doc__
    """
    Return the reference count of object.
    
    The count returned is generally one higher than you might expect,
    because it includes the (temporary) reference as an argument to
    getrefcount().
    """
    pass

可以清楚的看到,结果比我们预想的要高一个,是因为变量本身作为getrefcount的临时引用,所以会+1,所以结果为2

函数为什么会引用+2?

我们先看看下面这种情况:

  • 创建 +1
  • getrefcount +1
    那也只有2才对,可是结果为什么是4呢?
import sys


class Person:
    pass

def log_ref(var):
    print(sys.getrefcount(var))

p = Person()
# 输出:
4

在对象传给函数时,函数内部有两个属性func_globals, __globals__都会引用该参数,所以此时该对象的引用计数会 +2. 需要注意的是在python3中我们通过dir无法查看到 func_globals.
在python2中,函数包含两个属性:func_globals, __globals__,在python3中前者的命名发生了改变,具体可以参考: https://docs.python.org/3.1/whatsnew/3.0.html
Operators And Special Methods:

The function attributes named func_X have been renamed to use the X form, freeing up these names in the function attribute namespace for user-defined attributes. To wit, func_closure, func_code, func_defaults, func_dict, func_doc, func_globals, func_name were renamed to closure, code, defaults, dict, doc, globals, name, respectively.

既然对象作为参数传递给函数引用会+2,那么下面这段代码为结果为什么是2?

很明显 getrefcount也是函数,那打印结果应该是3才对,这是因为getrefcount会自动处理这种情况

import sys


class Person:
    pass


p = Person()
print("p ref count:", sys.getrefcount(p))

作为容器的元素的场景

这里的容器以列表为例:

import sys


class Person:
    pass


p = Person()

l = [p, ]

print(sys.getrefcount(p))
# 输出3

+1场景总结:

引用记数+1场景:

  • 对象被创建
  • 对象被引用
  • 对象作为参数,传入函数中
  • 对象作为对象存储在容器中

对象被显式销毁

主动将你忘记.但此时你已经不存在,无法通过getrefcount()来测试引用数量

对象的引用被指新的对象

没错,你被绿了.你对象移情别恋了,她心里只有另一个人了,记得你的人就少了一个

import sys


class Person:
    pass


you = Person()
your_gf = you
print(sys.getrefcount(you))  # 你对象还爱你的时候
another_handsome_boy = Person()
your_gf = another_handsome_boy
print(sys.getrefcount(you))  # 你对象爱上高富帅的时候
#输出:
3
2

离开作用域

在getrefcount函数中,记数会+1,那如果这样,我不停打印不会就不断增加吗?但离开了getrefcount的世界,它就把你忘记了

import sys


class Person:
    pass


you = Person()
print(sys.getrefcount(you))
print(sys.getrefcount(you))
#输出
2
2

销毁容器

当你的世界被销毁时:

import sys


class Person:
    pass


p = Person()

l = [p, ]
del l
print(sys.getrefcount(p))
# 输出2

-1场景总结

  • 显式销毁
  • 引用被指向新的对象
  • 离开作用域
  • 容器被销毁

标记清除

引用记数无法解决的问题:

import sys


class Person:
    pass


you = Person()
your_gf = Person()
you.gf = your_gf
your_gf.bf = you
print(sys.getrefcount(you))
print(sys.getrefcount(your_gf))
# 输出
3
3

除去你getrefcount引用,你和你对象相亲相爱,所以每人有两个引用.此时即出现了循环引用.
我们看官方文档: 只有容器类型,会存在这种循环引用,而对于简单原子数据类型如 数字,字符串不支持垃圾回收,或者不存储对其它对象引用的容器也不支持.

Python’s support for detecting and collecting garbage which involves circular references requires support from object types which are “containers” for other objects which may also be containers. Types which do not store references to other objects, or which only store references to atomic types (such as numbers or strings), do not need to provide any explicit support for garbage collection.

ref: https://docs.python.org/3.1/c-api/gcsupport.html?highlight=circular reference

如果我们显式删除,会导致无法查看getrefcount, 我们借助第三方库来查看对对象的引用数,注意count的参数为字符串:

pip install objgraph

import sys
import objgraph


class Person:
    pass


you = Person()
your_gf = Person()
print(objgraph.count("Person"))


del your_gf
print(objgraph.count("Person"))
# 输出
2
1

循环引用

彼此相爱的两人,任谁也分不开

import sys
import objgraph


class Person:
    pass


you = Person()
your_gf = Person()
print(objgraph.count("Person"))

you.gf = your_gf
your_gf.bf = you

del you
del your_gf
print(objgraph.count("Person"))

# 输出
2
2

借助objgraph 打印出这种节点图:

import sys
import objgraph


class Boy:
    pass


class Girl:
    pass


you = Boy()
your_gf = Girl()
print(objgraph.count("Boy"))
print(objgraph.count("Girl"))
you.gf = your_gf
your_gf.bf = you
# del you
# del your_gf
print(objgraph.count("Boy"))
print(objgraph.count("Girl"))
objgraph.show_backrefs([you,your_gf])

上面的代码中虽然在最后的print语句时仍能打印,但如果我们删除了you, your_gf则也无法打印出图形,会报错
我们通过引用图:
https://github.com/Andy963/notePic/blob/main/circular_ref.png

python的解决办法

python会收集所有的容器对象,放在一个双向链表中,将一个对象和它引用的对象的引用数都-1,如果它们的引用数变成0,则说明它们之间存在循环引用,那么这两个对象将标记出来,并被无情清除. 如果你和你的对象私定终生,他们总有办法发现的,尤其是他们不同意的时候.

分代回收

Python解释器在垃圾回收时,会遍历链表中的每个对象,如果存在循环引用,就将存在循环引用的对象的引用计数器 -1,同时Python解释器也会将计数器等于0(可回收)和不等于0(不可回收)的一分为二,把计数器等于0的所有对象进行回收,把计数器不为0的对象放到另外一个双向链表表(即:分代回收的下一代)
分代回收的代,有三代,按年轻到老的顺序为:0代,1代,2代
门限,有三个门限 ,门限0,门限1,门限2,默认情况下为700,10,10

import gc

print(gc.get_threshold())
# (700, 10, 10)

第一个参数表示:垃圾回收器中新增的对象个数-消亡的对象个数,当这个值达到700以上时,会触发检测机制.
简单点讲:当新生儿出生数,减去死亡人数大于700,将导致这种检测(这时候就该计划生育了),然后开始检测,当0代的检测10次后(你只生了一胎,不信,检测10次,确定你只生了一胎),才会检测1代(此时0代的检测到第11次了),然后1代检测10次后才会检测2代(此时0代已经检测到101次).

The GC classifies objects into three generations depending on how many collection sweeps they have survived. New objects are placed in the youngest generation (generation 0). If an object survives a collection it is moved into the next older generation. Since generation 2 is the oldest generation, objects in that generation remain there after a collection. In order to decide when to run, the collector keeps track of the number object allocations and deallocations since the last collection. When the number of allocations minus the number of deallocations exceeds threshold0, collection starts. Initially only generation 0 is examined. If generation 0 has been examined more than threshold1 times since generation 1 has been examined, then generation 1 is examined as well. Similarly, threshold2 controls the number of collections of generation 1 before collecting generation 2.

如果要修改这些门限: 调用set_threshold()即可

启用回收

垃圾回收机制默认是开启的:

import gc

print(gc.isenabled()) # True
gc.disable()

手动回收

我们以前面的循环引用为例:

import gc
import objgraph


class Boy:
    pass


class Girl:
    pass


you = Boy()
your_gf = Girl()
print(objgraph.count("Boy"))
print(objgraph.count("Girl"))
you.gf = your_gf
your_gf.bf = you
del you
del your_gf
gc.collect()
print(objgraph.count("Boy"))
print(objgraph.count("Girl"))

在未手动触发垃圾回收时,两次都输出

1
1
1
1

启用后则是:

1
1
0
0
posted on 2021-04-17 23:31  Andy_963  阅读(81)  评论(0编辑  收藏  举报