美文网首页
2018-08-12 python内存管理

2018-08-12 python内存管理

作者: maple_yang | 来源:发表于2018-08-12 21:39 被阅读0次

    python内存管理

    1. 引用和对象

      我们先看这样一个赋值语句 a=1

      在 python 中,整数 1 为一个对象,而 a 是一个引用 a→1

      Python是动态类型的语言(动态类型),对象与引用分离。Python通过引用来操作对象。

      在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。

    # id()是Python内置的函数,它用于返回对象的身份(identity),也就是内存地址。
    # hex 返回一个数的十六进制表示
    a = 1
    b = 1
    print(id(1))
    print(id(a))
    print(id(b))
    print(hex(id(a)))
    
    1644917568
    1644917568
    1644917568
    0x620b7340
    

      从上边代码的输出可以看出,a 和 b 其实是指向同一个对象的两个引用。

      我们现在使用 is 来检测一下,整数和短小的字符指的是什么。

    a = 1
    b = 1
    print(a is b)
    
    True
    
    a = 'good'
    b = 'good'
    print(a is b)
    
    True
    
    a = 'good night'
    b = 'good night'
    print(a is b)
    print(a[:4] is 'good')
    print(a[:4] is b[:4])
    
    False
    False
    False
    
    a = [1]
    b = [1]
    print(a is b)
    
    False
    

      可以看到,python缓存了整数和短小的字符,所以指向它们的引用都是指向的缓存好的对象。

      对于较长的字符,则是另外分配的内存,即使是相同的字符串的引用,由于它们指向的对象是另外分配的内存,它们的内存地址也是不同的。

    a = 'good night'
    b = 'good night'
    print(hex(id(a)))
    print(hex(id(b)))
    
    0x19b922e1070
    0x19b922e17b0
    
    a 和 b 指向的内存

      在 Python 中,每个对象都有指向该对象的引用计数。我们可以使用 sys.getrefcount() 来查看某个对象的引用计数,参数传递给 getrefcount()时,会创建一个临时的引用,所以 getrefcount() 会比期望结果多 1。

    import sys
    a = [1, 2]
    sys.getrefcount(a)
    
    2
    

      Python 中的容器对象,如 list、tuple、dict 等,可以包含多个对象。实际上,它们包含的只是对象的引用而已。

    a = [1, 2]
    b = [a]
    print(b)
    
    [[1, 2]]
    
    a[0] = -1
    print(b)
    
    [[-1, 2]]
    

      使用 del 可以删除一个引用,同时也会减少相应对象的引用计数。此外,将引用指向别的对象也会使引用计数减少。

    a = [1]
    b = a
    c = b
    print( sys.getrefcount(c) )
    del a # del 删除变量,减少引用计数
    print( sys.getrefcount(c) )
    b = None # 指向别的对象,减少引用计数
    print( sys.getrefcount(c) )
    
    4
    3
    2
    

      两个对象还可以相互引用,这样会形成一个引用环。

    a = [1]
    b = [a]
    a.append(b)
    print(a)
    
    [1, [[...]]]
    

      一个对象也有可能会形成引用环。

    a = [1]
    a.append(a) # 如果是 a = [a],则不会形成引用环,此时 a 为 [[1]]
    print(a)
    
    [1, [...]]
    

    2.垃圾回收

      当Python中的对象越来越多,它们将占据越来越大的内存,这个时候就需要垃圾回收(Garbage Collection)了。当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。

      不过,垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。我们可以通过gc模块的get_threshold()方法,查看该阈值:

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

      后面的两个10是与分代回收相关的阈值。700即是垃圾回收启动的阈值,可以通过 gc.set_threshold() 方法重新设置。另外,还可以手动回收gc.collect()

      另外,引用环会给 GC 带来很大的麻烦。对于上面提到的一个对象引用环的情况,即使删除了引用 a ,list 对象的引用计数也不会为0。


    单对象引用环

      Python 采用了分代回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。

      Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

      这两个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。

      另外,引用环会给 GC 带来很大的麻烦。

    a = [1]
    a.append(a) # 如果是 a = [a],则不会形成引用环,此时 a 为 [[1]]
    print(a)
    
    [1, [...]]
    

      对于上面提到的一个对象引用环的情况,即使删除了引用 a ,list 对象的引用计数也不会为0,不会被垃圾回收。


    单对象引用环

      为了回收这样的引用环,Python复制每个对象的引用计数,可以记为 gc_ref。假设,每个对象 i,该计数为 gc_ref_i。Python 会遍历所有的对象 i。对于每个对象 i 引用的对象 j,将相应的 gc_ref_j 减 1。在结束遍历后,gc_ref 不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。


    解决引用环

      下面例子中 a 指向的对象的引用计数为 2(=3-1),在去除引用 a 后变为 1,由于对象引用了本身,所以它的引用计数应该减一,即 gc_ref = 0,这表明改对象应该被垃圾回收。

    a = [1]
    a.append(a) # 如果是 a = [a],则不会形成引用环,此时 a 为 [[1]]
    print(a)
    print(sys.getrefcount(a))
    
    [1, [...]]
    3
    

    参考链接:http://www.cnblogs.com/vamei/p/3232088.html

    相关文章

      网友评论

          本文标题:2018-08-12 python内存管理

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