Python作为一种解释型语言,以代码简洁易懂着称。我们可以直接给名字赋值而不声明类型。名称类型的确定、内存空间的分配和释放都是由Python解释器在运行时完成的。Python的自动内存管理功能大大减轻了程序员的负担。对于Python这样的高级语言,开发者无需关心其内部的垃圾回收机制。相辅相成,通过学习Python内部的垃圾回收机制,理解其原理,开发者可以写出更好的代码,成为更Pythonista。目录Python内存管理机制Python垃圾回收机制2.1引用计数(referencecounting)2.2标记与清除(MarkandSweep)2.3分代垃圾收集器(Generationalgarbagecollector)Python中的gc模块Python内存管理机制在Python中,内存管理涉及到包含所有Python对象和数据结构的私有堆。这个私有堆的管理由内部Python内存管理器确保。Python内存管理器有不同的组件来处理各种动态存储管理方面,例如共享、拆分、预分配或缓存。在最低级别,原始内存分配器通过与操作系统的内存管理器交互,确保私有堆中有足够的空间来存储所有与Python相关的数据。在原有内存分配器的基础上,若干个对象特定的分配器运行在同一个堆上,根据每个对象类型的特点,实现不同的内存管理策略。例如,整数对象在堆上的管理方式不同于字符串、元组或字典,因为整数需要不同的存储要求以及速度和空间的权衡。因此,Python内存管理器将一些工作分配给对象特定的分配器,但确保后者在私有堆的范围内运行。重要的是要了解Python堆内存的管理是由解释器执行的,用户无法控制它,即使他们经常操作指向堆内内存块的对象指针。为了避免对小对象(<=512bytes)进行过多的GC,Python会造成性能消耗。Python使用子分配(内存池)来管理小对象的内存块。对于大对象,使用标准C分配器分配内存。Python对小对象的allocator从大到小分为三个层次:arena、pool、block。BlockBlock是最小的层级,每个block只能容纳一个固定大小的PythonObject。大小范围为8-512bytes,以8bytes为步长,分为64个不同的block。RequestinbytesSizeofallocatedblocksizeclassidx1-8809-1616117-2424225-3232333-4040441-48485…………505-51251263PoolPool具有相同大小的块的集合称为Pool。通常情况下,Pool的大小为4kb,与虚拟内存页的大小一致。将Pool中的block限制为固定大小有以下好处:当当前Pool中的block中有对象销毁时,Pool内存管理可以将新生成的对象放入block中。/*小块池。*/structpool_header{union{block*_padding;单位计数;}参考;/*已分配块的数量*/block*freeblock;/*池的空闲列表头*/structpool_header*nextpool;/*这个大小类的下一个池*/structpool_header*prevpool;/*前一个池""*/uintarenaindex;/*索引到baseadr的区域*/uintszidx;/*块大小类索引*/uintnextoffset;/*字节到原始块*/uintmaxnextoffset;/*最大的有效nextoffset*/};大小相同的pool通过双向链表连接起来,sidx用于标识Block的类型。arenaindex标识当前Pool属于哪个Arena。ref.conut标识当前Pool使用了多少块。freeblock:一个指针,标识当前Pools中可用的块。Freeblock实际上是作为一个单链表来实现的。当一个块为空时,该块被插入到freeblock链表的头部。每个Pool有三种状态:used:partiallyused,即Pool未满不空full:full,即Pool中的所有Block都已分配empty:empty,即Pool中的所有Block都有未分配usedpool为了更好高效地管理Pool,Python额外使用了array和usedpool进行管理。即如下图所示,usedpool依次存储每个特征大小的Pools的头指针,相同大小的Pools按照双向链表连接。在分配新的内存空间时,创建一个特定大小的Pool,只需要使用usedpools找到head指针并遍历即可。当没有内存空间时,只需要在Pool的双向链表头部插入一个新的Pool即可。ArenaPools和Blocks都不会直接分配内存。池和块将使用从竞技场分配的内存空间。Arena:是分配在堆上的一块256kb的块内存,提供64个Pools。structarena_object{uintptr_t地址;块*pool_address;单位nfreepools;uintntotalpools;structpool_header*freepools;结构arena_object*nextarena;structarena_object*prevarena;};所有的arenas也使用双链表进入链接(prevarena,nextarena字符串)。nfreepools和ntotalpools存储有关当前可用池的信息。freepools指针指向当前可用的池。arena的结构很简单,它的职责就是按需分配内存给pool。当一个arena为空时,将arena的内存返回给操作系统。Python的垃圾回收机制Python采用引用计数机制为主,标记清除和分代回收为辅的策略。引用计数(referencecounting)Python语言默认采用的垃圾回收机制是“引用计数方法ReferenceCounting”。该算法最早由GeorgeE.Collins于1960年提出。50年后的今天,该算法仍在被许多编程语言使用。使用。引用计数法的原理是:每个对象都维护一个ob_ref字段,用来记录该对象当前被引用的次数。每当有新的引用指向该对象时,它的引用计数ob_ref就加1。每当该对象的引用计数器ob_ref失效时就减1。一旦该对象的引用计数为0,该对象将立即被回收,该对象占用的内存空间将被释放。它的缺点是需要额外的空间来维护引用计数。这个问题是次要的,但主要的问题是不能解决对象的“循环引用”。所以Java等很多语言都不用这个算法来清除垃圾。征收机制。Python中的一切都是对象,也就是说,你在Python中使用的所有变量,本质上都是类对象。其实每个对象的核心都是一个结构体PyObject,里面有一个引用计数器ob_refcnt,程序在运行过程中会实时更新ob_refcnt的值,以反映引用当前对象的名字的数量。当一个对象的引用计数值为0时,说明这个对象已经变成了垃圾,那么它就会被回收,它所占用的内存也会立即被释放。typedefstruct_object{intob_refcnt;//引用计数struct_typeobject*ob_type;}PyObject;导致引用计数+1的情况:对象被创建,比如a=23对象被引用,比如b=a对象作为参数,传入到一个函数中,比如func(a)对象as一个元素,存储在容器中,例如list1=[a,a]导致引用计数为-1对象的别名被显式销毁,例如dela对象的别名是分配一个新对象,例如当a=24时,一个对象离开了它的作用域,例如f函数执行时,func函数中的局部变量(全局变量不会)对象所在的容器被销毁,或者对象被删除从容器中。我们可以通过sys包中的getrefcount()获取一个name引用的对象的当前引用计数。sys.getrefcount()本身会将引用计数加一。循环引用计数的另一种现象是循环引用,相当于两个对象a和b,其中a引用b,b引用a,这样a和b的引用计数都为1,永远不会为0,这意味着这两个对象将永远不会被回收。这是一个循环引用。A和b组成一个引用循环。例子如下:a=[1,2]#算作1b=[2,3]#算作1a.append(b)#算作2b.append(a)#算作2dela#算作1delb#除了上面两个都算作1除了可以互相引用,对象还可以引用自己:list3=[1,2,3]list3.append(list3)循环引用导致变量计数永远不会为0,导致在无法删除的引用计数中。引用计数方法有其明显的优点,如效率高、实现逻辑简单、实时性强等。一旦一个对象的引用计数归零,内存就会被直接释放。无需像其他机制那样等待特定时刻。垃圾回收随机分配到运行阶段,处理和回收内存的时间分配到平时,正常的程序运行起来比较顺利。引用计数也有一些缺点:逻辑简单,但实现起来有点麻烦。每个对象都需要单独分配一个空间来统计引用计数,无形中增加了空间的负担,而且需要维护引用计数,维护时容易出错。在某些情况下,它可能会更慢。正常情况下,垃圾回收会比较顺利,但是当需要释放一个大对象时,比如字典,需要循环嵌套调用所有引用的对象,这可能需要很长时间。循环引用。这将是引用计数的致命伤。引用计数对此没有解决办法,所以必须使用其他垃圾回收算法来补充。也就是说,Python的垃圾回收机制有很大一部分是为了处理可能的循环引用,是对引用计数的一种补充。MarkandSweepPython使用“MarkandSweep”算法来解决容器对象可能产生的循环引用问题。(注意只有容器对象才会产生循环引用,比如列表、字典、用户自定义类的对象、元组等,数字、字符串等简单类型不会产生循环引用。作为一种优化策略,只包含简单类型的元组在标记清除算法中不被考虑)正如它的名字一样,该算法在进行垃圾收集时分为两个步骤,即:标记阶段,遍历所有如果对象可达(reachable),即有对象引用它,则将该对象标记为可达;在清除阶段,再次遍历该对象,如果发现一个对象没有被标记为可达,则将其标记为可达回收。对象将通过引用(指针)链接在一起形成一个有向图,对象构成这个有向图的节点,引用关系构成这个有向图的边。从根对象开始,沿着有向边遍历对象。可达对象标记为活动对象,不可达对象为待清除的非活动对象。所谓根对象就是一些全局变量,调用栈,寄存器,这些对象是不能删除的。我们将小黑圈视为根对象。从小黑圈开始,如果对象1可达则标记,对象2、3间接可达则标记,而4、5不可达则1、2、3为活动对象,而4和5是将被GC回收的非活动对象。如下图所示,在mark-and-clear算法中,为了跟踪容器对象,每个容器对象需要维护两个额外的指针,用于组成容器对象的双端链表。和删除操作。Python解释器(Cpython)维护了两个这样的双端链表,一个链表存储需要扫描的容器对象,另一个链表存储暂时不可达的对象。图中,两个链表分别命名为“ObjecttoScan”和“Unreachable”。图中的例子是这样一种情况:link1、link2、link3组成一个引用环,link1也被一个变量A引用(其实这里还是叫名字A比较好)。link4是自引用的,也构成了一个引用循环。从图中我们还可以看出,每个节点除了有一个记录当前引用计数的变量ref_count外,还有一个gc_ref变量。这个gc_ref是ref_count的副本,所以初始值就是ref_count的大小。gc启动时,会逐一遍历“ObjecttoScan”链表中的容器对象,将当前对象引用的所有对象的gc_ref减一。(扫描link1时,因为link1引用了link2,所以link2的gc_ref会减一,然后再扫描link2。由于link2引用了link3,所以link3的gc_ref会减一.....)"Objectsto"检查完"Scan"链表中的所有对象后,两个链表中对象的ref_count和gc_ref如下图所示。这一步相当于去掉了循环引用对引用计数的影响。然后gc会再次扫描所有的容器对象,如果对象的gc_ref值为0,那么这个对象就会被标记为GC_TENTATIVELY_UNREACHABLE,移到“Unreachable”列表中,下图中的link3和link4就是这种情况。如果对象的gc_ref不为0,则该对象会被标记为GC_REACHABLE,同时当gc发现某个节点可达时,会递归地把所有从该节点可达的节点标记为GC_REACHABLE,这就是遇到的情况fi中的link2和link3图如下。除了将所有可达的节点标记为GC_REACHABLE之外,如果该节点当前在“Unreachable”列表中,则需要将其移回“ObjecttoScan”列表。下图是link3移回后的情况。第二次遍历所有对象遍历完后,“Unreachable”链表中存在的对象就是真正需要释放的对象。如上图所示,此时Unreachable链表中存在link4,gc立即释放。上面描述的垃圾回收阶段会暂停整个应用程序,等待标记被清除,然后再恢复应用程序的运行。mark-and-sweep的好处是可以解决循环引用的问题,并且在整个算法的执行过程中没有额外的开销。缺点是在执行标记和扫描时,正常程序会被阻塞。还有一个缺点就是mark-and-sweep算法执行多次后,程序的堆空间会产生一些小的内存碎片。分代垃圾收集器(Generationalgarbagecollector)分代回收技术是20世纪80年代初期发展起来的一种垃圾收集机制,也是Java垃圾收集的核心算法。分代回收是基于一个统计事实,对于一个程序来说,一定比例的内存块的生命周期比较短;而其余内存块的生命周期相对较长,甚至可以从程序开始到程序结束。寿命短的对象所占比例通常在80%到90%之间。因此,简单地认为:对象存在的时间越长,它就越有可能不是垃圾,越不应该被回收。这样在执行mark-clear算法时可以有效减少遍历对象的数量,从而提高垃圾回收的速度,这是一种以空间换时间的方法策略。Python将所有对象分为三代:0、1、2;所有新创建的对象都是第0代的对象;当某一代的对象在垃圾回收中幸存下来并且仍然存活时,它被归类为下一代的对象。那么,对象的划分标准是什么?将一个对象随机分成某一代就足够了吗?答案是不。其实对象生成有很多学问,一个好的划分标准可以显着提高垃圾回收的效率。Python内部根据对象的生存时间将对象分为3代,每一代都由一个gc_generation结构体维护(定义在Include/internal/mem.h中):structgc_generation{PyGC_Headhead;整数阈值;/*收集阈值*/intcount;/*年轻一代的分配或收集计数*/};其中:head,可以收集对象链表的头部,代中的对象通过链表维护阈值。只有当计数超过这个阈值时,Python的垃圾回收操作才会扫描本代的对象计数和计数器,不同代的统计项是不同的。Python虚拟机的运行时状态由Include/internal/pystate.h中的pyruntimestate结构体表示,其中有一个_gc_runtime_state(Include/internal/mem.h)结构体,保存了GC状态信息,包括3个对象世代。这3代在GC模块(Modules/gcmodule.c)_PyGC_Initialize函数中初始化:structgc_generationgenerations[NUM_GENERATIONS]={/*PyGC_Head,threshold,count*/{{{_GEN_HEAD(0),_GEN_HEAD(0),0}},7000},{{{_GEN_HEAD(1),_GEN_HEAD(1),0}},10,0},{{{_GEN_HEAD(2),_GEN_HEAD(2),0}},10,0},};为了讨论方便,我们将这三代分别称为:第一代、中代和老一代。这三个generation初始化之后,对应的gc_generation数组是这样的:每个gc_generation结构体链表的头节点都指向自己,也就是说每个collectibleobject链表一开始都是空的,counter字段count被初始化为0,并且threshold字段threshold有自己的策略。如何理解这些策略?当Python调用_PyObject_GC_Alloc为需要跟踪的对象分配内存时,该函数会将初级代的计数计数器加1,然后将对象连接到初级代的对象列表中。当Python调用PyObject_GC_Del释放垃圾对象的内存时,该函数会统计第一代计数器,1,如果_PyObject_GC_Alloc递增计数超过阈值(700),就会调用collect_generations进行一次垃圾回收(GC).collect_generations函数从老年代开始,逐个遍历每一代,找到最老的需要回收的代(,count>threshold)。然后调用collect_with_callback函数开始回收生成,这个函数最后调用collect函数。collect函数处理某一代时,首先将比自己小的代的counter计数重置为0,然后移除它们的objectlist,加入自己并执行GC算法,最后将下代的counter递增1.所以:系统每增加701个需要GC的对象,Python就会进行一次GC操作。每次GC操作需要处理的代可能不同,count和threshold共同决定某一代需要进行GC(count>threshold),它之前的所有年轻代也同时进行GC以多代执行GC,Python将它们的对象链表拼接在一起,执行完一次性处理GC后清零计数,然后下一代的计数加1这里举个简单的例子:第一个generation触发GC操作,Python执行collect_generations函数。发现最老的达到阈值的代是中生代,于是调用collection_with_callback(1),其中1是数组中中生代的下标。collection_with_callback(1)最后调用的是collect(1),先对下一代计数器加1;然后将当前代和所有之前的年轻代计数器重置为零;最后调用gc_list_merge将这些可回收??对象的链表合并在一起:最后,collect函数执行标记清除算法对合并后的链表进行垃圾回收。这就是分代回收机制的全部秘密。看似复杂,但稍微总结一下就可以得到一些直截了当的策略:每701个需要GC的新对象,每11次触发新一代GC新一代GC触发一次MesozoicGC。每11次MesozoicGC执行一次。老年代GC触发一次(老年代GC还受其他策略影响,频率较低)。在执行某一代GC之前,新生代对象列表也被移入这一代,与GC一起,一个对象创建后,随着时间的推移逐渐移入老年代,回收频率逐渐降低.Python中的gc模块gc模块是我们在Python中进行内存管理的接口,Python程序员一般不需要关心自己程序的内存管理问题,但是有时候,比如你发现你的程序有内存泄漏,可能需要使用gc模块的接口来排查问题。有些Python系统会关闭自动垃圾回收,程序会自行判断回收的时机。据说Instagram系统就是这样做的,整体运行效率提升了10%。常用函数:set_debug(flags):设置gc的调试日志,一般设置为gc.DEBUG_LEAK,可以看到内存泄漏的对象。collect([generation]):执行垃圾收集。具有循环引用的对象将被回收。该函数可以传参,0表示只回收0代垃圾对象,1表示回收0代和1代对象,2表示回收0、1、2代对象。如果没有传递参数,将使用2作为默认参数。get_threshold():获取gc模块执行垃圾收集的阈值。返回的是一个元组,0th是第0代的阈值,first是1代的阈值,second是2代的阈值。set_threshold(threshold0[,threshold1[,threshold2]):设置执行垃圾收集的阈值。get_count():获取当前自动垃圾回收的计数器。返回一个元组。第0个是零代垃圾对象的个数,第一个是零代链表的遍历次数,第二个是第1代链表的遍历次数。以上就是本次分享的全部内容。觉得文章还不错的话,请关注公众号:Python编程学习圈,每日干货分享,发送“J”还能领取大量学习资料。或者去编程学习网了解更多编程技术知识。
