什么是“内存管理机制”?( 二 )


  • 对象所在的容器被销毁或从容器中删除对象(例如:del c )
  • 引用超出作用域或被重新赋值(例如:a=[3,4])
  • 引用计数能够解决大多数垃圾回收的问题,但是遇到两个对象相互引用的情况,del语句可以减少引用次数,但是引用计数不会归0,对象也就不会被销毁,从而造成了内存泄漏问题 。针对该情况,Python引入了标记-清除机制 。
     
    2、标记-清除标记-清除用来解决引用计数机制产生的循环引用,进而导致内存泄漏的问题。循环引用只有在容器对象才会产生,比如字典,元组,列表等 。
    顾名思义,该机制在进行垃圾回收时分成了两步,分别是:
    • 标记阶段,遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达;
    • 清除阶段,再次遍历对象,如果发现某个对象没有标记为可达(即为Unreachable),则就将其回收 。
     
    1. >>> a=[1,2]
    2. >>> b=[3,4]
    3. >>> sys.getrefcount(a)
    4. 2
    5. >>> sys.getrefcount(b)
    6. 2
    7. >>> a.append(b)
    8. >>> sys.getrefcount(b)
    9. 3
    10. >>> b.append(a)
    11. >>> sys.getrefcount(a)
    12. 3
    13. >>> dela
      >>> del b
       
    • a引用b,b引用a,此时两个对象各自被引用了2次(去除getrefcout()的临时引用)

    什么是“内存管理机制”?

    文章插图
    • 执行del之后,对象a,b的引用次数都-1,此时各自的引用计数器都为1,陷入循环引用

    什么是“内存管理机制”?

    文章插图
    • 标记:找到其中的一端a,因为它有一个对b的引用,则将b的引用计数-1

    什么是“内存管理机制”?

    文章插图
    • 标记:再沿着引用到b,b有一个a的引用,将a的引用计数-1,此时对象a和b的引用次数全部为0,被标记为不可达(Unreachable)

    什么是“内存管理机制”?

    文章插图
    • 清除: 被标记为不可达的对象就是真正需要被释放的对象
    上面描述的垃圾回收的阶段,会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行 。为了减少应用程序暂停的时间,Python 通过“分代回收”(Generational Collection)以空间换时间的方法提高垃圾回收效率 。
     
    3、分代回收
    分代回收是基于这样的一个统计事实,对于程序,存在一定比例的内存块的生存周期比较短;而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束 。生存期较短对象的比例通常在 80%~90%之间 。因此,简单地认为:对象存在时间越长,越可能不是垃圾,应该越少去收集 。这样在执行标记-清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度,是一种以空间换时间的方法策略 。
    Python将所有的对象分为年轻代(第0代)、中年代(第1代)、老年代(第2代)三代 。所有的新建对象默认是 第0代对象 。当在第0代的gc扫描中存活下来的对象将被移至第1代,在第1代的gc扫描中存活下来的对象将被移至第2代 。
    gc扫描次数(第0代>第1代>第2代)
    当某一代中被分配的对象与被释放的对象之差达到某一阈值时,就会触发当前一代的gc扫描 。当某一代被扫描时,比它年轻的一代也会被扫描,因此,第2代的gc扫描发生时,第0,1代的gc扫描也会发生,即为全代扫描 。
     
    1. >>> importgc
      >>> gc.get_threshold ## 分代回收机制的参数阈值设置
      (700,10,10)
    • 700=新分配的对象数量-释放的对象数量,第0代gc扫描被触发
    • 第一个10:第0代gc扫描发生10次,则第1代的gc扫描被触发
    • 第二个10:第1代的gc扫描发生10次,则第2代的gc扫描被触发
     
    4、思考在标记-清除中,如果对象c也引用a,执行del操作后,会发生什么?对象a,b,c的引用关系如下图所示: 


    推荐阅读