生命周期的概念: 世界上的万事万物都有它的生命周期, 那么针对对象的生命周期到底是从哪里开始从哪里结束呢? 当我们创建一个对象时, 会自动分配一个内存地址给这个对象使用, 这就是生命周期的开始, 当我们不使用这个对象时, 就会回收这个对象, 并释放对象所占用的内存地址, 这就是对象的消亡;
那么针对对象的生命周期我们应该如何监控呢? 主要就是 3 个方法, 首先创建对象的时候, 会执行__new__方法, 当对象创建成功时会调用__init__这个初始化方法, 当对象被回收的时候会调用__del__方法, 我们主要就是通过这 3 个方法来监控对象的生命周期:
- class Person:
- # def __new__(cls, *args, **kwargs):
- # print('我被创建了')
- def __init__(self):
- print('创建成功了')
- def __del__(self):
- print('我被删除了')
- p=Person()
结果:
创建成功了
我被删除了
生命周期小案例:
在讲生命周期小案例之前我们先补充两个小的知识点, 就是 global 和 nonlocal 这两个关键字, global 是修饰全局变量的, 当用 global 修饰之后, 我们在局部进行赋值操作的时候, 就会把局部赋值的这个变量当成全局变量, nonlocal 这个关键字是修饰函数的变量, 特别需要注意的是 nonlocal 修饰类属性是起不到作用的, nonlocal 只能作用于函数的变量, 当我们用这个关键字修饰的时候, 那么在内函数给变量赋值就会把这个变量当成外函数所定义的变量:
global 的使用:
- num=5
- def text_global():
- global num
- num=6
- print(num)
- text_global()
nonlocal 的使用:
现在我们在来看看生命周期的小案例: 比如我们需要统计一个班级的学生, 当增加一个学生, 或者减少一个学生我们就把学生数输出出来, 分析: 首先肯定需要一个学生类, 然后当创建一个对象, 或者减少一个对象我们都需要输出当前对象的个数:
下面就把代码贴出来: 我们来分析一下代码, 首先学生的增减时外部不能控制的, 所以我们这个属性应该是私有属性, 是由类本身决定的, 当实例出一个对象会调用__init__方法, 当回收一个对象会调用__del__方法, 所以我们在这两个方法里面去操控数据以及输出, 这里需要注意一点是属性的查找操作和赋值操作有不同的机制, 比如说我们用实例查找属性时首先会在自身的__dict__去找, 如果没有就会去__class__对应的类去查找, 但是赋值操作不一样, 因为赋值操作有两种含义, 一种是修改值, 一种是增加属性, 比如说我们给实例属性一个赋值操作, 如果实例有这个属性的话就是修改, 如果实例没有这个属性的话就是给这个实例增加这个属性, 并不是继续往它的 class 去找, 我们需要记住赋值操作永远都是针对自己, 而不会去找其他人, 就好比你有一百万, 你肯定会自己留着, 而不会给别人一个道理, 那么我们给类的属性赋值, 直接用类去点, 或者通过实例找到类再去操作, 好了, 扯远了, 就酱;
内存管理机制:
首先我们从存储的层面去看, 1. 在 python 里面万物皆对象, 不像 java 里面有什么基本数据类型, 而 python 里面不存在基本数据类型, 1,2,'a', 等都是对象; 2. 所有的对象在内存里面都会开辟一块空间给它存储, 解释器会根据不同的类型以及大小开辟不同的空间存储, 在返回空间的存储地址以便于外界对这个对象的操作, 3. 对于整数和短小的字符, python 会对其进行缓存, 不会创建多个相同的对象, 4. 对于集合, 列表等容器对象存储的其他对象仅仅是存储的其他对象的引用, 并不是对象本身;
我们只验证一下 python 的缓存, 其他没啥可验证的:
接下来我们来看看垃圾回收方面: 说到垃圾回收, 肯定会涉及到引用计数器的概念, 所谓引用计数器就是对当前对象被引用的个数, 当增加引用的时候, 这个引用计数器就会自动加一, 当减少一个引用的时候, 这个引用计数器就会减一, 举例看看:
从下面的代码来看, 获取引用个数是通过 sys 模块的 getrefcount() 函数, 里面直接传对象, 并且需要注意的是当用 getrefcount() 获取引用个数的时候, 会比真正的引用个数大一, 因为当前也有一个正在被 getrefcount 用着; 但是当这个函数执行完之后, 那么它的引用也会被释放, 下面我们再来看看什么情况会增加: 1. 当我们实例出对象的时候, 2. 当这个对象被赋值引用的时候, 3. 当对象被函数引用的时候, 特别需要注意的是被函数引用个数是加 2,4. 当被容器当成元素的时候, 那么什么情况又会减少呢? 1. 使用 del 删除的时候, 2. 函数执行完毕的时候, 3. 容量被删除或者容器内该元素被移除,
我们在来分析一下为什么对象传进函数会增加两次呢? 这两次分别是哪里: 通过代码可以看到 person1 在 text 函数的属性里出现过两次, 分别是__globals__和 func_globals, 所以对象被函数调用计数器会加 2
引用计数器循环引用的问题: 比如说有这样的一个场景, 有一个 teacher 类有一个 student 类, teacher 类有一个属性指向 student, 而 student 也有个属性指向 teacher, 在这种情况下当我们分别删除实例出的对象之后, 那属性指向的对象会被回收吗? 我们看代码: 这里我们安装了一个 objgraph 模块, 里面有个方法 count 统计的是垃圾回收器跟踪的对象个数, 说白了就是当前实例出来的对象的个数, 从下面的代码我们可以看到当我们把 s 和 t 实例删除之后, 实例对象仍然存在, 这就造成了内存泄漏, 那针对这种问题该怎么解决呢? 其实 python 的内存管理机制由两部分组成: 计数器机制和垃圾回收机制;
垃圾回收机制: 其实一般情况下的垃圾通过引用计数器都能回收, 垃圾回收机制是针对于通过了引用计数器还存活的对象, 在找到循环引用的对象, 最后在释放它;
垃圾回收机制的原理:
1. 通过引用计数器还能存活的垃圾其实就是被循环引用的对象, 所以我们第一步肯定是要找到所有被循环引用的对象, 能被循环引用的对象其实都是容器对象, 容器对象指的就是能装对象的容器, 比如说字典, 列表, 等等, 你想想像 int 这种对象能产生循环引用的效果吗, 产生循环引用就是因为互相都有属性指向对方, 在说明白一点就是互相都能装其他对象, 那 int 能装其他对象吗, 明显是不能的, 所以我们第一步就是找到所有的容器对象, 找到了所有的容器对象, 肯定要找个东西存下来啊, 并且这个东西要很灵活方便的处理里面装的东西, 所以选择了双向链表, 对于双向链表的优点自行百度;
2. 找到了容器变量之后, 自然是看这个对象有多少个引用, 大家如果不好理解, 其实就可以把容器对象理解成普通的没被回收的对象, 底层是把引用的数字存在了变量 gc_refs 里面;
3. 对于每个容器对象 a, 要找到它所引用的容器对象 b, 并且把容器对象 b 的引用数减一, 这一步就是最重要的, 相当于把互相引用的那条引用去掉;
4. 通过了第 3 步, 如果有容器对象的引用数为 0. 证明它肯定是被循环引用才活到现在的, 所以就把这个对象回收掉;
垃圾回收机制的优化: 比如说我们有一万个对象都是存活的, 每隔 5s 进行一次检测, 总不能每次都去检测一万个吧, 这样是很耗时的, 性能会降低很多, 所以 python 使用的是分代回收, 什么是分代回收呢? 比如说我们总共有一百个对象, 在经历了 10 次检测之后, 有 50 个对象还是存活的, 那么我们就把这 50 个对象分为一代, 在进行重新检测的时候我们就不用检测这 50 个对象当检测到一定次数的时候, 会促发检测一次这 50 个对象, 也就是一代, 当一代经历过 10 次检测之后还剩 5 个存活对象, 那就把这 5 个存活对象分为二代, 当一代检测到一定次数就会促发第二代的检测, 如此类推, 其实就有点像是差生和优生之间的检测, 在经过 100 次考试之后, 有 10 个一直都是优生, 那么这 10 个人接下来就可以不用进行测试, 当继续测试到一定次数之后, 这 10 个人才又参加一次测试, 这是这个道理;
垃圾回收机制配置参数: 我们的垃圾回收机制除了使用分代回收之外, 还在是否促发检测上面给定了一个阈值, 只有当目前存在的对象达到这个阈值时才会促发检测; 我们通过 gc 模块可以得到这个阈值, 也能修改这个阈值: 我们可以看到下面的代码, 一共有三个参数, 第一个是阈值, 第二个是促发一代检测的次数, 第三个是促发二代检测的次数;
垃圾回收机制自动回收需要的条件:
1. 垃圾回收机制是打开的; 垃圾回收可以通过 gc 模块来进行打开或者关闭, 也可以判断当前垃圾回收是否打开; 默认情况下是打开的;
2. 达到指定的阈值;
垃圾回收机制的手动触发: 我们知道垃圾回收机制在一定的条件下会自动促发, 但是这个条件往往比较高, 有时候需要手动促发, 手动促发用的是 gc 模块的 collect 方法, 这个方法有一个选填参数, 默认不填是检测 0,1,2 代的垃圾, 填 1 检测的是 0 和 1 代的垃圾, 填 2 检测的是 0,1,2, 的垃圾; 我们可以看下面的代码通过 gc.collect() 把之前不能回收的循环引用的对象都回收了;
垃圾回收机制解决循环引用的兼容问题: 如下代码所示, 我们把 python 版本改成了 2.7, 并且重写了类的 del 方法, 我们会发现 gc.collect() 函数不管用了, 还是回收不到这两个对象, 接下来我们就来解决这样的问题
解决方案 1: 通过弱引用来代替强引用, 引用对象, 所谓弱引用其实就是当我们使用弱引用引用一个对象时, 这个对象的 refcount 不会增加, 这样的话就不会造成循环引用, 自然就解决了; 我们看下面的代码, 弱引用用的是 weakref 模块下的 ref 方法; 在补充一点获取弱引用字典可以使用 weakref.WeakKeyDictionary 这个是字典的 key 全是弱引用以及 weakref.WeakValueDictionary 这个是字典的值全是弱引用;
解决方案 2: 就是我们手动把他们之一的引用给释放掉, 就相当于是置空, 这样也就没有了循环引用:
至此, 这一大块就结束了, 讲的不对的地方, 希望大家给出指正的意见;
来源: http://www.jianshu.com/p/22a8bedc39fd