• 【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的垃圾回收方式。

  • 相关阅读:
    SQL Server 索引基础知识(1) 记录数据的基本格式
    SQL语句优化技术分析
    AcceptEncoding: gzip,deflate
    用Delphi实现网络驱动器的映射和断开
    在DELPHI中HOOK程序的编写
    DelphiHookApi(经典)
    截获API
    窗体的扩展样式GWL_EXSTYLE:用于SetWindowLong
    delphi中WNetAddConnection2的使用
    Delphi中的线程类 TThread详解http://eelab.gxu.edu.cn/list.asp?unid=542
  • 原文地址:https://www.cnblogs.com/zhouliweiblog/p/11946819.html
Copyright © 2020-2023  润新知