软件测试布道师的江湖

深耕软件测试领域,定期分享技术干货,一起成长!

【python测试开发栈】python内存管理机制(一)—引用计数

什么是内存

在开始进入正题之前,我们先来回忆下,计算机基础原理的知识,为什么需要内存。我们都知道计算机的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的垃圾回收方式。

posted @ 2019-11-28 08:56  布道师玄柯  阅读(687)  评论(0编辑  收藏  举报