Python基础30-面向对象(内存管理机制-引用计数/垃圾回收/循环引用/弱引用)

1 对象存储

  1. 在Python中万物皆对象
不存在基本数据类型,`0,  1.2,  True, False, "abc"`等,这些全都是对象
  1. 所有对象, 都会在内存中开辟一块空间进行存储
2.1 会根据不同的类型以及内容, 开辟不同的空间大小进行存储
2.2 返回该空间的地址给外界接收(称为"引用"), 用于后续对这个对象的操作
2.3 可通过 id() 函数获取内存地址(10进制)
2.4 通过 hex() 函数可以查看对应的16进制地址
class Person:
    pass

p = Person()
print(p)
print(id(p))
print(hex(id(p)))

>>>> 打印结果

<__main__.Person object at 0x107030470>
4412605552
0x107030470
  1. 对于整数和短小的字符, Python会进行缓存; 不会创建多个相同对象
此时, 被多次赋值, 只会有多份引用
num1 = 2
num2 = 2
print(id(num1), id(num2))

>>>> 打印结果

4366584464 4366584464
  1. 容器对象, 存储的其他对象, 仅仅是其他对象的引用, 并不是其他对象本身
4.1 比如字典, 列表, 元组这些"容器对象"
4.2 全局变量是由一个大字典进行引用
4.3 可通过 global() 查看

2 对象回收

2.1 引用计数器

2.1.1概念

  • 一个对象, 会记录着自身被引用的个数
  • 每增加一个引用, 这个对象的引用计数会自动+1
  • 每减少一个引用, 这个对象的引用计数会自动-1

2.1.2 计数器变化常见场景

  • 引用计数+1场景
1、对象被创建
    p1 = Person()
2、对象被引用
    p2 = p1
3、对象被作为参数,传入到一个函数中
    log(p1)
    这里注意会+2, 因为内部有两个属性引用着这个参数
4、对象作为一个元素,存储在容器中
    l = [p1]
  • +1 情况3说明:内部有两个属性引用着这个参数
    Python2.x 下打印 :_globals__func_globals 引用该参数对象,计数+2
    Python3.x 下打印:则只有一个_globals__引用该对象,同样计数+2
import sys

class Person:
    pass

p_xxx = Person() # 1

print(sys.getrefcount(p_xxx))


def log(obj):
    print(sys.getrefcount(obj))

log(p_xxx)


# for attr in dir(log):
#     print(attr, getattr(log, attr))


>>>> 打印结果

2
4
  • 引用计数-1场景
1、对象的别名被显式销毁
    del p1
2、对象的别名被赋予新的对象
    p1 = 123
3、一个对象离开它的作用域
    一个函数执行完毕时
    内部的局部变量关联的对象, 它的引用计数就会-1
4、对象所在的容器被销毁,或从容器中删除对象

2.1.3 查看引用计数

  • 注意计数器会>1,因为对象在 getrefcount方法中被引用
import sys
sys.getrefcount(对象) 
import sys

class Person:
    pass

p1 = Person() # 1

print(sys.getrefcount(p1)) # 2

p2 = p1 # 2

print(sys.getrefcount(p1)) # 3

del p2 # 1
print(sys.getrefcount(p1)) # 2

del p1
# print(sys.getrefcount(p1)) #error,因为上一行代码执行类p1对象已经销毁

>>>> 打印结果

2
3
2

2.2 循环引用

  • 对象间互相引用,导致对象不能通过引用计数器进行销毁
# 循环引用
class Person:
    pass

class Dog:
    pass

p = Person() 
d = Dog()   

p.pet = d 
d.master = p
  • 使用 objgraph 模块
  • objgraph.count() 可以查看, 垃圾回收器, 跟踪的对象个数
# 正常情况
import objgraph

class Person:
    pass


class Dog:
    pass

p = Person()
d = Dog()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

# 删除 p, d之后, 对应的对象是否被释放掉
del p
del d

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果

1
1
0
0

# 循环引用
import objgraph

class Person:
    pass


class Dog:
    pass

p = Person()
d = Dog()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

p.pet = d
d.master = p

# 删除 p, d之后, 对应的对象是否被释放掉
del p
del d

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果

1
1
1
1

2.2 垃圾回收

2.2.1 主要作用

  • 从经历过 引用计数器机制 仍未被释放的对象中, 找到 循环引用 对象, 并回收相关对象

2.2.2 垃圾回收底层机制

2.2.2.1 如何找到 循环引用 对象

  1. 收集所有的"容器对象", 通过一个双向链表进行引用
* 容器对象
    可以引用其他对象的对象
        列表
        元组
        字典
        自定义类对象
        ...
* 非容器对象
    不能引用其他对象的对象
        数值
        字符串
        布尔
        ...
    注意: 针对于这些非容器对象的内存, 有其他的管理机制
  1. 针对于每一个"容器对象", 通过一个变量gc_refs来记录当前对应的引用计数
  2. 对于每个"容器对象",找到它引用的"容器对象", 并将这个"容器对象"的引用计数 -1
  3. 经过步骤3之后, 如果一个"容器对象"的引用计数为0, 就代表这个对象可以被回收, 而它肯定是因为"循环引用"导致不能被回收(存活到现在)

2.2.2.2 如何提升查找"循环引用"的性能?

1 问题:

  • 如果程序当中创建了很多个对象, 而针对于每一个对象都要参与"检测"过程; 则会非常的耗费性能

2 假设:

  • 基于这个问题, 产生了一种假设:
1. 越命大的对象, 越长寿
2. 假设一个对象10次检测都没给它干掉, 就认定这个对象一定很长寿, 就减少这货的"检测频率"

3 设计机制:

  • 分待回收 机制
1. 默认一个对象被创建出来后, 属于 0 代
2. 如果经历过这一代"垃圾回收"后, 依然存活, 则划分到下一代
3. "垃圾回收"的周期顺序为
    0代"垃圾回收"一定次数, 会触发 0代和1代回收
    1代"垃圾回收"一定次数, 会触发0代, 1代和2代回收

2.2.2.3 垃圾检测时机

  • 垃圾回收器当中, 新增的对象个数-消亡的对象个数 , 达到一定的阈值时, 才会触发, 垃圾检测
  • 通过 gc 模块查看当前垃圾回收机制促发条件及设置条件
import gc

# 获取
print(gc.get_threshold())

>>>> 打印结果
(700, 10, 10) 
# 参数1,700;代表:新增的对象个数-消亡的对象个数 == 700 时会促发垃圾检测时机
# 参数2,10;代表:当第0代对象检测次数达到10次时候,会促发0代和1代对象的检测
# 参数3,10;代表:当第1代对象检测次数达到10次时候,会促发0代、1代和2代对象的检测


# 设置
gc.set_threshold(200, 5, 5)
print(gc.get_threshold())

>>>> 打印结果
(200, 5, 5)

2.2.3 垃圾回收时机

2.2.3.1 自动回收

  1. 触发条件
* 开启垃圾回收机制
* 且达到启动垃圾回收对应的阈值
  1. 开启垃圾回收机制
gc.enable()
    开启垃圾回收机制(默认开启)
gc.disable()
    关闭垃圾回收机制
gc.isenabled()
    判定是否开启
  1. 启动垃圾回收对应的阈值
  • 垃圾回收器中, 新增的对象个数和释放的对象个数之差到达某个阈值
查看方法
    gc.get_threshold()
        获取自动回收阈值
    gc.set_threshold()
        设置自动回收阈值

# 自动回收
import gc

gc.disable()
print(gc.isenabled())

gc.enable()

print(gc.isenabled())

print(gc.get_threshold())
gc.set_threshold(1000, 15, 5)

一般会将对应的阈值设置成更大的值,这样可以提高程序性能

2.2.3.2 手动回收

  1. 触发条件
  • 调用 gc 模块的collection() 方法
gc.collect(generation=None)
"""
    collect([generation]) -> n
    
    With no arguments, run a full collection.  The optional argument
    may be an integer specifying which generation to collect.  A ValueError
    is raised if the generation number is invalid.
    
    The number of unreachable objects is returned.
    """
  • generation 参数:如果为空时,则表明全代回收;指定代数的话,则会检测该代之前的所有对象,如:generation = 2,则检测0、1和2代的对象
  • 不管之前垃圾回收机制是否开启,调用 collection() 方法后都会执行一次垃圾回收
  1. 通过 objgraph 查看该类实例引用计数
import objgraph
class Person:
    pass

class Dog:
    pass

p = Person()
d = Dog()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
0
0
  1. 因循环引用情况,引用计数器处理不了
import objgraph

class Person:
    pass

class Dog:
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p

del p
del d

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
1
1
  1. 手动促发垃圾回收,处理循环引用对象
import objgraph
import gc

class Person:
    pass

class Dog:
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p


del p
del d

gc.collect() #手动触发垃圾回收

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
0
0
  1. 即使将垃圾回收机制关闭,调用gc.collect() 时候都会启动触发垃圾回收,执行完后并未自动开启
import objgraph
import gc

gc.disable() #手动关闭垃圾回收

class Person:
    pass

class Dog:
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p


del p
del d

gc.collect() #手动启动垃圾回收

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

print(gc.isenabled()) # 检查关闭后是否自动开启

>>>> 打印结果
0
0
False

3 循环引用 - 版本兼容方案、弱引用打破循环引用

  1. 循环引用通过手动启动垃圾回收机制进行回收
import objgraph
import gc

# 1. 定义了两个类
class Person:
    pass

class Dog:
    pass

# 2. 根据这两个类, 创建出两个实例对象
p = Person()
d = Dog()

# 3. 让两个实例对象之间互相引用, 造成循环引用
p.pet = d
d.master = p

# 4. 尝试删除可到达引用之后, 测试真实对象是否有被回收
del p
del d

# 5. 通过"引用计数机制"无法回收; 需要借助"垃圾回收机制"进行回收
gc.collect()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
0
0
  1. 可以通过重写类方法 __del__ 查看类实例被释放时标识
  • Python3.x 环境
import objgraph
import gc

class Person:
    def __del__(self):
        print("Person对象, 被释放了")
    pass

class Dog:
    def __del__(self):
        print("Dog对象, 被释放了")
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p

del p
del d

gc.collect()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
Person对象, 被释放了
Dog对象, 被释放了
0
0
  • Python2.x 环境时:
    如果循环引用对象任意一个类里面实现了__del__方法,都会导致垃圾回收机制无法正常运行
    并且不会执行__del__方法
import objgraph
import gc

class Person:
    def __del__(self):
        print("Person对象, 被释放了")
    pass

class Dog:
    def __del__(self):
        print("Dog对象, 被释放了")
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p

del p
del d

gc.collect()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
1
1
  • Python2.x 中循环引用类都没有重写__del__时,则垃圾回收机制正常
import objgraph
import gc

class Person:
    # def __del__(self):
    #     print("Person对象, 被释放了")
    pass

class Dog:
    # def __del__(self):
    #     print("Dog对象, 被释放了")
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p

del p
del d

gc.collect()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
0
0

4 打破循环引用的方法

  1. 手动强制将循环引用的属性置 None。 p.pet = None
    Python2.x 环境
import objgraph
import gc

# 1. 定义了两个类
class Person:
    def __del__(self):
        print("Person对象, 被释放了")
    pass

class Dog:
    def __del__(self):
        print("Dog对象, 被释放了")
    pass

p = Person()
d = Dog()

p.pet = d
d.master = p

p.pet = None #强制置 None
del p
del d

gc.collect()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果
Dog对象, 被释放了
Person对象, 被释放了
0
0

  1. 通过弱引用方式打破循环引用
    Python2.x 环境
  • 导入 weakref 模块
  • 一对一弱引用 d.master = weakref.ref(p)

import objgraph
import gc
import weakref

# 1. 定义了两个类
class Person:
    def __del__(self):
        print("Person对象, 被释放了")
    pass

class Dog:
    def __del__(self):
        print("Dog对象, 被释放了")
    pass

p = Person()
d = Dog()

p.pet = d
d.master = weakref.ref(p)

del p
del d

gc.collect()

print(objgraph.count("Person"))
print(objgraph.count("Dog"))

>>>> 打印结果

Person对象, 被释放了
Dog对象, 被释放了
0
0
  • 弱引用一对多
# 单个写法
p.pets = {"dog":  weakref.ref(d1), "cat":  weakref.ref(c1)}
或
字典形式
p.pets = weakref.WeakValueDictionary({"dog": d1, "cat": c1})

一般还有 如下方法
WeakKeyDictionary
WeakSet
···
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 生命周期的概念:世界上的万事万物都有它的生命周期,那么针对对象的生命周期到底是从哪里开始从哪里结束呢?当我们创建一...
    hello_我的哥阅读 7,689评论 0 5
  • python内存管理是通过引用计数来实现的。当对象的引用计数为0时,会被gc回收。 为了探索对象在内存的存储,我们...
    冬季恋歌1218阅读 1,649评论 0 2
  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,211评论 4 16
  • http://python.jobbole.com/85231/ 关于专业技能写完项目接着写写一名3年工作经验的J...
    燕京博士阅读 7,562评论 1 118
  • 碧纱窗外, 两树梅英, 一瓣馨香。 闲庭院冷, 叶落轻盈, 千里月华浓。 草露多情, 芒花随性, 散作天涯归路。 ...
    微苒阅读 459评论 0 4