美文网首页编程
面试日记--python的内存管理

面试日记--python的内存管理

作者: SavingUnhappy | 来源:发表于2019-03-13 13:26 被阅读0次

    面试中被问到python的内存管理,只是说是python有自己的内存管理机制,有自己的垃圾回收机制,却不能详细作答,面试官表示很遗憾。建议我代码的业务逻辑需要想,但是学习需要深入底层,也有助于扩宽自己的知识面,对自己之后的学习路径有帮助,哈哈,感谢面试官帮我指出自己的不足。

    回家马上查资料,先解决这个问题。

    首先看看各种python常见面试题上的答案:

    python内存管理是由私有堆空间管理的,所有的python对象和数据结构都存储在私有堆空间中。程序员没有访问堆的权限,只有解释器才能操作。为python的堆空间分配内存的是python的内存管理模块进行的,核心api会提供一些访问该模块的方法供程序员使用。python自有的垃圾回收机制回收并释放没有被使用的内存供别的程序使用。

    如果仅仅问道这,上面的答案也足够了,但是面试官想要了解到更多,可能会衍生一些别的问题,那上面的答案就不够了。

    以下内容:
    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

    语言的内存管理是语言设计的一个重要方面。他是决定语言性能的重要因素。无论是C的手动管理还是java的垃圾回收,都成为语言重要的特征。下面已python语言为例子,说明一门动态类型的面向对象的语言的内存管理方式。


    对象的内存使用

    赋值语句是语言最常见的功能了。但即使是最简单的赋值语句,也可以很有内涵.
    首先看看python的赋值语句:

    a = 1
    

    整数“1”为一个对象,存储在内存空间中。a是一个引用。利用赋值语句,将引用a指向对象1。Python是动态类型的语言,对象与引用分离。文章作者比较形象的解释就是:Python像使用“筷子”那样,通过引用来接触和翻动真正的食物——对象。

    下面就是一系列的实验了,建议亲自尝试
    可以通过python的内置函数id(),来探索对象在内存的存储。

    >>> a = 1
    >>> id(a)
    140035503539424  # 内存地址的十进制表示
    >>> hex(id(a))
    '0x7f5c8e71c4e0'  # 内存地址的十六进制表示
    

    在python中整数和短小的字符,python都会缓存这些对象,以便重复使用,当我们创建多个等于1的引用的时候,实际是让所有引用都指向同一个对象:

    >>> b = 1
    >>> id(b)
    140035503539424  # 等于上面id(a)的值
    

    对比可以看出a和b实际是指向同一个对象的不同引用。
    为了校验两个引用指向同一个对象,我们可以用“is”来判断。is用于判断两个引用所指的对象是否相同。

    a = 1
    b = 1
    print(a is b)
    True
    
    a = "good"
    b = "good"
    print(a is b)
    True
    
    a = "very good morning"
    b = "very good morning"
    print(a is b)
    True   # 原文在此处就是False,但是我的为True,通过查资料发现是python版本原因
    # Python2.3简单整数缓存范围是(-1,100),Python2.5.4以后简单整数缓存范围至少是(-5,256)。所有的短字符也都在缓存区。
    
    a = "为了校验两个引用指向同一个对象,我们可以用“is”来判断。is用于判断两个引用所指的对象是否相同。"
    b = "为了校验两个引用指向同一个对象,我们可以用“is”来判断。is用于判断两个引用所指的对象是否相同。"
    print(a is b)
    False  # 增加了字符串的长度,结果也是False
    
    a = []
    b = []
    print(a is b)
    False
    

    根据上面的运行结果,可以看到由于python缓存了整数和短字符串,因此每个对象只存有一份。比如所有的1的引用都指向同一对象。即使使用赋值语句,也只是创造了新的引用,而不是对象本身,长的字符串和其他对象可以有多个相同对象,可以使用赋值语句创建出新的对象。

    在python中,每个对象都有存有指向该对象的应用总数,即引用计数(reference count)

    我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,会创建一个临时引用,所以结果会比预期多1。

    from sys import getrefcount
    
    a = [1, 2, 3]
    print(getrefcount(a))
    # 2
    b = a
    print(getrefcount(b))
    # 3
    

    由于上述原因,getrefcount()返回的结果分别是2,3,而不是期望的1。


    对象引用对象

    python的一个容器对象(container),比如列表字典等,可以包含多个对象。实际上,容器对象中包含的并不是对象本身,而是指向各个元素对象的引用。

    class from_obj(object):
        def __init__(self, to_obj):
            self.to_obj = to_obj
    b = [1,2,3]
    a = from_obj(b)
    print(id(a.to_obj))
    print(id(b))
    # 140035473779144
    # 140035473779144
    

    可以看到a引用了对象b。

    对象引用对象是python最基本的构成方式。即使是a = 1这一赋值方式,实际上是让词典的一个键值“a”的元素引用整数对象1。该词典对象用于记录所有的全局引用。该词典引用了整数对象1。我们可以通过内置函数globals()来查看该词典。
    当一个对象a被另一个对象b引用时,a的引用计数将增加1。

    from sys import getrefcount
    a = [1, 2, 3]
    print(getrefcount(a))
    b = [a, a]
    print(getrefcount(a))
    # 2
    # 4
    

    由于对象b引用了a两次,所以a的引用计数加2。

    容器对象引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系。
    objgraph是python的一个第三方包。objgraph官网

    pip install objgraph
    使用objgraph需要安装xdot。根据自己的系统发行版本安装。
    sudo pacman -S xdot
    或者
    sudo apt install xdotsudo yun install xdot

    x = [1, 2, 3]
    y = [x, dict(key1=x)]
    z = [y, (x, y)]
    
    import objgraph
    objgraph.show_refs([z], filename='ref_topo.png')
    
    ref_topo.png

    两个对象可能互相引用,从而构成所谓的引用环(reference cycle)

    >>> a = []
    >>> b = [a]
    >>> a.append(b)
    >>> a
    [[[...]]]
    

    即使是一个对象,只需要自己引用自己,也能构成引用环。

    >>> a = []
    >>> a.append(a)
    >>> print(getrefcount(a))
    3
    

    引用环会给垃圾回收机制带来很大的麻烦,我将在后面详细叙述这一点。


    引用减少

    某个引用对象的引用计数可能减少。比如使用del关键字删除某个引用

    >>> a = [1, 2, 3]
    >>> b = a
    >>> print(getrefcount(b))
    3
    >>> del a
    >>> print(getrefcount(b))
    2
    

    del也可以删除容器中的元素,比如:

    >>> a = [1,2,3]
    >>> del a[0]
    >>> print(a)
    [2, 3]
    
    >>> b = {"q": 1, "w":2}
    >>> b
    {'q': 1, 'w': 2}
    >>> del b["q"]
    >>> b
    {'w': 2}
    

    如果某个引用指向对象a,当这个引用被重新定向到其他对象b的时候,对象a的引用计数会减少

    >>> from sys import getrefcount
    ... 
    ... a = [1, 2, 3]
    ... b = a
    ... print(getrefcount(b))
    ... 
    ... a = 1
    ... print(getrefcount(b))
    3
    2
    

    垃圾回收

    当python中的对象越来越多,他占据的内存也会越来越大。不过不需要担心太多,python会在适当的时候启动垃圾回收机制(garbage collection),将没用的对象清除,在许多语言中都有垃圾回收机制,比如Java和Ruby。

    从基本原理来说,当一个对象的引用计数降为0的时候,说明没有任何引用指向对象,这时候该对象就成为需要被清除的垃圾了。比如某个新建对象,分配给某个引用,引用数为1,当引用被删除之后,引用数为0,那么该对象就可以被垃圾回收。

    a = [1,2,3]
    del a
    

    del a之后已经没有任何引用指向[1,2,3]了,用户不可能通过任何方式接触或者动用这个对象,这个对象如果继续待在内存里,就成了不健康的数据。当python的垃圾回收机制启动的时候,python扫描到这个引用为0的对象,就会将它所占据的内存清空。

    然而清理过程是个费力的过程。垃圾回收的时候,python不能进行其他的任务,频繁的垃圾回收,会大大降低python的工作效率。如果内存中的对象不多,就没必要总启动垃圾回收。所以python只会在特定的条件下,自动启动垃圾回收。当python运行的时候,会记录其中分配对象和取消分配对象的次数,两者的差值高于某个阈值的时候,垃圾回收才会启动。
    我们可以通过gc模块的get_threshold()来查看该阈值。

    >>> import gc
    >>> gc.get_threshold()
    (700, 10, 10)
    

    返回值中,后面的两个10,是与分代回收相关的阈值,分代回收在后面会讲到。700既是垃圾回收的启动阈值。可以通过gc中的set_threshold()来重新设定。
    也可以手动使用gc.collect()启动垃圾回收机制。


    分代回收

    python同时使用了分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有长期存在被使用的对象,出于信任和效率,对于这样一些对象,我们相信他的用处,所以减少在垃圾回收中扫描他们的频率。

    python将所有的对象分为0,1,2三代,所有新建的对象都是0代对象,当某一代对象经历过垃圾回收之后,依然存活,那就归入到下一代中,垃圾回收启动时,一定会扫描所有的0代对象。如果0代对象经历过一定次数的垃圾回收,那么就启动对0待和1代的扫描清理,当1代也经历了一定数量的垃圾回收,那就启动对0,1,2,即所有的对象进行扫描。

    上面gc.get_threshold()返回的(700,10,10)中后面的两个数,意义就是,每经过10次对0代的垃圾回收,就会配合启动一次对1代的扫描,没经过10次对1代的扫描,才会启动一次对2代的垃圾回收。

    同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。

    import gc
    gc.set_threshold(700, 10, 5)
    

    孤立的引用环

    引用环的存在会给垃圾回收带来很大的困难,这些引用环可能构成无法使用,但是引用计数不为0的一些对象。

    a = []
    b = [a]
    a.append(b)
    del a
    del b
    

    上面我们先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。


    孤立的引用环

    为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。


    遍历后的结果

    在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。


    总结

    python作为一种动态类型的语言,其对象和引用分离,这与面向过程的编程语言有很大的区别。为了有效的释放内存,python内置了垃圾回收的支持。python采用了一种相对简单的垃圾回收机制,即引用计数,并因此需要解决孤立引用环的问题。Python与其它语言既有共通性,又有特别的地方。对该内存管理机制的理解,是提高Python性能的重要一步。

    相关文章

      网友评论

        本文标题:面试日记--python的内存管理

        本文链接:https://www.haomeiwen.com/subject/mlfrpqtx.html