Python内存管理机制 Java内存管理与垃圾回收

俗话说,出来混迟早要还的,Python还有不少知识点没有总结成博客,欠了太多,先还一部分吧html

1. Python对象的内存使用

内存管理是语言设计的一个重要方面。它是决定语言性能的重要因素。不管是C语言的手工管理,仍是Java的垃圾回收,都成为语言最重要的特征。java

Python认为一切都是对象,在使用对象时须要进行内存管理,简单说,使用对象时须要借用系统资源,为对象分配内存;用完之后,一样须要释放借用的系统资源(防止内存泄露,当一个对象已经不须要再使用本该被回收时,另一个正在使用的对象持有它的引用从而致使它不能被回收,这致使本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。);对于python程序员来讲,python的解释器承担了内存管理的复杂任务,因此python程序员刻意没必要关心内存管理的问题;可是,了解一下Python的内存管理机制仍是颇有必要的;python

1)引用计数机制

Python采用引用计数机制对内存进行管理;程序员

python认为一切都是对象,它们的核心就是一个结构体:PyObjectapp

 typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

PyObject是每一个对象必有的内容,其中ob_refcnt就是作为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增长,当引用它的对象被删除,它的ob_refcnt就会减小函数

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增长计数
#define Py_DECREF(op) \ //减小计数
    if (--(op)->ob_refcnt != 0) \
        ; \
    else \
        __Py_Dealloc((PyObject *)(op))

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

以常见的赋值语句为例:性能

a = 'hello'

当python解释器执行到这条语句,首先会建立一个字符串对象'hello',而后将该对象的引用赋值给a;在python中,存在一个内部跟踪变量,用于记录全部使用中的对象各有多少个引用,这个变量称为“引用计数”;url

经过sys包中的getrefcount(),能够查看某个对象的引用计数;spa

 1 >>> import sys
 2 >>>
 3 >>> a = 'hello'
 4 >>> sys.getrefcount('hello')
 5 3
 6 >>> sys.getrefcount(a)
 7 2
 8 >>>
 9 >>> b = 'hello'
10 >>> sys.getrefcount('hello')
11 4
12 >>> sys.getrefcount(a)
13 3
14 >>>
15 >>> c = b
16 >>> sys.getrefcount('hello')
17 5
18 >>> sys.getrefcount(a)
19 4
20 >>>

上例中第3-6行中。‘hello’的引用计数为3,首先'hello'被建立时引用计数为1,以后将引用赋值给了a,引用计数加1,变为2,以后经过getrefcount()函数查看引用计数时,引用计数再加1,变为3;

上句是错的,由于:

n.
>>> import sys
>>> sys.getrefcount('winter')
3
>>>

不清楚为何开始'winter'的引用计数就是3,往后要搞明白,能够参考

Fun with Python's sys.getrefcount()

同理,a在赋值语句以后引用计数为1,以后经过getrefcount()函数查看引用计数时,引用计数加1,变为2;

那么第9-13行和第15-19行都是说明了什么状况会形成引用计数的增长

2)引用计数增长的状况

  • 对象被建立:x = 3.14
  • 另外的别名被建立:y = x
  • 被做为参数传递给函数(新的本地引用):foobar(x)
  • 成为容器对象的一个元素:myList = [123, x, 'xyz']

3)引用计数减小的状况:

  • 一个本地引用离开了其做用范围。如foobar()函数结束时
  • 对象的别名被显式销毁:del y
  • 对象的一个别名被赋值给其余对象:x = 123
  • 对象被从一个窗口对象中移除:myList.remove(x)
  • 窗口对象自己被销毁:del myList

其中del xxx会有两个结果,举例说明:

>>> import sys
>>>
>>> a = 'hello'
>>> b = a
>>>
>>> sys.getrefcount('hello')
4
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
>>>
>>> del a
>>>
>>> sys.getrefcount('hello')
3
>>> sys.getrefcount(b)
2
>>> sys.getrefcount(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>>

结果是:首先从目前的命名空间删除了a,同时,b和'hello'的引用计数减1

2. 垃圾回收机制

吃太多,总会变胖,Python也是这样。当Python中的对象愈来愈多,它们将占据愈来愈大的内存。不过你不用太担忧Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,好比Java和Ruby。尽管最终目的都是塑造苗条的提醒,但不一样语言的减肥方案有很大的差别 ,关于java,可参看Java内存管理与垃圾回收

 Python中的垃圾回收是以引用计数为主,标记-清除和分代收集为辅。

Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还能够经过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题。经过“分代回收”(generation collection)以空间换取时间来进一步提升垃圾回收的效率。

1)引用计数优缺点

当一个对象的引用被建立或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1;当对象的引用计数减小为0时,就意味着对象已经没有被任何人使用了,能够将其所占用的内存释放了。

优势:

虽然引用计数必须在每次分配和释放内存的时候加入管理引用计数的动做,然而与其余主流的垃圾收集技术相比,引用计数有一个最大的优势,即“实时性”,任何内存,一旦没有指向它的引用,就会当即被回收。而其余的垃圾收集计数必须在某种特殊条件下(好比内存分配失败)才能进行无效内存的回收。

缺点:

引用计数机制所带来的维护引用计数的额外操做与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的。而这点相比其余主流的垃圾回收机制,好比“标记-清除”,“中止-复制”,是一个弱点,由于这些技术所带来的额外操做基本上只是与待回收的内存数量有关。

若是说执行效率还仅仅是引用计数机制的一个软肋的话,那么很不幸,引用计数机制还存在着一个致命的弱点,正是因为这个弱点,使得侠义的垃圾收集历来没有将引用计数包含在内,能引起出这个致命的弱点就是循环引用(也称交叉引用)

 2)循环引用

包含其余对象引用的容器对象(好比:list,set,dict,class,instance)均可能产生循环引用。

首先,先看一个小例子:

 1 >>>
 2 >>> import sys
 3 >>>
 4 >>> a = []
 5 >>> b = []
 6 >>> a.append(b)
 7 >>>
 8 >>> id(b)
 9 41589704
10 >>>
11 >>> sys.getrefcount(a)
12 2
13 >>> sys.getrefcount(b)
14 3
15 >>> del b
16 >>>
17 >>>
18 >>> sys.getrefcount(a)
19 2
20 >>> id(a[0])
21 41589704
22 >>>

上例中咱们看到,在a.append(b)之后,b的引用计数变为2(第14行,显示为3,其实是2,由于getrefcount增长了引用计数);在咱们del b之后,b的引用计数-1,变为1(实际应该是b引用的[]的引用计数);为了肯定咱们的理论正确,咱们经过比较id值来讲明实际上b引用的[]还在占据占据内存空间(第8行和第21行);这是该对象是可达的,由于a中还在引用他;若是最后del a的话,b引用的[]的引用计数就会再减1,变为0,就会被回收;

那么,再看一个引用计数的小例子:

 1 >>> import sys
 2 >>>
 3 >>> a = []
 4 >>> b = []
 5 >>> a.append(b)
 6 >>> b.append(a)
 7 >>>
 8 >>> sys.getrefcount(a)
 9 3
10 >>> sys.getrefcount(b)
11 3
12 >>>
13 >>> del a
14 >>> del b
15 >>>

上面的例子中,在第6行之后,能够看到a和b的引用计数都是2(忽略getrefcount增长的引用计数),那么在del a和del b之后,a和b的引用计数为1,非0;因此引用计数机制就没法回收,形成了内存泄露;

 经过“标记-清除”的方法来解决循环引用问题:

3)标记-清除

“标记-清除”是为了解决循环引用的问题。能够包含其余对象引用的容器对象(好比:list,set,dict,class,instance)均可能产生循环引用。
咱们必须认可一个事实,若是两个对象的引用计数都为1,可是仅仅存在他们之间的循环引用,那么这两个对象都是须要被回收的,也就是说,它们的引用计数虽然表现为非0,但实际上有效的引用计数为0。咱们必须先将循环引用摘掉,那么这两个对象的有效计数就现身了。假设两个对象为A、B,咱们从A出发,由于它有一个对B的引用,则将B的引用计数减1;而后顺着引用达到B,由于B有一个对A的引用,一样将A的引用减1,这样,就完成了循环引用对象间环摘除。
可是这样就有一个问题,假设对象A有一个对象引用C,而C没有引用A,若是将C计数引用减1,而最后A并无被回收,显然,咱们错误的将C的引用计数减1,这将致使在将来的某个时刻出现一个对C的悬空引用。这就要求咱们必须在A没有被删除的状况下复原C的引用计数,若是采用这样的方案,那么维护引用计数的复杂度将成倍增长。

原理:“标记-清除”采用了更好的作法,咱们并不改动真实的引用计数,而是将集合中对象的引用计数复制一份副本,改动该对象引用的副本。对于副本作任何的改动,都不会影响到对象生命走起的维护。
这个计数副本的惟一做用是寻找root object集合(该集合中的对象是不能被回收的)。当成功寻找到root object集合以后,首先将如今的内存链表一分为二,一条链表中维护root object集合,成为root链表,而另一条链表中维护剩下的对象,成为unreachable链表。之因此要剖成两个链表,是基于这样的一种考虑:如今的unreachable可能存在被root链表中的对象,直接或间接引用的对象,这些对象是不能被回收的,一旦在标记的过程当中,发现这样的对象,就将其从unreachable链表中移到root链表中;当完成标记后,unreachable链表中剩下的全部对象就是名副其实的垃圾对象了,接下来的垃圾回收只需限制在unreachable链表中便可。

4)分代回收

背景:分代的垃圾收集技术是在上个世纪80年代初发展起来的一种垃圾收集机制,一系列的研究代表:不管使用何种语言开发,不管开发的是何种类型,何种规模的程序,都存在这样一点相同之处。即:必定比例的内存块的生存周期都比较短,一般是几百万条机器指令的时间,而剩下的内存块,起生存周期比较长,甚至会从程序开始一直持续到程序结束。
从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操做实际上与系统中总的内存块的数量是相关的,当须要回收的内存块越多时,垃圾检测带来的额外操做就越多,而垃圾回收带来的额外操做就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操做。为了提升垃圾收集的效率,采用“空间换时间的策略”。

原理:将系统中的全部内存块根据其存活时间划分为不一样的集合,每个集合就成为一个“代”,垃圾收集的频率随着“代”的存活时间的增大而减少。也就是说,活得越长的对象,就越不多是垃圾,就应该减小对它的垃圾收集频率。那么如何来衡量这个存活时间:一般是利用几回垃圾收集动做来衡量,若是一个对象通过的垃圾收集次数越多,能够得出:该对象存活时间就越长。

举例说明:

当某些内存块M通过了3次垃圾收集的清洗以后还存活时,咱们就将内存块M划到一个集合A中去,而新分配的内存都划分到集合B中去。当垃圾收集开始工做时,大多数状况都只对集合B进行垃圾回收,而对集合A进行垃圾回收要隔至关长一段时间后才进行,这就使得垃圾收集机制须要处理的内存少了,效率天然就提升了。在这个过程当中,集合B中的某些内存块因为存活时间长而会被转移到集合A中,固然,集合A中实际上也存在一些垃圾,这些垃圾的回收会由于这种分代的机制而被延迟。
在Python中,总共有3“代”,也就是Python实际上维护了3条链表。具体能够查看Python源码详细了解。

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

gc模块里面会有一个长度为3的列表的计数器,能够经过gc.get_count()获取。
例如(488,3,0),其中488是指距离上一次一代垃圾检查,Python分配内存的数目减去释放内存的数目,注意是内存分配,而不是引用计数的增长。例如:

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),即检查1、二代对象的垃圾,并重置计数器为(0,0,1)
    • 当计数器从(699,9,9)增长到(700,9,9),gc模块就会执行gc.collect(2),即检查1、2、三代对象的垃圾,并重置计数器为(0,0,0)

 

未完待续。。。扩展gc模块

相关文章
相关标签/搜索