第八篇:python的内存管理与深浅拷贝

 关于python的内存堆栈问题,本人没有剖析过cpython解释器源代码,网上也是极少的参考文档,这里提供一个讲解比较详细的文档,请移步https://www.jianshu.com/p/2f98dd213f04

一.变量的存储

值语义:变量的值直接保存在变量的存储区里,这种方式被我们称为值语义,例如C语言,采用这种存储方式,每一个变量在内存中所占的空间就要根据变量实际的大小而定,比如 c中 int 一般为2个字节 而char为一个字节。

引用语义:变量存储的对象值的内存地址,而不是对象值的本身实体,变量所需的存储空间大小一致,因为变量只是保存了一个引用。也被称为对象语义和指针语义。变量的每一次初始化,都开辟了一个新的空间,将新内容的地址赋值给变量。

python的变量存储采用的是引用语义,引用和对象相分离,对象是内存中储存数据的实体,引用指向对象。引用和对象的分离,是动态类型的核心。引用可以随时指向一个新的对象,即变量存储的对象地址可被覆盖。

二.引用与对象

引用:在 Python 中,变量也称为对象的引用。因为变量存储的就是对象的地址。变量通过地址引用了“对象”。Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

对象:Python 中,一切皆对象。每个对象由:标识(identity)、类型(type)、value(值)组成。 标识用于唯一标识对象,通常对应于对象在计算机内存地址。使用内置函数 id(obj)可返回对象 obj 的标识。类型用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的操作。可以使用 type(obj)获得对象的所属类型。值表示对象所存储的数据的信息。使用 print(obj)可以直接打印出值。

Python 是动态类型语言,变量不需要显式声明类型。根据变量引用的对象,Python 解释器自动确定数据类型。

str1 = 'abcd'  # 不可变数据类型代表                                    
list1 = [str1, 2, [3, 6]]  # 可变类型的代表                            
print("变量str1储存的内存地址,即字符串对象'abcd'的内存地址:%s" % id(str1))
print("变量list1储存的内存地址,即列表对象'abcd'的内存地址:%s" % id(list1))
print("list1第一个元素储存的内存地址,即变量str1储存的内存地址:%s" % id(list1[0]))
print("list1第三个元素储存的内存地址,即列表对象'abcd'的内存地址:%s" % id(list1[2]))

在 python 中我们可以通过 id 函数来获取某个 python 对象的内存地址,或者可以通过调用对象的__repr__魔术函数来获取对象的详细信息,但是不知大家是否想过,其实这个内存地址可以直接加载 python 对象的。不过这是很扯淡的东西,以后编写程序最好不要这种方式加载对象。类似画蛇添足的操作。就当是了解一下吧:

#在_ctypes 包中,就提供 PyObj_FromPtr 这个 api 去实现我们的需求
import _ctypes #导入模块
str1 = 'abcd'  # 不可变数据类型代表
list1 = [str1, 2, [3, 6]]  # 可变类型的代表
def func():
    print(1)
print("变量str1储存的内存地址,即字符串对象'abcd'的内存地址:%s" % id(str1))
print("变量list1储存的内存地址,即列表对象'abcd'的内存地址:%s" % id(list1))
print(_ctypes.PyObj_FromPtr(id(str1)))
print(_ctypes.PyObj_FromPtr(id(list1)))
print(id(func))
_ctypes.PyObj_FromPtr(id(func))() 

三.可变数据类型与不可变数据类型

可变类型:对象在内存地址中存储的数据可变,即可以做增、删、改操作,对象的内存地址不变,如列表,字典。

不可变类型:对象在内存地址中存储的数据不能变化,若修改数据,其实是重新创建新对象,然后重新赋给变量新对象的内存地址,如number、字符串、元祖。

创建相同内容的同类对象,可变数据类型是各个变量引用不同的对象。不可变数据类型会引用同一个对象(如number、字符串),除不可变容器类外(如元祖)。判断两个变量指向的对象是否同一个对象使用运算符 is(即内存地址是否相等),判断两个变量a指向的对象的数据内容是否一致【不做深层判断】使用运算符 ==。

int1 = 5
print('变量int1储存的内存地址,即整数2的内存地址:%s'%id(int1))
int2 = 3
int3 = int2 + 2
print('变量int1储存的内存地址,即(int2+2)运算后得到的对象的内存地址:%s'%id(int3),)
print('int1和int3的值是否一样:{}。int1和int3是否指向同一个对象:{}'.format(int1 == int3, int1 is int3),end='\n'*2)

str1 = 'abcd'
print("变量str1储存的内存地址,即字符串'abcd'的内存地址:%s"%id(str1))
str2 = 'abcd'
print("变量str1储存的内存地址,即字符串'abcd'的内存地址:%s"%id(str2))
print('str1和str2的值是否一样{},str1和str2是否指向同一个对象:{}'.format(str1 == str2, str1 is str2))
str2 += str1
print("变量str1储存的内存地址,即(str1+str2)运算后的字符串对象的内存地址:%s"%id(str2))
print('修改之后的str2和str1是否指向同一个对象:{}'.format(str1 is str2),end='\n'*2)

list1 = [int1,3,[str1,'a']]
print("变量list1储存的内存地址,即列表[int1,3,[str1,'a']]的内存地址:%s"%id(list1))
list2 = [int1,3,[str1,'a']]
print("变量list2储存的内存地址,即列表[int1,3,[str1,'a']]的内存地址:%s"%id(list2))
print("变量list1与变量list2的值是否一样:{},list1和list2是否指向同一个对象:{}".format(list1 == list2, list1 is list2))
print("list1第二个元素和list2第二个元素的值是否一样:{},list1第二个元素和list2第二个元素是否指向同一个对象:{}".format(list1[1] == list2[1], list1[1] is list2[1]))
print("list1第三个元素和list2第三个元素的值是否一样:{},list1第三个元素和list2第三个元素是否指向同一个对象:{}".format(list1[2] == list2[2], list1[2] is list2[2]))
list2 += list1
print("修改之后的list2储存的内存地址,即列表%s的内存地址:%s"%(list2,id(list2)),end='\n'*2)

# tuple1 = (2,'a',3)
tuple1 = (2,'a',[2,3])
print("变量tuple1储存的内存地址,即元祖%s的内存地址:%s"%(tuple1,id(tuple1)))
# tuple2 = (2,'a',3)
tuple2 = (2,'a',[2,3])
print("变量tuple2储存的内存地址,即元祖%s的内存地址:%s"%(tuple2,id(tuple2)))
print("变量tuple1与变量tuple2的值是否一样:{},tuple1和tuple2是否指向同一个对象:{}".format(tuple1 == tuple2, tuple1 is tuple2))
print("tuple1第三个元素和tuple2第三个元素的值是否一样:{},tuple1第三个元素和tuple2第三个元素是否指向同一个对象:{}".format(tuple1[2] == tuple2[2], tuple1[2] is tuple2[2]))
tuple2 += tuple1
print("修改后的tuple2储存的内存地址,即元祖%s的内存地址:%s"%(tuple2,id(tuple2)))

 看完代码还是不明白可变与不可变数据类型的之间的储存区别,那咱再来看看下面的图解吧

从上图可以看出列表的引用关系比较复杂,还有字典,集合,元祖等容器对象的引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系。objgraph是Python的一个第三方包。安装之前需要安装xdot,obigraph官网

在window系统安装python第三方包的方法:pip install 包名

import objgraph
str1 = 'ab'
list1 = ['ab',[2,'ab']]
list2 = ['ab',[2,'ab']]
objgraph.show_refs([str1,list1,list2], filename='./ref_top.png')
list2 += list1
objgraph.show_refs([str1,list1,list2], filename='./ref_topo.png')

四.浅拷贝与深拷贝

我们知道可变类型对象的存存储的数据是可变的,而其的引用关系又如此复杂,若一个可变对象被多数对象引用,那么只要任意一个引用对象对它做修改,引用它的其他引用对象的数值也会变化。所以接下来我们学习一个重要的知识点:python的赋值拷贝、浅拷贝与深拷贝。

赋值拷贝:直接赋值,其实就是对象的引用。

#不可变类型的赋值拷贝
str1 = 'ab'
str2 = str3 = str1
print(str2 is str1)
str1 = 'abcd'
print(str3)
print(str1 is str2)
print(str1)

#可变类型的赋值拷贝
list1 = [2,[5,'a'],'b']
list2 = list1
print(list1 is list2)
list2.append('ab')
print(list1)
浅拷贝:不管多么复杂的数据结构,浅拷贝都只会copy一层引用。不会拷贝对象的内部的可变子对象(多层)。浅拷贝是指拷贝的只是原子对象元素的引用,
换句话说,浅拷贝产生的对象本身是新的,但是它的内容不是新的,只是对原子对象的一个引用。当我们使用下面的操作的时候,会产生浅拷贝的效果:
list1 = [2,[5,'a'],'b']
list2 = list1
print(list1 is list2)
list2.append('ab')
print(list1,end='\n'*2)

#切片
list3 = list1[0:2]
print(list3[1] is list1[1])
list3[1].append('123')
print(list1)
print(list2,end='\n'*2)

#list.copy()
list4 = list2.copy()
print(list4[1] is list2[1],end='\n'*2)

#copy.copy()
import copy
list5 = copy.copy(list2)
print(list5[1] is list2[1])

深拷贝:深拷贝就是在内存中重新开辟一块空间,不管数据结构多么复杂,只要遇到可能发生改变的数据类型,就重新开辟一块内存空间把内容复制下来,直到最后一层。

import copy
#深拷贝
list1 = [2,[5,'a'],'b']
list2 = copy.deepcopy(list1)
print(list1 is list2)
print(list1[1] is list2[1])
list2[1].append('ab')
print(list1)
print(list2,end='\n'*2)

#浅拷贝
list3 = copy.copy(list1)
print(list3[1] is list1[1])

五.引用传递与值传递

可变对象为引用传递,不可变对象为值传递。(函数传值)

值传递: 简单来说 对于函数输入的参数对象,函数执行中首先生成对象的一个副本,并在执行过程中对副本进行操作。执行结束后对象不发生改变。即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,然后把内存地址赋值给形参变量引用,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(被调函数新开辟内存空间存放的是实参的副本值)

def test(b):
    b += 2
    print(id(b))
    print(b,end='\n'*2)
    return

a = 2
print(id(a),end='\n'*2)
test(a)
print(a)
print(id(a))

引用传递:当传递列表或者字典时,如果改变引用的值,就修改了原始的对象。(被调函数新开辟内存空间存放的是实参的地址,实际上可变类型的赋值拷贝,形参变量=实参变量)

def test(str1):
    print('形参变量str1存储的内存地址,即形参内存地址:%s'%id(str1),end='\n'*2)
    str1[1] = "changed " #此处修改就是直接修改str1的值
    return

string = ['hello world',2,3]
print(id(string))
print(string,end='\n'*2)
test(string)  # str1 = string
print(string)
print(id(string))

六.垃圾回收机制

在许多语言中都有垃圾回收机制,比如Java和Ruby,python以 引用计数垃圾回收算法 为主要回收机制,以 标记-清除 和 分代回收 为辅助回收机制,三种回收机制共同协作,实现了PYTHON非常高处理效率的垃圾回收机制。

引用计数

Python里面每一个东西都是对象,他们的核心是一个结构体Py_Object,所有Python对象的头部包含了这样一个结构PyObject,任何一个python对象都分为两部分: PyObject_HEAD + 对象本身数据。每个对象都有存有指向该对象的引用总数,即引用计数。我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。当一个对象被创建出来,他的引用计数就会+1,当对象被引用的时候,计数继续增加,当引用它的对象被删除(del)的时候,它的引用计数就会减少。直到变为0,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空,Python虚拟机就会回收这个对象的内存。我们也可以手动启动垃圾回收,即使用gc.collect()。

1.导致引用计数+1的情况:

  1. 对象被创建,例如a=23
  2. 对象被引用,例如b=a
  3. 对象被作为参数,传入到一个函数中,例如func(a)
  4. 对象作为一个元素,存储在容器中,例如list1=[a,a]

2.导致引用计数-1的情况:

  1. 对象的别名被显式销毁,例如del a
  2. 对象的别名被赋予新的对象,例如a=24
  3. 一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
  4. 对象所在的容器被销毁,或从容器中删除对象

使用sys包中的getrefcount()查看引用计数:

from sys import getrefcount

int1 = 35
print('int1对象的引用总数:{}'.format(getrefcount(int1) - 1)) #对于不可类型内存中是为一的一份,在我们引用之前已被引用,
# 第一个输出的结果可能与我们调用的结果不一致,重要是看其后面被引用的变化量。
str1 = 'abc'
var = getrefcount(str1) - 1 #通过减去差值,可使输出结果与我们创建对象时开始计数的引用总数一致。
print('str11对象的引用总数:{}'.format(getrefcount(str1) - var),end='\n'*2)

list1 = [5,int1,str1]
print('int1对象的引用总数:{}'.format(getrefcount(int1) - 1))
print('str1对象的引用总数:{}'.format(getrefcount(str1) - var))
print('list1对象的引用总数:{}'.format(getrefcount(list1) - 1),end='\n'*2)

list2 = list1
print('int1对象的引用总数:{}'.format(getrefcount(int1) - 1))
print('str1对象的引用总数:{}'.format(getrefcount(str1) - var))
print('list1对象的引用总数:{}'.format(getrefcount(list1) - 1))
print('list2对象的引用总数:{}'.format(getrefcount(list2) - 1),end='\n'*2)

list3 = list1.copy()
print('int1对象的引用总数:{}'.format(getrefcount(int1) - 1))
print('str1对象的引用总数:{}'.format(getrefcount(str1) - var))
print('list1对象的引用总数:{}'.format(getrefcount(list1) - 1))
print('list2对象的引用总数:{}'.format(getrefcount(list2) - 1))
print('list3对象的引用总数:{}'.format(getrefcount(list3) - 1),end='\n'*2)

 引用计数法有很明显的优点:

  1. 高效
  2. 运行期没有停顿 可以类比一下Ruby的垃圾回收机制,也就是 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
  3. 对象有确定的生命周期
  4. 简单,易于实现

引用计数法也有明显的缺点: 

  1. 维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比,而不像mark and sweep等基本与回收的内存数量有关。
  2. 无法解决循环引用的问题。
from sys import getrefcount
list1 = [1]
print("创建列表对象[1],并被变量list1引用,列表对象[1]的引用计数为:%s"%(getrefcount(list1)-1))
list2 = [2]
print("创建列表对象[2],并被变量list2引用,列表对象[1]的引用计数为:%s"%(getrefcount(list2)-1))
list1.append(list2)
print("通过变量list1的引用对list1引用的对象进行添加变量list2的引用的操作,此时list2引用的对象的引用计数+1为:%s"%(getrefcount(list2)-1))
list2.append(list1)
print("通过变量list2的引用对list2引用的对象进行添加变量list1的引用的操作,此时list1引用的对象的引用计数+1为:%s"%(getrefcount(list1)-1))
print(list1)
print(list2)
import objgraph
objgraph.show_refs([list1,list2], filename='./ref_top.png')
del list1 #del只是删除变量的引用,变量引用的对象的引用计数-1,并不是删除对象。
del list2

当循环引用不再被变量引用时,任然保持引用计数大于0,引用计数回收机制就无法回收,从而导致内心泄露,一旦出现循环引用,我们就得采取新的办法了。上面说到python里回收机制是以引用计数为主,标记-清除和分代收集两种机制为辅。

标记-清除

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

 

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达(unreachable)的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。 mark-sweepg 在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

针对循环引用这个问题,比如有两个对象互相引用了对方,当外界没有对他们有任何引用,也就是说他们各自的引用计数都只有1的时候,如果可以识别出这个循环引用,把它们属于循环的计数减掉的话,就可以看到他们的真实引用计数了。基于这样一种考虑,有一种方法,比如从对象A出发,沿着引用寻找到对象B,把对象B的引用计数减去1;然后沿着B对A的引用回到A,把A的引用计数减1,这样就可以把这层循环引用关系给去掉了。

                                 

不过这么做还有一个考虑不周的地方。假如A对B的引用是单向的, 在到达B之前我不知道B是否也引用了A,这样子先给B减1的话就会使得B称为不可达的对象了。为了解决这个问题,python中常常把内存块一分为二,将一部分用于保存真的引用计数,另一部分拿来做为一个引用计数的副本,在这个副本上做一些实验。比如在副本中维护两张链表,一张里面放不可被回收的对象合集,另一张里面放被标记为可以被回收(计数经过上面所说的操作减为0)的对象,然后再到可回收的对象链表中找一些被不可回收的对象链表中一些对象直接或间接单向引用的对象,把这些移动到不可回收的对象链表里面。这样就可以让不应该被回收的对象不会被回收,应该被回收的对象都被回收了。

分代回收

分代回收是建立在标记清除基础上的一种辅助回收容器对象的GC机制。我们知道标记-清除有一个明显的缺点就是清除非活动的对象前它必须顺序扫描整个堆内存为了提高垃圾回收机制的执行效率,于是添加了分代回收机制。分代技术是一种典型的以空间换时间的技术,这也正是java里的关键技术。这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。这样的思想,可以减少标记-清除机制所带来的额外操作。分代就是将回收对象分成数个代,每个代就是一个链表(集合),代进行标记-清除的时间与代内对象存活时间成正比例关系。

分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,即一个代就是一个链表, 所有属于同一”代”的内存块都链接在同一个链表中 。它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。每个代的threshold值表示该代最多容纳对象的个数。默认情况下,当0代超过700,或1,2代超过10,垃圾回收机制将触发。

分代回收策略着眼于提升垃圾回收的效率。研究表明,任何语言,任何环境的编程中,对于变量在内存中的创建/销,总有频繁和不那么频繁的。比如任何程序中总有生命周期是全局的、部分的变量。而在垃圾回收的过程中,其实在进行垃圾回收之前还要进行一步垃圾检测,即检查某个对象是不是垃圾,该不该被回收。当对象很多,垃圾检测将耗费大量的时间而真的垃圾回收花不了多久。对于这种多对象程序,我们可以把一些进行垃圾回收频率相近的对象称为“同一代”的对象。垃圾检测的时候可以对频率较高的“代”多检测几次,反之,进行垃圾回收频率较低的“代”可以少检测几次。这样就可以提高垃圾回收的效率了。至于如何判断一个对象属于什么代,python中采取的方法是通过其生存时间来判断。如果在好几次垃圾检测中,该变量都是reachable的话,那就说明这个变量越不是垃圾,就要把这个变量往高的代移动,要减少对其进行垃圾检测的频率。

完整的收集流程:1.链表建立,2.确定根节点,3.垃圾标记,4.垃圾回收。

gc模块

 由于Python 有了自动垃圾回收功能,就造成了不少初学者误认为不必再受内存泄漏的骚扰了。但如果仔细查看一下Python文档对 __del__() 函数的描述,就知道这种好日子里也是有阴云的。根据以上的介绍,我们知道了python对于垃圾回收,采取的是引用计数为主,标记-清除+分代回收为辅的回收策略。对于循环引用的情况,一般的自动垃圾回收方式肯定是无效了,这时候就需要显式地调用一些操作来保证垃圾的回收和内存不泄露。这就要用到python内建的垃圾回收的扩展模块gc模块了,gc模块提供一个接口给开发者设置垃圾回收的选项。上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能就是解决循环引用的问题。它会实现上面“垃圾回收机制”部分中提到的一些策略比如“标记-清除”来进行垃圾回收。通过gc来查看不能回收掉的对象的详细信息。

常用函数:

    1. gc.set_debug(flags)
      设置gc的debug日志,一般设置为gc.DEBUG_LEAK,调试信息会通过sys.stderr输出。
    2. gc.collect([generation])
      显式进行垃圾回收,可以输入参数,0代表只检查第一代的对象,1代表检查一,二代的对象,2代表检查一,二,三代的对象,如果不传参数,执行一个full collection,也就是等于传2。
      返回不可达(unreachable objects)对象的数目
    3. gc.set_threshold(threshold0[, threshold1[, threshold2])
      设置自动执行垃圾回收的频率。
    4. gc.get_count()
      获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表
    5. gc.disable()      关闭自动的垃圾回收,改为手动
    6. gc.garbage()    
      返回一个不可到达(unreachable)而且不能回收(uncollectable)的对象列表。从Python3.4开始,该列表大多数应该是空的。
    7. gc.get_referrers(*objs)  返回直接引用任何objs的对象的列表
    8. gc.get_debug() 返回当前设置的调试设置;
    9. gc.get_referrers(*objs) 返回直接引用任何objs的对象的列表
    10. gc.get_referents(*objs) 返回任何参数直接引用的对象列表。
    11. gc.get_stats() 返回一个包含三代回收信息的列表,每代的回收信息
    12. gc.DEBUG_STATS 打印回收期间的统计信息
    13. gc.DEBUG_COLLECTABLE 打印找到可收集对象的信息
    14. gc.DEBUG_UNCOLLECTABLE 打印找到的不可收集对象的信息(不能被收集器释放但不能被收集器释放的对象)。这些对象将被添加到垃圾清单
    15. gc.DEBUG_SAVEALL 设置时,找到的所有不可达对象将被追加到垃圾中,而不是被释放。这对调试泄漏程序很有用。
    16. gc.DEBUG_LEAK 收集器需要打印有关泄漏程序的相关信息,等同于(DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL
import sys
import gc

a = [1]
b = [2]
a.append(b)
b.append(a)
####此时a和b之间存在循环引用####
print(sys.getrefcount(a)-1)  #结果应该是2
print(sys.getrefcount(b)-1)   #结果应该是2
print(gc.isenabled(),end='\n'*2) #python默认是自动回收 gc.disable() #关闭自动回收,改为手动, print(gc.isenabled()) gc.enable() print(gc.isenabled(),end='\n'*2) del a del b print(gc.garbage) print(gc.collect()) ####gc.collect()专门用来处理这些循环引用,返回处理这些循环引用一共释放掉的对象个数。这里返回是2#### print(gc.garbage)
import objgraph
import gc

class Foo(object):

    def __init__(self):
        self.bar = None
        print("foo init")

    def __del__(self):
        print("foo del")


class Bar(object):

    def __init__(self):
        self.foo = None
        print("bar init")

    def __del__(self):
        print("bar del")

# gc.set_debug(gc.DEBUG_SAVEALL)
foo = Foo()
bar = Bar()
foo.bar = bar
bar.foo = foo
del foo
del bar
print(objgraph.count('Foo'))
print(objgraph.count('Bar'))
print(gc.collect())
print(objgraph.count('Foo'))
print(objgraph.count('Bar'))

这里需要明确一下,之前对于“垃圾”二字的定义并不是很明确,在这里的这个语境下,垃圾是指在经过collect的垃圾回收之后仍然保持unreachable状态,即无法被回收,且无法被用户调用的对象应该叫做垃圾。gc模块中有garbage这个属性,其为一个列表,每一项都是当前解释器中存在的垃圾对象。一般情况下,这个属性始终保持为空集。

collect返回4的原因是因为,在A和B类对象中还默认有一个__dict__属性,里面有所有属性的信息。比如对于a,有a.__dict__ = {'_b':<__main__.B instance at xxxxxxxx>}。a的__dict__和b的__dict__也是循环引用的。

有时候garbage里也会出现那两个__dict__,这主要是因为在前面可能设置了gc模块的debug模式,比如gc.set_debug(gc.DEBUG_LEAK),会把所有已经回收掉的unreachable的对象也都加入到garbage里面。set_debug还有很多参数诸如gc.DEBUG_STAT|DEBUG_COLLECTABLE|DEBUG_UNCOLLECTABLE|DEBUG_SAVEALL等等,设置了相关参数后gc模块会自动检测垃圾回收状况并给出实时地信息反映。

有三种情况会触发垃圾回收:
1.调用gc.collect(),
2.当gc模块的计数器达到阀值的时候。
3.程序退出的时候

垃圾回收=垃圾检查+垃圾回收

 gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取。

例如(488,3,0),其中488是指距离上一次0代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加3是指距离上一次1代垃圾检查,0代垃圾检查的次数,同理,0是指距离上一次2代垃圾检查,1代垃圾检查的次数。

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组,这个方法返回的是(700,10,10),这也是gc的默认值。这个值的意思是说,在第0代对象数量达到700个之前,不把未被回收的对象放入第一代;而在第一代对象数量达到10个之前也不把未被回收的对象移到第二代。使用gc.set_threshold(threashold0,threshold1,threshold2)可以手动设置这组阈值。
例如,假设阀值是(700,10,10)

    • 当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查0代对象的垃圾,并重置计数器为(0,4,0)
    • 当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查0、1代对象的垃圾,并重置计数器为(0,0,1)
    • 当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查0、1、2代对象的垃圾,并重置计数器为(0,0,0)

调优手段:

  1. 项目中避免循环引用
  2. 引入gc模块,启动gc模块的自动清理循环引用的对象机制

python的内存管理机制

Python中的内存管理机制的层次结构提供了4层,其中最底层则是C运行的mallocfree接口,往上的三层才是由Python实现并且维护的。

Python在运行期间会大量地执行malloc和free的操作,频繁地在用户态和核心态之间进行切换,这将严重影响Python的执行效率。为了加速Python的执行效 率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。但是它将释放的内存放到内存池而不是返回给操作系统。

参考连接:https://docs.python.org/zh-cn/3.6/c-api/memory.html

     https://blog.csdn.net/zhzhl202/article/details/7547445

     https://blog.csdn.net/qq_33339479/article/details/81609159

     https://www.cnblogs.com/qq_841161825/articles/10174739.html



 



猜你喜欢

转载自www.cnblogs.com/us-wjz/p/10920926.html
今日推荐