当前位置: 首页 > 科技观察

说说Python中的“垃圾”回收

时间:2023-03-22 10:28:57 科技观察

前言对于python来说,万物皆对象,所有的变量赋值都遵循对象引用机制。程序运行时,需要在内存中开辟一块空间,用于存放运行过程中产生的临时变量;计算完成后,将结果输出到永久存储器中。如果数据量太大,内存空间管理不好,很容易出现OOM(outofmemory),俗称爆内存,程序可能会被操作系统终止。对于服务器来说,内存管理就更重要了,否则很容易造成内存泄露——这里的泄露并??不是说你的内存存在信息安全问题,被恶意程序利用,而是程序本身没有设计好。导致程序无法释放不再使用的内存。-内存泄漏并不是说你的内存在物理上消失了,而是指代码分配了某块内存后,由于设计错误而失去了对这块内存的控制,造成了内存的浪费。也就是说,这块内存是不受gc控制的。计数引用因为python中的一切都是对象,所以你看到的所有变量本质上都是一个指向对象的指针。当一个对象不再被调用的时候,也就是这个对象的引用计数(指针的个数)为0的时候,就意味着这个对象永远不可达了,自然就变成了垃圾,需要回收。可以简单理解为没有变量指向它。importosimportpsutil#显示当前python程序占用的内存大小defshow_memory_info(hint):pid=os.getpid()p=psutil.Process(pid)info=p.memory_full_info()memory=info.uss/1024./1024print({}memoryused:{}MB.format(hint,memory))您可以看到调用了函数func()。创建列表a后,内存使用量迅速增加到433MB:函数调用结束后,内存恢复正常。这是因为在函数内部声明的列表a是一个局部变量。函数返回后,局部变量的引用将被取消;此时列表a引用的对象的引用号为0,Python会进行垃圾回收,所以之前占用的大量内存又回来了。deffunc():show_memory_info(initial)globalaa=[iforiinrange(10000000)]show_memory_info(aftercreated)func()show_memory_info(finished)##########output##########initialmemoryused:48.88671875MBAfteracreatedmemoryuused:433.94921875MBfinishedmemoryuused:433.94921875MB在这段新代码中,globala意味着将a声明为全局变量。那么,即使函数返回后,对列表的引用仍然存在,所以该对象不会被垃圾回收,仍然占用大量内存。同样,如果我们返回生成的列表,在主程序中接收,那么引用依然存在,不会触发垃圾回收,仍然占用大量内存:deffunc():show_memory_info(initial)a=[iforiinderange(10000000)]show_memory_info(aftercreated)returnaa=func()show_memory_info(finished)##########output##########initialmemoryused:47.96484375MB查看一个变量被引用了多少次?通过sys.getrefcount。importsysa=[]#两个引用,一个来自a,一个来自getrefcountprint(sys.getrefcount(a))deffunc(a):#四个引用,a,python的函数调用栈,函数参数,getrefcountprint(sys.getrefcount(a)))func(a)#两个引用,一个来自a,一个来自getrefcount,函数func调用不存在了print(sys.getrefcount(a))##########output##########242如果涉及函数调用,会额外增加两个1.函数栈2.函数调用。从这里可以看出python不再像C一样需要释放内存,但是python也为我们提供了手动释放内存的方法gc.collect()。importgcshow_memory_info(initial)a=[iforiinrange(10000000)]show_memory_info(aftercreated)delagc.collect()show_memory_info(finish)print(a)##########输出##########初始使用内存:48.1015625MB创建后使用内存:434.3828125MB完成使用内存:48.33203125MB------------------------------------------------------------------------NameErrorTraceback(最近calllast)in1112show_memory_info(finish)--->13print(a)NameError:nameaisnotdefined至此看来python的垃圾回收机制很简单,只要对象引用数为0,必然触发gc,那么引用数是否为0是否触发gc的充要条件是什么?如果循环回收中有两个对象,它们相互引用,不再被其他对象引用,是否应该进行垃圾回收?deffunc():show_memory_info(initial)a=[iforiinrange(10000000)]b=[iforiinrange(10000000)]show_memory_info(aftera,bcreated)a.append(b)b.append(a)func()show_memory_info(finished))##########output##########initialmemoryuused:47.984375MBaftera,bcreatedmemoryuused:822.73828125MBfinishedmemoryuused:821.73046875MB从结果可以明显看出它们没有被回收,但是从从程序的角度来看,当这个函数结束时,a作为局部变量,b已经从程序意思不存在,但是因为它们相互引用,所以它们的引用数不为0。这时候如何避免1.整顿代码逻辑,避免这种循环引用2.手动回收。importgcdeffunc():show_memory_info(initial)a=[iforiinrange(10000000)]b=[iforiinrange(10000000)]show_memory_info(aftera,bcreated)a.append(b)b.append(a)func()gc.collect()show_memory_info(finished)##########output##########initialmemoryused:49.51171875MBaftera,bcreatedmemoryused:824.1328125MBfinishedmemoryused:49.98046875MBpython有循环引用的自动垃圾回收算法1.Mark-sweep算法2.分代收集。标记清除标记清除的步骤总结如下:1.GC会标记所有的“活动对象”2.回收那些没有标记的对象“非活动对象”那么python是如何判断什么是非活动对象的呢?通过使用图论来理解不可达性的概念。对于有向图,如果我们从一个节点开始遍历,并标记它经过的所有节点;那么,遍历结束后,所有没有被标记的节点称为不可达节点。显然,这些节点的存在是没有意义的,自然需要对它们进行垃圾回收。但是每次都遍历整个图对于Python来说是一种巨大的性能浪费。因此,在Python的垃圾回收实现中,mark-sweep使用双向链表来维护一个数据结构,只考虑容器类对象(只有容器类对象,list、dict、tuple、instance,才能产生循环引用)。图中黑色小圆圈作为全局变量,即作为根对象。从小黑圈开始,如果对象1可以直接到达就标记,对象2、3如果可以间接到达就标记,4、5不可达,则1、2、3active对象,4和5是inactive对象,会被GC回收。分代恢复分代恢复是一种以空间换取时间的操作方式。Python根据对象的存活时间将内存划分为不同的集合。每个集合称为一代。Python将内存分为3个“代”,分别是年轻代(0thgeneration)、中间代(1stgeneration)、老年代(2ndgeneration)。它们对应3个链表,它们的垃圾回收频率随着对象存活时间的增加而降低。新创建的对象会分配到新生代。当年轻代链表总数达到上限时(当垃圾收集器中的新增对象减去已删除对象达到相应阈值时),Python垃圾收集机制将被触发。被回收的对象被回收,那些不会被回收的对象会被移到中年,以此类推,老年代的对象是寿命最长的对象,甚至在整个系统的生命周期中也是如此。同时,分代回收基于标记清除技术。事实上,分代收集是基于新对象更有可能被垃圾收集的想法,存活时间更长的对象继续存活的概率更高。因此,通过这种方式,可以节省大量的计算量,从而提高Python的性能。所以对于刚才的问题,引用计数只是触发gc的充分非必要条件,循环引用也会被触发。调试可以使用objgraph来调试程序,因为没有仔细看它的官方文档,所以只能把文档放在这里,供大家参考~其中两个函数非常好用1.show_refs()2.show_backrefs()。