关于python的内存堆栈问题,本人没有剖析过cpython解释器源代码,网上也是极少的参考文档,这里提供一个讲解比较详细的文档,请移步https://www.jianshu.com/p/2f98dd213f04html
值语义:变量的值直接保存在变量的存储区里,这种方式被咱们称为值语义,例如C语言,采用这种存储方式,每个变量在内存中所占的空间就要根据变量实际的大小而定,好比 c中 int 通常为2个字节 而char为一个字节。java
引用语义:变量存储的对象值的内存地址,而不是对象值的自己实体,变量所需的存储空间大小一致,由于变量只是保存了一个引用。也被称为对象语义和指针语义。变量的每一次初始化,都开辟了一个新的空间,将新内容的地址赋值给变量。python
python的变量存储采用的是引用语义,引用和对象相分离,对象是内存中储存数据的实体,引用指向对象。引用和对象的分离,是动态类型的核心。引用能够随时指向一个新的对象,即变量存储的对象地址可被覆盖。算法
引用:在 Python 中,变量也称为对象的引用。由于变量存储的就是对象的地址。变量经过地址引用了“对象”。Python的一个容器对象(container),好比表、词典等,能够包含多个对象。实际上,容器对象中包含的并非元素对象自己,是指向各个元素对象的引用。编程
对象:Python 中,一切皆对象。每一个对象由:标识(identity)、类型(type)、value(值)组成。 标识用于惟一标识对象,一般对应于对象在计算机内存地址。使用内置函数 id(obj)可返回对象 obj 的标识。类型用于表示对象存储的“数据”的类型。类型能够限制对象的取值范围以及可执行的操做。可使用 type(obj)得到对象的所属类型。值表示对象所存储的数据的信息。使用 print(obj)能够直接打印出值。api
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 对象的。不过这是很扯淡的东西,之后编写程序最好不要这种方式加载对象。相似多此一举的操做。就当是了解一下吧:app
#在_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))()
可变类型:对象在内存地址中存储的数据可变,便可以作增、删、改操做,对象的内存地址不变,如列表,字典。ide
不可变类型:对象在内存地址中存储的数据不能变化,若修改数据,实际上是从新建立新对象,而后从新赋给变量新对象的内存地址,如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的状况:
a=23
b=a
func(a)
list1=[a,a]
2.致使引用计数-1的状况:
del a
a=24
使用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)
引用计数法有很明显的优势:
引用计数法也有明显的缺点:
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可直达,那么它将被标记,对象二、3可间接到达也会被标记,而4和5不可达,那么一、二、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.垃圾回收。
因为Python 有了自动垃圾回收功能,就形成了很多初学者误认为没必要再受内存泄漏的骚扰了。但若是仔细查看一下Python文档对 __del__() 函数的描述,就知道这种好日子里也是有阴云的。根据以上的介绍,咱们知道了python对于垃圾回收,采起的是引用计数为主,标记-清除+分代回收为辅的回收策略。对于循环引用的状况,通常的自动垃圾回收方式确定是无效了,这时候就须要显式地调用一些操做来保证垃圾的回收和内存不泄露。这就要用到python内建的垃圾回收的扩展模块gc模块了,gc模块提供一个接口给开发者设置垃圾回收的选项。上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能就是解决循环引用的问题。它会实现上面“垃圾回收机制”部分中提到的一些策略好比“标记-清除”来进行垃圾回收。经过gc来查看不能回收掉的对象的详细信息。
经常使用函数:
返回一个不可到达(unreachable)并且不能回收(uncollectable)的对象列表。从Python3.4开始,该列表大多数应该是空的。
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、一、2代对象的垃圾,并重置计数器为(0,0,0)
调优手段:
Python中的内存管理机制的层次结构提供了4层,其中最底层则是C运行的malloc
和free
接口,往上的三层才是由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