重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
python中的变量和java中的变量本质是不一样的,python中的变量实质上是一个指针(指针的大小固定的)
十载的无棣网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。全网营销推广的优势是能够根据用户设备显示端的尺寸不同,自动调整无棣建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。成都创新互联公司从事“无棣网站设计”,“无棣网站推广”以来,每个客户项目都认真落实执行。
is可以用来判断id是否相等
对于这种赋值,虽然所赋值是相同的,但是他们的id不同,即他们是不同的对象,a is b 即为false ,但是有个特例: a = 1 b = 1 时他们的id相同。其实这是python内部的优化机制,对于小整数和小的字符串来说,python在前边定义一个对象时,下次在遇到时会直接调用前边生成的对象,而不会去重新申请一个。
他们的对象内存地址不一样,但是,a和b里的值是相等的,这是由于a和b都为list,而list里有内置的魔法函数 eq 通过 eq 魔法函数可以判断里边两个的值是否相同,若相同则返回True
python中垃圾回收的算法回收的算法是采用引用计数,当程序中有一个变量引用该python对象时,python会自动保证该对象引用计数为1;当程序中有两个变量引用该python对象时,python会自动保证该对象计数器为2, 以此类推,当一个对象的引用计数器变为0 时,则说明程序中不再有变量对其进行引用,因此python就会回收该对象。
大多数情况,python的ARC都能准确,高效的回收系统中的每一个对象。但如果系统中出现循环引用时,比如a对象持有一个实例变量引用对象b,而b对象又持有一个实例变量引用对象a,此时 两个对象的计数器都为1, 而实际上python不再需要这两个对象,也没有程序在引用他们,系统回收他们时python的垃圾回收器就没有那儿快,要等到专门的循环垃圾回收器(Cyclic Garbage Collector)来检测并回收这种引用循环
当一个对象被垃圾回收式,python就会自动调用该对象的 del 方法
当没有注释掉x = im时, item对象被两个变量所引用,所以在执行完del im时并不会去回收item对象,所以先输出--------,当程序完全执行完成后,引用item的对象的变量被释放,然后系统便会执行 del 方法,回收item对象。
当 x = im被注释后,只有一个变量去引用item对象,所以在执行完后程序变回去调用 del 方法,回收item对象,然后在继续向下执行 输出-----
python采用的是 引用计数 机制为主, 标记-清除 和 分代收集(隔代回收) 两种机制为辅的策略。
python里每一个东西都是对象,它们的核心就是一个结构体:PyObject
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少
引用计数为0时,该对象生命就结束了。
引用计数机制的优点:
1、简单
2、实时性:一旦没有引用,内存就直接释放了,不用像其他机制得等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
引用计数机制的缺点:
1、维护引用计数消耗资源
2、循环引用
案例:
循环引用导致内存泄露
有三种情况会触发垃圾回收:
gc模块提供一个接口给开发者设置垃圾回收的选项。上面说到,采用引用计数的方法管理内存的一个缺陷是循环引用,而gc模块的一个主要功能就是解决循环引用的问题。
常用函数 :
gc实践案例
必须要import gc模块,并且is_enable()=True才会启动自动垃圾回收。
这个机制的主要作用就是发现并处理不可达的垃圾对象。
在Python中,采用分代收集的方法。把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾检查中,该对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中。
gc模块里面会有一个长度为3的列表的计数器,可以通过 gc.get_count() 获取。
gc模快有一个自动垃圾回收的阀值,即通过 gc.get_threshold 函数获取到的长度为3的元组,例如 (700,10,10)
每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器
注意:
如果循环引用中,两个对象都定义了 __del__ 方法,gc模块不会销毁这些不可达对象,因为gc模块不知道应该先调用哪个对象的 __del__ 方法,所以为了安全起见,gc模块会把对象放到 gc.garbage 中,但是不会销毁对象。
标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。 mark-sweepg 在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。
标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。
1.Python的垃圾回收机制原理
Python无需我们手动回收内存,它的垃圾回收是如何实现的呢?
引用计数为主(缺点:循环引用无法解决)
引入标记清除和分代回收解决引用计数问题
引用计数为主+标记清除和分代回收为辅
垃圾回收(GC)
(1)引用计数
a = [1] # [1]对象引用计数增加1,ref=1
b = a # [1]对象引用计数增加1,ref=2
b = None # [1]对象引用计数减少1,ref=1
del a # [1]对象引用计数减少1,ref=0
a = [1],当把列表 [1] 赋值给 a 的时候,它的引用计数就会增加1,此时列表 [1] 对象的引用计数ref=1 ; b = a 又把 a 赋值给 b ,a和b 同时引用了列表[1]对象,ref又增加1,此时 ref =2。继续执行 b = None, 让b指向None,这个时候它就不会指向原来的列表[1]对象,列表[1]对象的引入计数就会减少1,又变成ref=1。执行del a ,引用计数就会减少1,这个时候 ref = 0。当对象的引用计数为0就可以回收掉,
注意:del 作用就会减少对象引用计数,并不是销毁对象。只有当引用计数为0的时候,Python解释器才回去把对象占用的内存回收掉。
// object.h
struct _object {
Py_ssize_t ob_refcnt; # 引用计数值
}PyObject;
① 什么时候引用计数增加呢?
② 什么时候引用计数会减少呢?
(2)引用计数无法解决循环引用问题
循环引用
a = [1] # 对象[1]引用计数增加1,ref=1
b = [2] # 对象[2]引用计数增加1,ref=1
a.append(b) # b被a引用,对象[2]引用计数增加1,ref=2
b.append(a) # a又被b引用,对象[1]引用计数增加1,ref=2
del a # 对象[1]引用计数减少1,ref=1
del b # 对象[2]引用计数减少1,ref=1
(3)标记清除(Mark and Sweep)
(4)分代回收
import gc
众所周知,Python 是一门面向对象语言,在 Python 的世界一切皆对象。所以一切变量的本质都是对象的一个指针而已。
Python 运行过程中会不停的创建各种变量,而这些变量是需要存储在内存中的,随着程序的不断运行,变量数量越来越多,所占用的空间势必越来越大,如果对变量所占用的内存空间管理不当的话,那么肯定会出现 out of memory。程序大概率会被异常终止。
因此,对于内存空间的有效合理管理变得尤为重要,那么 Python 是怎么解决这个问题的呢。其实很简单,对不不可能再使用到的内存进行回收即可,像 C 语言中需要程序员手动释放内存就是这个道理。但问题是如何确定哪些内存不再会被使用到呢?这就是我们今天要说的垃圾回收了。
目前垃圾回收比较通用的解决办法有三种,引用计数,标记清除以及分代回收。
引用计数也是一种最直观,最简单的垃圾收集技术。在 Python 中,大多数对象的生命周期都是通过对象的引用计数来管理的。其原理非常简单,我们为每个对象维护一个 ref 的字段用来记录对象被引用的次数,每当对象被创建或者被引用时将该对象的引用次数加一,当对象的引用被销毁时该对象的引用次数减一,当对象的引用次数减到零时说明程序中已经没有任何对象持有该对象的引用,换言之就是在以后的程序运行中不会再次使用到该对象了,那么其所占用的空间也就可以被释放了了。
我们来看看下面的例子。
函数 print_memory_info 用来获取程序占用的内存空间大小,在 foo 函数中创建一个包含一百万个整数的列表。从打印结果我们可以看出,创建完列表之后程序耗用的内存空间上升到了 55 MB。而当函数 foo 调用完毕之后内存消耗又恢复正常。
这是因为我们在函数 foo 中创建的 list 变量是局部变量,其作用域是当前函数内部,一旦函数执行完毕,局部变量的引用会被自动销毁,即其引用次数会变为零,所占用的内存空间也会被回收。
为了验证我们的想法,我们对函数 foo 稍加改造。代码如下:
稍加改造之后,即使 foo 函数调用结束其所消耗的内存也未被释放。
主要是因为我们将函数 foo 内部产生的列表返回并在主程序中接收之后,这样就会导致该列表的引用依然存在,该对象后续仍有可能被使用到,垃圾回收便不会回收该对象。
那么,什么时候对象的引用次数才会增加呢。下面四种情况都会导致对象引用次数加一。
同理,对象引用次数减一的情况也有四种。
引用计数看起来非常简单,实现起来也不复杂,只需要维护一个字段保存对象被引用的次数即可,那么是不是就代表这种算法没有缺点了呢。实则不然,我们知道引用次数为零的对象所占用的内存空间肯定是需要被回收的。那引用次数不为零的对象呢,是不是就一定不能回收呢?
我们来看看下面的例子,只是对函数 foo 进行了改造,其余未做更改。
我们看到,在函数 foo 内部生成了两个列表 list_a 和 list_b,然后将两个列表分别添加到另外一个中。由结果可以看出,即使 foo 函数结束之后其所占用的内存空间依然未被释放。这是因为对于 list_a 和 list_b 来说虽然没有被任何外部对象引用,但因为二者之间交叉引用,以至于每个对象的引用计数都不为零,这也就造成了其所占用的空间永远不会被回收的尴尬局面。这个缺点是致命的。
为了解决交叉引用的问题,Python 引入了标记清除算法和分代回收算法。
显然,可以包含其他对象引用的容器对象都有可能产生交叉引用问题,而标记清除算法就是为了解决交叉引用的问题的。
标记清除算法是一种基于对象可达性分析的回收算法,该算法分为两个步骤,分别是标记和清除。标记阶段,将所有活动对象进行标记,清除阶段将所有未进行标记的对象进行回收即可。那么现在的为问题变为了 GC 是如何判定哪些是活动对象的?
事实上 GC 会从根结点出发,与根结点直接相连或者间接相连的对象我们将其标记为活动对象(该对象可达),之后进行回收阶段,将未标记的对象(不可达对象)进行清除。前面所说的根结点可以是全局变量,也可以是调用栈。
标记清除算法主要用来处理一些容器对象,虽说该方法完全可以做到不误杀不遗漏,但 GC 时必须扫描整个堆内存,即使只有少量的非可达对象需要回收也需要扫描全部对象。这是一种巨大的性能浪费。
由于标记清除算法需要扫描整个堆的所有对象导致其性能有所损耗,而且当可以回收的对象越少时性能损耗越高。因此 Python 引入了分代回收算法,将系统中存活时间不同的对象划分到不同的内存区域,共三代,分别是 0 代,1 代 和 2 代。新生成的对象是 0 代,经过一次垃圾回收之后,还存活的对象将会升级到 1 代,以此类推,2 代中的对象是存活最久的对象。
那么什么时候触发进行垃圾回收算法呢。事实上随着程序的运行会不断的创建新的对象,同时也会因为引用计数为零而销毁大部分对象,Python 会保持对这些对象的跟踪,由于交叉引用的存在,以及程序中使用了长时间存活的对象,这就造成了新生成的对象的数量会大于被回收的对象数量,一旦二者之间的差值达到某个阈值就会启动垃圾回收机制,使用标记清除算法将死亡对象进行清除,同时将存活对象移动到 1 代。 以此类推,当二者的差值再次达到阈值时又触发垃圾回收机制,将存活对象移动到 2 代。
这样通过对不同代的阈值做不同的设置,就可以做到在不同代使用不同的时间间隔进行垃圾回收,以追求性能最大。
事实上,所有的程序都有一个相识的现象,那就是大部分的对象生存周期都是相当短的,只有少量对象生命周期比较长,甚至会常驻内存,从程序开始运行持续到程序结束。而通过分代回收算法,做到了针对不同的区域采取不同的回收频率,节约了大量的计算从而提高 Python 的性能。
除了上面所说的差值达到一定阈值会触发垃圾回收之外,我们还可以显示的调用 gc.collect() 来触发垃圾回收,最后当程序退出时也会进行垃圾回收。
本文介绍了 Python 的垃圾回收机制,垃圾回收是 Python 自带的功能,并不需要程序员去手动管理内存。
其中引用计数法是最简单直接的,但是需要维护一个字段且针对交叉引用无能为力。
标记清除算法主要是为了解决引用计数的交叉引用问题,该算法的缺点就是需要扫描整个堆的所有对象,有点浪费性能。
而分代回收算法的引入则完美解决了标记清除算法需要扫描整个堆对象的性能浪费问题。该算法也是建立在标记清除基础之上的。
最后我们可以通过 gc.collect() 手动触发 GC 的操作。
题外话,如果你看过 JVM 的垃圾回收算法之后会发现 Python 的垃圾回收算法与其是如出一辙的,事实再次证明,程序语言设计时是会相互参考的。