简单说说python垃圾回收机制

前言

高级语言如python, java等,都采用了垃圾收集机制,而不再是像c用户自己需要管理维护内存的方式。自己管理内存极其自由,可以任意申请内存,但如同一把双刃剑,为大量内存泄露,悬空指针等bug埋下隐患。对于一个字符串、列表、类甚至数值都是对象,且定位简单易用的语言,自然不会让用户去处理如何分配回收内存的问题。python采用的是 引用计数 机制为主,标记-清除 分代回收 两种机制为辅的策略。

再说垃圾回收机制前,先和大家一起先了解下什么是 内存泄漏

内存泄漏

内存泄漏是由于开发人员的疏忽或则错误引起的程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是意味着代码在分配了某段内存后,因为设计错误,失去了对这段内存的控制,从而造成了内存的浪费。常见的有 循环引用 导致的内存泄漏问题。

接下来我们就开始分别讲解下 引用计数标记清除分代回收 三种机制。

引用计数

引用计数是一种垃圾收集机制,而且也是一种最直观、最简单的垃圾收集技术。当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1,如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。不过如果出现 循环引用 的话,引用计数机制就不再起有效的作用了。

在python中我们可以通过 sys.getrefcount() 函数,来查看一个变量的引用次数。我们要注意两点:

  1. getrefcount 本身也会引入一次计数。
  2. 在函数调用发生的时候,会产生额外的两次引用,一次来自函数栈,另一个是函数参数。
import sys

a = []

print(sys.getrefcount(a))


def func(a):
    print(sys.getrefcount(a))


func(a)
b = a
c = b
print(sys.getrefcount(a))


"""
输出结果:
2
4
4
"""

上面我们说到了循环引用,这里我们说下什么是循环引用。

循环引用

    a = { } #对象A的引用计数为 1
    b = { } #对象B的引用计数为 1
    a['b'] = b  #B的引用计数增1
    b['a'] = a  #A的引用计数增1
    del a #A的引用减 1,最后A对象的引用为 1
    del b #B的引用减 1, 最后B对象的引用为 1

在这里虽然最后执行了del ,但因为A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数虽然都为1,但显然应该被回收。

 

我们可以通过另外一种方式来体现循环引用的危害。

import os
import psutil

def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)

    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))

def test():
    get_memory('初始化')
    a = [i for i in range(1000000)]
    b = [i for i in range(1000000)]
    get_memory('after create a and b')
    a.append(b)
    b.append(a)


test()
get_memory('finish')


# 返回结果
初始化 memory used: 8.8984375 mb
after create a and b memory used: 96.44921875 mb
finish memory used: 96.44921875 mb

结果看出来虽然执行完,但是内存没有释放,如果这种错误出现在生产环境中,哪怕 a 和 b 一开始占用的空间不是很大,但经过长时间运行后,Python 所占用的内存一定会变得越来越大,最终撑爆服务器,后果不堪设想。

这种情况我们也可以通过显示 gc.collect() 释放资源 

import os
import psutil
import gc


def get_memory(name):
    pid = os.getpid()
    p_info = psutil.Process(pid)

    memory_size = p_info.memory_full_info().uss
    memory_size = memory_size/  1024 / 1024
    print('{name} memory used: {memory} mb'.format(name=name, memory=memory_size ))


def test():
    get_memory('初始化')
    list_a = [i for i in range(1000000)]
    list_b = [i for i in range(1000000)]
    get_memory('after create a and b ')
    list_a.append(list_b)
    list_b.append(list_a)


test()
gc.collect()
get_memory('finish')


# 返回结果
初始化  memory used: 8.87890625 mb
after create a and b  memory used: 95.953125 mb
finish memory used: 13.12890625 mb

最后说一句还是注意代码设计,尽量避免循环引用

标记清除

标记清除是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:

  1. 第一阶段是标记阶段,GC会把所有的 活动对象 打上标记;
  2. 第二阶段是把那些没有标记的 非活动对象 进行回收。

GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

作用对象

标记清除算法作为Python的辅助垃圾收集技术主要处理的是容器对象(list、dict、tuple等),因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象

分代回收

分代回收是一种以空间换时间的操作方式,Python将所有对象分为三代,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象.

查看内存调用关系

import objgraph

a = [1, 2, 3, 4]
b = [5, 6, 7, 8]

a.append(b)
b.append(a)

objgraph.show_backrefs([a])
# objgraph.show_refs([a])

返回结果会生成一个调用的图谱和一个执行过程的文件(windows下执行的)

 

 

学习链接

[转载]Python垃圾回收机制--完美讲解!

Python垃圾回收机制详解

Python中的垃圾回收机制

Python垃圾回收机制--完美讲解!

Python中的垃圾回收机制

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子林_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值