美文网首页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 内存管理和垃圾回收机制

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