什么是内存
在开始进入正题之前, 我们先来回忆下, 计算机基础原理的知识, 为什么需要内存. 我们都知道计算机的 CPU 相当于人类的大脑, 其运算速度非常的快, 而我们平时写的数据, 比如: 文档, 代码等都是存储在磁盘上的. 磁盘的存取速度完全不能匹配 CPU 的运算速度, 因此就需要一个中间层来适配两者的不对等, 内存由此而来, 内存的存取速率很快, 但是存储空间不大.
举一个图书馆的例子, 便于大家理解, 我们图书馆的书架就相当于磁盘, 存放了大量的图书可以供我们阅读, 但是如果书放在书架上, 我们没办法直接阅读(效率低), 只能将书取出来, 放在书桌上看, 那书桌就相当于内存.
内存回收
内存资源毕竟是有限的, 所以在使用之后, 必须被回收掉, 否则系统运行一段时间后就会因无内存可用而瘫痪. 我们软件测试领域常用的两种语言: java 和 python, 全部都采用内存自动回收的方法, 也就是我们只管申请内存, 但是不管释放内存, 由 jvm 和 python 解释器来定期触发内存回收. 作为对比, C 语言和 C++ 中, 程序员需要使用 malloc 申请内存, 使用 free 去释放内存, malloc 和 free 必须成对的出现, 否则非常容易出现内存问题.
还拿上面图书馆的例子, 假如图书馆的书看完之后放在书桌上就可以 (因为图书可自动回收), 那么很快的, 就没有位置给新进来的同学看书了. 这时候就需要图书馆管理员(jvm 或 python 解释器) 定期的回收图书, 清空书桌. 不过正常情况下, 我们离开图书馆时, 要自己清空书桌, 将书放回书架(类似 C 语言和 C++ 的内存回收方式).
python 内存管理
引用计数
python 通过引用计数来进行内存管理, 每一个 python 对象, 都维护了一个指向该对象的引用计数. python 的 sys 库提供了 getrefcount()函数来获取对象的引用计数. 下面我们看个例子(注意: 不同版本的 python, 运行结果不同, 我这里采用的是 python3.7.4):
- """
- @author: xuanke
- @time: 2019/11/27
- @function: 测试 python 内存
- """
- import sys
- class RefClass(object):
- def __init__(self):
- print("this is init")
- def ref_count_test():
- # 验证普通字符串
- str1 = "abc"
- print(sys.getrefcount(str1))
- # 验证稍微复杂点的字符串
- print(sys.getrefcount("xuankeTester"))
- # 验证小的数字
- print(sys.getrefcount(12))
- # 验证大的数字
- print(sys.getrefcount(257))
- # 验证类
- a = RefClass()
- print(sys.getrefcount(a))
- # 验证引用计数增加
- b = a
- print(sys.getrefcount(a))
- # 验证引用计数减少
- b = None
- print(sys.getrefcount(a))
- if __name__ == '__main__':
- ref_count_test()
大家先来思考下, 最终的结果会是什么?! 我觉得应该很多人都会答错, 因为不同版本的 python, 对引用变量个数有影响(主要是可复用的对象). 我们先贴出来运行结果, 再来分析产生结果的原因:
- 27
- 4
- 9
- 3
- this is init
- 2
- 3
- 2
不过提前声明一点: sys.getrefcount 函数在使用时, 因为将对象 (比如上例中的 str1) 作为参数传入, 所以会额外增加一个变量(相当于 getrefcount 持有了 str1 的引用), 因此实际每个对象的实际引用计数都得减 1. 下面分别介绍下上面的几种情况:
字符串: str1='abc'的引用数是 27-1=26, 是因为字符串'abc'比较简单, 在 python 解释器 (CPython) 中确实可能存在 26 个引用. 作为对比, 在 python2.7 中, str1 的引用变量个数是 3-1=2. 而字符串'xuanketester', 是我自定义的一个字符串, 所以不可能会有其他额外的引用, 所以其引用变量个数是 3-1=2(至于为什么是 2, 理论应该是 0, 是因为 python 解释器默认持有了所有字符串的两个引用).
数字: 数字 12 对应的引用计数个数是 9-1=8, 而 257 对应的引用计数个数是 3-1=2, 这主要是因为, 在 python 初始化过程中, 就创建了从 - 5 到 256 的数字, 缓存起来, 这样做是为了频繁的分配内存, 提高效率. 而对于不在这个区间的数字, 则会重新分配内存空间. 所以数字 12 因为被复用, 其引用计数个数是 8(在 python2.7.14 中, 其引用计数个数是 8).
类: 在上面例子中, 创建一个 RefClass 对象, 其引用计数就是 2-1=1, 因为其是一个我们自定义的类对象, 在 python 解释器 (Cpython) 中肯定不会被复用.
我们可以通过打印内存地址的方式来验证上面这几种情况:
- def memory_address_test():
- str1 = 'xuankeTester'
- str2 = 'xuankeTester'
- print(id(str1))
- print(id(str2))
- str3 = 'abc'
- str4 = 'abc'
- print(id(str3))
- print(id(str4))
- a = 12
- b = 12
- print(id(a))
- print(id(b))
- c = 257
- d = 257
- print(id(c))
- print(id(d))
按照我们上面的分析, c 和 d 的地址应该是不一样的, a 和 b 的地址是一样的, 字符串 str1 和 str2,str3 和 str4 内存地址都是一样的. 但是我在 pycharm 中, 直接运行 py 文件, 结果却和预想的不一致, 结果如下:
- 2854496960176
- 2854496960176
- 2854496857840
- 2854496857840
- 140724423258720
- 140724423258720
- 2854498931120
- 2854498931120
所有情况的内存地址都是一样的, 这是为什么呢? 我考虑到是不是 pycharm 对 py 文件做了优化, 于是我又在命令行尝试执行, 结果还是一样的. 所以, 我猜测可能是 python 解释器在执行文件时, 为了提高 py 文件的执行效率, 对文件的内存地址做了优化 - 相同内容的对象内存地址都一样.
为了验证这个想法, 我直接在 python 交互模式下执行, 果然得到了我想要的结果:
- Python 3.7.4 (tags/v3.7.4:e09359112e, Jul 8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)] on win32
- Type "help", "copyright", "credits" or "license" for more information.
- >>> a=12
- >>> b=12
- >>> id(a)
- 140724423258720
- >>> id(b)
- 140724423258720
- >>> a=257
- >>> b=257
- >>> id(a)
- 2559155778384
- >>> id(b)
- 2559155778192
- >>> a='xuankeTester'
- >>> b='xuankeTester'
- >>> id(a)
- 2559155711280
- >>> id(b)
- 2559155711280
- >>>
从上面可以看到两个 257 对应的地址确实是不一样的, 和我们最初判断的是一致的.
总结
python 通过对象的引用计数来管理内存, 其实 java 的 JVM 也有用引用计数, 所以理解了引用计数, 为我们理解 python 的垃圾回收方法打下了基础. 本计划这一篇文章就将 python 内存管理的机制讲完的, 但是发现一个内存引用计数就有很多东西得写, 所以索性就分两篇文章来写, 之后再写一篇文章来介绍 python 的垃圾回收方式.
来源: https://www.cnblogs.com/zhouliweiblog/p/11946819.html