python中的垃圾回收机制

1、小整数对象池

在程序中整数的使用非常的广泛,Python为了优化速度,使用了小整数对象池,避免为整数频繁申请和销毁内存的空间。 Python中对象小整数的定义时[-5,256],这些整数的对象时提前建立好的,不会被垃圾回收。在一个Python的程序中,所有位于这个范围内的整数使用的都是同一个对象。

In [1]: a = -5

In [2]: id(a)
Out[2]: 10911520

In [3]: b = -5

In [4]: id(b)
Out[4]: 10911520

In [5]: c = 256

In [6]: id(c)
Out[6]: 10919872

In [7]: d = 256

In [8]: id(d)
Out[8]: 10919872

In [9]: e = 257

In [10]: id(e)
Out[10]: 139826489334128

In [11]: f = -6

In [12]: id(f)
Out[12]: 139826489334064

​ 上面中,我们可以看到范围在[-5,256]之间的整数,包含-5和256,对象在内存中的地址是相同的,同理,单个字母也是这样。

​ 但是当定义两个相同的字符串时,引用计数为0,那么就会触发垃圾回收。

2、大整数对象池

每一个大整数,均创建一个新的对象。

In [14]: a = 1234

In [15]: id(a)
Out[15]: 139826489334576

In [16]: b = 1234

In [17]: id(b)
Out[17]: 139826489334640


3、intern机制

假如有好多个对象:

a1 = "HelloWorld"
a2 = "HelloWorld"
a3 = "HelloWorld"
a4 = "HelloWorld"
a5 = "HelloWorld"
a6 = "HelloWorld"
a7 = "HelloWorld"
a8 = "HelloWorld"

python会不会创建8个对象呢?会不会在内存中开辟8个内存 空间? 答案是不会的,想一下,如果我们写10000个对象,像上面那样,那岂不是会在内存中开辟10000个空间,那得占用多少的内存。所以在Python中有这样的一个机制—->intern机制

让他只占用一个“HelloWorld”所占的空间,靠引用计数去维护何时释放。

In [19]: a = 'abcd'

In [20]: id(a)
Out[20]: 139826471344368

In [21]: b = 'abcd'

In [22]: id(b)
Out[22]: 139826471344368

In [23]: c = b

In [24]: id(c)
Out[24]: 139826471344368

In [25]: del a

In [26]: del b

In [27]: id(c)
Out[27]: 139826471344368

总结

  • 小整数[-5,256]共用内存,常驻内存。

  • 单个字符公用内存,常驻内存

  • 单个单词,不可修改,默认开启intern机制,公用内存,引用计数为0,则销毁

  • 字符串(含有空格),不可修改,没开启intern机制,不共用对象,引用计数为0,销毁

    In [30]: a = 'hello world'
    
    In [31]: b = 'hello world'
    
    In [32]: id(a)
    Out[32]: 139826489193264
    
    In [33]: id(b)
    Out[33]: 139826489192816
    
    In [34]: c = 'helloworld'
    
    In [35]: d = 'helloworld'
    
    In [36]: id(c)
    Out[36]: 139826486896048
    
    In [37]: id(d)
    Out[37]: 139826486896048
    
    
  • 大整数不共用内存,引用计数为0,销毁

  • 数值类型和字符串类型在Python中都是不可变的数据类型,这意味着你无法修改这个对象的值,每次对变量的修改,实际上是创建了一个新的对象。

    In [39]: a = 110
    
    In [40]: id(a)
    Out[40]: 10915200
    
    In [41]: a += 1
    
    In [42]: id(a)
    Out[42]: 10915232
    
    In [43]: b = 'hello'
    
    In [44]: id(b)
    Out[44]: 139826471345768
    
    In [45]: b = 'world'
    
    In [46]: id(b)
    Out[46]: 139826471344368
    

4、Garbage collection(GC垃圾回收)

python采用的是引用计数机制为主,分代收集机制为辅的策略

当引用计数为0时,该对象的生命就结束了

  • 引用计数机制的优点:

    • 简单

    • 实时性:一旦没有引用,即引用计数为0,内存就直接释放了。不用像其他机制等到特定的时机,实时性还带来一个好处:处理回收内存的时间分摊到了平时

  • 引用计数机制的缺点:

    • 维护引用计数消耗资源

    • 循环引用

    list1 = []
    list2 = []
    list1.append(list2)
    list2.append(list1)
    

    list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(分代收集)

    GC系统所承担的工作远比”垃圾回收”多得多。实际上,它们负责三个重要任务:

    • 为新生成的对象分配内存
    • 识别哪些垃圾对象
    • 从垃圾对象那回收内存

5、垃圾回收机制

  • 1、导致引用计数+1的情况

    • 对象被创建,如:a = 10
    • 对象被引用,如:b = a
    • 对象被作为参数,传递到一个函数中,如:func(a)
    • 对象作为一个元素,存储在容器中,如:list = [a, a]
  • 2、导致引用计数-1的情况:

    • 对象的别名被显示销毁,如:del a
    • 对象的别名被赋予新的对象,如:a = 20
    • 一个对象离开他的作用域, 如函数f执行完毕时,func函数中的局部变量(全局变量不会)
    • 对象所在的容器被销毁,或从容器中删除对象
  • 3、查看一个对象的引用计数

    import sys
    a = "hello world"
    sys.getrefcount(a)
    

    可以查看a对象的应用计数,但是比正常计数大1,因为在调用函数的时候传入a,这会让a的引用计数+1

6、循环引用导致内存泄露

​ 内存泄露:

​ 申请了某些内存,但是忘记了释放,那么这就造成了内存的浪费,久而久之内存就不够用了.

import gc

class ClassA():
    def __init__(self):
        print('object born,id:%s'%str(id(self)))

def f2():
    while True:
        c1 = ClassA()
        c2 = ClassA()
        c1.t = c2
        c2.t = c1
        del c1
        del c2

#python默认是开启垃圾回收的,可以通过下面代码来将其关闭
gc.disable()

f2()

​ 执行f2(),进程占用的内存会不断增大。

  • 创建了c1,c2后这两块内存的引用计数都是1,执行c1.t=c2c2.t=c1后,这两块内存的引用计数变成2.
  • 在del c1后,引用计数变为1,由于不是为0,所以c1对象不会被销毁;同理,c2对象的引用数也是1。
  • python默认是开启垃圾回收功能的,但是由于以上程序已经将其关闭,因此导致垃圾回收器都不会回收它们,所以就会导致内存泄露。

7、垃圾回收

class ClassA():
    def __init__(self):
        print('object born,id:%s'%str(id(self)))

def f2():
    while True:
        c1 = ClassA()
        c2 = ClassA()
        c1.t = c2
        c2.t = c1
        del c1
        del c2
        gc.collect()#手动调用垃圾回收功能,这样在自动垃圾回收被关闭的情况下,也会进行回收

#python默认是开启垃圾回收的,可以通过下面代码来将其关闭
gc.disable()

f2()

有三种情况会触发垃圾回收

  • 当gc模块的计数器达到阀值的时候,自动回收垃圾
  • 调用gc.collect(),手动回收垃圾
  • 程序退出的时候,python解释器来回收垃圾

8、gc模块的自动垃圾回收触发机制

在Python中,采用分代收集的方法。把对象分为三代,一开始,对象在创建的时候,放在一代中,如果在一次一代的垃圾检查中,改对象存活下来,就会被放到二代中,同理在一次二代的垃圾检查中,该对象存活下来,就会被放到三代中。

gc模块里面会有一个长度为3的列表的计数器,可以通过gc.get_count()获取。

例如(488,3,0),其中488是指距离上一次一代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增加。例如:

print gc.get_count() # (590, 8, 0)
a = ClassA()
print gc.get_count() # (591, 8, 0)
del a
print gc.get_count() # (590, 8, 0)

3是指距离上一次二代垃圾检查,一代垃圾检查的次数,同理,0是指距离上一次三代垃圾检查,二代垃圾检查的次数。

gc模快有一个自动垃圾回收的阀值,即通过gc.get_threshold函数获取到的长度为3的元组,例如(700,10,10),每一次计数器的增加,gc模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器。

例如,假设阀值是(700,10,10):

当计数器从(699,3,0)增加到(700,3,0)gc模块就会执行gc.collect(0),即检查一代对象的垃圾,并重置计数器为(0,4,0)
当计数器从(699,9,0)增加到(700,9,0)gc模块就会执行gc.collect(1),即检查一、二代对象的垃圾,并重置计数器为(0,0,1)
当计数器从(699,9,9)增加到(700,9,9)gc模块就会执行gc.collect(2),即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)
阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页