python如何管理内存?
在Python 中的对象越来越多,占用的内存越来越大,垃圾回收机制就是将没用的对象清除,释放内存。Python垃圾回收采用引用计数机制为主,标记-清除和分代回收机制为辅的策略,其中标记清除机制用来解决技术引用带来的循环引用而无法释放内存的问题,分代回收机制是为提升垃圾回收的效率。
引用计数在Python中,每个对象都有指向该对象的引用总数,即引用计数(reference count)。一个对象会记录着引用自己的对象的个数,每增加1个引用,个数加1,每减少1个引用,个数减1。在垃圾回收过程中,利用引用计数器方法,在检测到对象引用个数为 0 时,对普通的对象进行释放内存的机制。
我们可以使用 sys.getrefcount 方法,来查看每个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给 getrefcount方法时,参数实际上创建了一个临时的引用。因此,getrefcount 方法所得到的结果比期望的多1。
由上可见,l 中的 [t,27] 两个元素,都指向了同一个对象,实际上,容器对象(如,列表、字典等)中包含的并不是元素对象本身,是指向各个元素对象的引用。同时,l 的引用计数随着 ll 的创建和删除,引用计数也随着增加1和减少1。
导致引用计数增加的场景如下:
对象被创建:t = 27其它的别名被创建:ll = l作为参数传递给函数:getrefcount(l)作为容器对象的一个元素:l = [t, 27]导致引用计数减少的场景如下:
对象的别名被显式的销毁:del ll对象的一个别名被赋值给其他对象:l = 789对象所在的容器被销毁或从容器中删除对象 如,del ll 或 l.remove(t)。一个本地引用离开了它的作用域,比如上面的 getrefcount(x) 函数结束时,x指向的对象引用减1。引用计数中的循环引用循环引用即对象之间进行相互引用,出现循环引用后,利用上述引用计数机制无法对循环引用中的对象进行释放空间,从而导致内存泄漏,这就是循环引用问题,如下:
对象 test 中的元素引用 ops,而对象 ops 中元素同时来引用 test ,从而造成仅仅删除 test和 ops对象,无法释放其内存空间,因为他们依然在被引用(引用个数不为0)。进一步解释就是循环引用后,test 和 ops 被引用个数为2,删除 test 和 ops 对象后,两者被引用的个数变为1,并不是0,而Python只有在检查到一个对象的被引用个数为0时,才会自动释放其内存,所以这里无法释放 test 和 ops 的内存空间,因此这也是导致内存泄漏的原因之一。
垃圾回收当Python的对象的引用计数降为 0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾。比如新建一个对象,被赋值给某个变量,则该对象的引用计数变为1。如果变量被删除,对象的引用计数为0,那么该对象就会被垃圾回收。如上,执行 del t 后,已经没有任何引用指向之前建立的对象 9527,该对象引用计数变为0,用户不可能通过任何方式使用这个对象,当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占用的内存进行回收。
标记-清除机制——解决循环引用问题标记-清除机制顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收),标记清除用来解决引用计数机制产生的循环引用,进而导致内存泄漏的问题。循环引用只有在容器对象才会产生,比如字典,元组,列表等。首先为了追踪对象,需要每个容器对象维护两个额外的指针,用来将容器对象组成一个链表,指针分别指向前后两个容器对象,这样可以将对象的循环引用摘除,就可以得出两个对象的有效计数,我们通过如下示例,进一步了解一下。
在 标记-清除机制中,存在root链表、unreachable链表,这里简单介绍一下。
如上,在未执行 del 语句时,test、ops的引用计数都为 2。但是在 del 执行完以后,test、ops 引用次数互相减 1。test、ops陷入循环引用中,此时标记清除机制来打破这种循环引用,找到其中一端 test 开始拆test、ops的引用环。即从 test 出发,因为它有一个对 ops的引用,则将 ops的引用计数减1,然后顺着引用达到 ops,因为 ops有一个对 test的引用,同样将 test的引用减1,如此就完成了循环引用对象间环摘除。
引用环去掉以后发现,test、ops循环引用变为了0,所以test、ops就被添加到 unreachable链表 中直接被回收掉。
分代回收机制-提升垃圾回收效率解决循环引用问题,引入的标记-清除机制,处理过程非常繁琐,需要处理每一个容器对象,因此Python考虑一种改善性能的做法,基于“对象存在时间越长,越不可能在后面的程序中变成垃圾”的假设,提出分代回收机制。出于信任和效率,对于这样一些“寿命长”的对象,我们相信它们的存在价值,所以降低在垃圾回收中扫描它们的频率,分代回收是一种以空间换时间的操作方式。我们可以通过 gc.get_threshold 方法,查看分代回收机制的参数阈值设置,如下:
Python将所有的对象分为年轻代(第0代)、中年代(第1代)、老年代(第2代)三代。所有的新建对象默认是 第0代对象。当某一代对象经历过垃圾回收,若依然存活,那么它就将被划分到下一代对象。垃圾回收启动时,会扫描所有的 第0代对象。如果 第0代经过一定次数垃圾回收,那么就触发对0代和1代的扫描清理。当第1代也经历了一定次数的垃圾回收后,那么会触发对 第0,1,2代,即对所有对象进行扫描。
如上,gc.get_threshold 方法返回的 (700, 10, 10),700即是垃圾回收启动的阈值,返回的两个10是指,每10次0代垃圾回收,会执行1次1代的垃圾回收;而每10次1代的垃圾回收,会执行1次的2代垃圾回收。
同样可以用 gc.set_threshold 来调整分代回收策略,比如对 第2代对象进行更频繁的扫描,如下:
通过此分代回收机制,循环引用中的内存回收处理过程就会得到很大的性能提升。
更加详细的介绍可以阅读下这篇文章《面试中的高频问题,如何理解Python内存管理中的垃圾回收机制》
https://www.toutiao.com/i6741657155532751373/Copyright © 广州京杭网络科技有限公司 2005-2024 版权所有 粤ICP备16019765号
广州京杭网络科技有限公司 版权所有