美文网首页Python必知必会
Python 内存管理和垃圾回收机制

Python 内存管理和垃圾回收机制

作者: dawsonenjoy | 来源:发表于2020-01-03 11:19 被阅读0次

内存管理机制

可变对象/不可变对象
  • 可变对象:如列表、字典等,本质是不论怎么改变值,他的地址都不会发生改变。
  • 不可变对象:如int、float、bool、str、元组等,当你重新赋值时,在底层他将会修改指向的数据,而不会对原来指向的值进行修改,对原来的值修改的仅时引用次数减一
内存分配原则

python中一切皆对象,在底层中每个对象都是基于结构体实现的,而这些结构体里都有着上下指向、引用计数和数据类型的属性,在初始化时都会给数据的引用计数设置为1,每当多一个指向时,其引用计数就加一,每删除一个他的指向,就引用计数减一,当为0时就会对该数据进行垃圾回收

缓存机制

在python的内存管理当中可能存在一些缓存机制(如:int、float、str、list等都有),即:将某个数据删除时,其可能不会将这个对象完全销毁,而是将对象存放到一个链表中,当又创建同类型的对象时,将会直接赋值给缓存的同类型对象,再通过变量引用指向他,举例:

>>> a = "xxx"
>>> id(a)
2436976108520
>>> del a
# 删除了字符串a
>>> b = "yyy"
# 创建字符串b
>>> id(b)
# 可以发现内存地址时一样的
2436976108520

垃圾回收机制

对象引用次数

一般情况下垃圾回收基于对象引用次数,当初始化时次数为1,被其他对象引用时加1,使用del本质则是将引用次数减一,而当引用次数变成0以后则会自动触发垃圾回收机制将其回收,代码层面上则是会调用该对象的__del__方法,举例:

class D(dict):
    def __init__(self, name):
        self.name = name
        print("对象:{}被创建!".format(self.name))
    def __del__(self):
        print("对象:{}被销毁!".format(self.name))
a = D('a')
a = D('b')
print("程序结束!")

结果:
对象:a被创建!
对象:b被创建!
对象:a被销毁!
程序结束!
对象:b被销毁!

可以看出第一句实例化A时对象被创建,对象的引用计数初始化为1,当第二句执行时,新的A对象被创建,新的对象引用计数为1,而旧的A对象因为a指向了其他数据,所以引用次数减一,此时旧的A对象引用次数变成0,触发销毁机制,从而自动调用了__del__方法。当程序结束时,因为要回收内存,因此新的对象A也自动调用__del__方法。

查看引用次数

可以用sys.getrefcount()方法来查看引用次数,要注意因为将内容传入该方法时引用也会加1,所以我们实际想知道的引用次数应该是输出的结果减一,举例:

>>> a = 1000
>>> sys.getrefcount(a)
# 加上传入方法的a,引用次数为2
2
>>> b = a
>>> sys.getrefcount(a)
# 因为b也引用了a,所以引用次数加1
3
>>> del b
>>> sys.getrefcount(a)
# 删除了b以后引用次数减1
2
标记清除

前面的引用计数能够解决一般情况下的内存回收问题,但是对于循环引用的情况,可能就会无法回收,从而造成内存泄漏的问题,例如下面代码:

class D(dict):
    def __init__(self, name):
        self.name = name
        print("对象:{}被创建!".format(self.name))
    def __del__(self):
        print("对象:{}被销毁!".format(self.name))

a = D('a')
b = D('b')

a['x'] = b
b['x'] = a

a = 1
b = 1
print("程序结束!")

结果:
对象:a被创建!
对象:b被创建!
程序结束!
对象:a被销毁!
对象:b被销毁!

可以看到上面的两个字典类因为互相指向了,所以即使销毁了,引用计数也永远大于0,此时垃圾回收机制也就不起作用了,所以之后即使a和b都指向了其他值,但因为他们原先指向的字典类互相有指向,引用计数不为0,导致他们直到程序结束内存才被回收。此时如果想要回收,那么就需要先收集垃圾,然后再进行回收,Python提供了gc.collect()方法用于手动回收数据,举例:

import gc

class L(list):
    def __del__(self):
        print(self, "end")

a = L([1,2,3])
b = L([1,2,a])
a[-1] = b
del a, b
# 手动回收第0代(后面会介绍分代回收,总共有3代)
print("gc generation 0 nums:", gc.collect(0))
print("end")

# [1, 2, [1, 2, [...]]] end
# [1, 2, [1, 2, [...]]] end
# gc generation 0 nums: 2
# end

可以看到两个互相引用的对象被回收了,而这种手动回收的方式就基于了标记清除来实现:

  1. 首先GC会对所有活动的对象打上标记,即一个个点,然后他们之间的引用通过指向来表明,此时就构成了一个有向图
  2. 然后GC会从根对象出发,沿着有向边遍历整个图,而对于不可达的对象,那么就被视为需要清理的垃圾对象。
分代回收

建立在垃圾清除的基础上,其将对象的活动时间分为3代,新生的对象在0代,如果他们在第0代中能够存活下来,就会被放入1代里,当在1代中也存活了下来,再被放到2代,默认当对象数量减去释放的对象数量(即当前可达的对象数量)超过700时将会对0代对象进行回收处理,当进行了10次0代回收则会触发1代回收,当进行了10次1代回收则会触发2代回收,这些配置可以通过gc.get_threshold()方法获取,并通过gc.set_threshold()自定义,举例:

>>> gc.get_threshold()
(700, 10, 10)
>>> gc.set_threshold(500, 5, 3)
>>> gc.get_threshold()
(500, 5, 3)

这里我们再来对前面循环引用的情况通过分代回收来查看效果,首先由于默认的设置里是需要对象数量减去释放数量超过700时才会触发,而这里我们使用的对象示例较少,所以需要我们调整这个触发的阈值,然后为了更加明显地看出回收的步骤,这里也重写了__new__方法,代码如下:

import gc
gc.set_threshold(2, 10, 10)
# 第一个参数代表,如果设置为0代表禁用,这里设置2,代表第0代超过2个对象时触发垃圾回收
# 后面两个是对第一代和第二代的进行回收,这里只要大于1就行了
# 等于1的话那么会不停触发对1/2代的回收,从而导致对第0代的回收失败
class D(dict):
    def __new__(self, name):
        print("对象:{}被分配!".format(name))
        return dict.__new__(self)
    def __init__(self, name):
        self.name = name
        print("对象:{}被创建!".format(self.name))
    def __del__(self):
        print("对象:{}被销毁!".format(self.name))

print("初始时的垃圾回收计数器:", gc.get_count())
a = D('a')
b = D('b')
print("创建了两个对象时的回收计数器:", gc.get_count())
a['x'] = b
b['x'] = a

a = 1
b = 2
print("修改了两个对象时的垃圾回收计数器:", gc.get_count())
c = D('c')
# 分配空间给C时,可以看到触发了第0代的回收
print("新分配空间给对象C时的垃圾回收计数器:", gc.get_count())
print("程序结束!")

结果:
初始时的垃圾回收计数器: (0, 8, 1)
对象:a被分配!
对象:a被创建!
对象:b被分配!
对象:b被创建!
创建了两个对象时的回收计数器: (2, 8, 1)
修改了两个对象时的垃圾回收计数器: (2, 8, 1)
对象:c被分配!
对象:a被销毁!
对象:b被销毁!
对象:c被创建!
新分配空间给对象C时的垃圾回收计数器: (0, 9, 1)
程序结束!
对象:c被销毁!

可以看出在我们的主要代码跑起前已经进行过8次1代和1次2代的垃圾回收了,当创建了两个对象以后,0代增加了2个,修改了这两个对象的指向后,计数器看起来还是2个a和b,但是实际上因为原来的两个字典循环引用导致未被释放,所以实际有4个,只是有2个是不可达的,因此在给对象c分配空间时计数器增加1变成3,因为超过了2,需要进行一次对0代的垃圾回收,因此a和b这两个不可达的就被销毁,然后再创建对象c,最终程序结束,将未被释放的对象a、b和c都销毁

更多参考:
https://blog.csdn.net/it_yuan/article/details/52850270
https://www.jb51.net/article/79306.htm
https://www.jianshu.com/p/0c37059ce224
https://testerhome.com/topics/16556

弱引用

当引用某个数据时,引用计数不会加一,假如有些数据被删除后,希望直接被垃圾回收,就可以利用弱引用来实现,举例:

import weakref

s = {1,2,3}
w = weakref.ref(s)
print(w())
s.remove(1)
print(w())
del s
print(w())

# {1, 2, 3}
# {2, 3}
# None
弱引用集合
  • 示例1:
import weakref

class A: pass
class B: pass

s = {1,2,3}
w = weakref.WeakSet()
a = A()
b = B()
w.add(a)
w.add(b)
print(w.data)
del a
print(w.data)

# {<weakref at 0x000002105CAE38B8; to 'A' at 0x000002105C83A2B0>, <weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
# {<weakref at 0x000002105CAE3778; to 'B' at 0x000002105C9494E0>}
  • 示例2:
import weakref

class A:
    def __del__(self):
        print("对象A被删除!")

a = A()
# b是a的引用
b = a
# c是a的弱引用
c = weakref.ref(a)
# 创建一个弱引用集合
s = weakref.WeakSet()
# 往集合当中添加一个对a的弱引用
s.add(a)
print("a的弱引用:", weakref.getweakrefs(a), "数量:", weakref.getweakrefcount(a))
del a
print("c指向的对象:", c())
del b
print("c指向的对象:", c())

# a的弱引用: [<weakref at 0x000001F8C66FB548; to 'A' at 0x000001F8C66FA1D0>, <weakref at 0x000001F8C6994778; to 'A' at 0x000001F8C66FA1D0>] 数量: 2
# c指向的对象: <__main__.A object at 0x000001F8C66FA1D0>
# 对象A被删除!
# c指向的对象: None

参考:https://www.jianshu.com/p/b94b054b8a5d

弱引用字典
import weakref

class A: pass
a = A()
b = A()
w = weakref.WeakValueDictionary()
# w = {}
# 将w改成字典,则会发现a没有被回收
w["a"] = a
w["b"] = b
print(list(w.keys()))
del a
print(list(w.keys()))

# ['a', 'b']
# ['b']

可以看到将a删除以后,弱引用字典里的a也被删除,从而起到一个类似缓冲的作用

参考:https://blog.csdn.net/MZP_man/article/details/99236003

相关文章

  • python内存管理机制

    Python内存管理机制 Python内存管理机制主要包括以下三个方面: 引用计数机制 垃圾回收机制 内存池机制 ...

  • python必知必会12

    Python 的内存机制 内存机制主要包括垃圾收集和内存管理两部分。Python 主要使用基于引用计数的垃圾回收机...

  • python学习一

    1 python 如何管理内存 对象引用计数机制,垃圾回收机制,内存池机制 一。对象引用机制 python内部引用...

  • Python内存管理机制

    Python内存管理机制主要包括以下三个方面:引用计数机制 垃圾回收机制 内存池机制

  • 面试日记--python的内存管理

    面试中被问到python的内存管理,只是说是python有自己的内存管理机制,有自己的垃圾回收机制,却不能详细作答...

  • Python的垃圾回收机制,可变类型,不可变类型

    Python的垃圾回收机制 什么是GC(垃圾回收机制),出现的原因 GC:说白了就是内存自动管理机制.它的出现的原...

  • python拾遗7 - 垃圾回收

    垃圾回收机制 相比 C++ 的手动回收内存,python 的垃圾回收机制可谓是省心省力,判断是否回收一块内存,主要...

  • 2018-10-13

    浅谈浏览器的垃圾回收机制和内存泄露 JavaScript使用垃圾回收机制来自动管理内存。 JS的回收机制分两种:1...

  • JavaScript的垃圾回收机制

    大纲 1、认识垃圾回收机制2、垃圾回收机制的原理3、垃圾回收机制的标记策略4、垃圾回收机制与内存管理 1、认识垃圾...

  • Python Memory Management

    Python的内存管理机制可以从三个方面来讲: 引用计数 垃圾回收 内存池机制 引用计数 Python采用了类似W...

网友评论

    本文标题:Python 内存管理和垃圾回收机制

    本文链接:https://www.haomeiwen.com/subject/gyiyoctx.html