2019python面试题-垃圾回收(GC)机制

1、什么是GC

在Java中,对象所占用的内存在对象再也不使用后会自动被回收。这些工做是由一个叫垃圾回收器 (Garbage Collector )的进程完成的。python

python和其余不少高级语言同样,都自带垃圾回收机制,即GC机制。算法

 

2、GC机制

 Python中的垃圾回收是以引用计数为主,标记-清除和分代收集为辅。引用计数最大缺陷就是循环引用的问题,因此Python采用了辅助方法。缓存

 

注意:app

  一、垃圾回收时,Python不能进行其它的任务,频繁的垃圾回收将大大下降Python的工做效率;函数

  二、Python只会在特定条件下,自动启动垃圾回收(垃圾对象少就不必回收)spa

  三、当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当二者的差值高于某个阈值时,垃圾回收才会启动。操作系统

一、引用计数

Python内部使用引用计数,来保持追踪内存中的对象,全部对象都有引用计数。指针

原理code

引用计数法的原理是每一个对象维护一个ob_refcnt,用来记录当前对象被引用的次数,也就是来追踪到底有多少引用指向了这个对象。当一个对象有新的引用时,它的ob_refcnt就会增长,当引用它的对象被删除,它的ob_refcnt就会减小。当引用计数为0时,该对象生命就结束了。对象

源码以下:

// object.h
struct _object { Py_ssize_t ob_refcnt; # 引用计数值 struct PyTypeObject *ob_type; } PyObject;

 

引用计数增长的状况:

  1. 对象被建立:x='spam'
  2. 用另外一个别名被建立:y=x
  3. 被做为参数传递给函数:foo(x)
  4. 做为容器对象的一个元素:a=[1,x,'33']

引用计数减小状况

  1. 一个本地引用离开了它的做用域。好比上面的foo(x)函数结束时,x指向的对象引用减1。
  2. 对象的别名被显式的销毁:del x ;或者del y
  3. 对象的一个别名被赋值给其余对象:x=789
  4. 对象从一个窗口对象中移除:myList.remove(x)
  5. 窗口对象自己被销毁:del myList,或者窗口对象自己离开了做用域。

引用计数法有很明显的优势:

  1. 高效
  2. 运行期没有停顿 能够类比一下Ruby的垃圾回收机制,也就是 实时性:一旦没有引用,内存就直接释放了。不用像其余机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。 对象有肯定的生命周期
  3. 易于实现

原始的引用计数法也有明显的缺点:

  1. 维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比,而不像mark and sweep等基本与回收的内存数量有关。
  2. 没法解决循环引用的问题。A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数都为1,但显然应该被回收。
# 循环引用的示例:
list1 = [] list2 = [] list1.append(list2) list2.append(list1)

对于现现在强大的硬件来讲,缺点维护引用计数消耗资源还尚可接受,可是循环引用致使内存泄漏却无疑是致命的,所以python还引入了新的回收机制(标记清除和分代收集)

 

二、标记-清除算法

针对循环引用的状况:咱们有一个“孤岛”或是一组未使用的、互相指向的对象,可是谁都没有外部引用。换句话说,咱们的程序再也不使用这些节点对象了,因此咱们但愿Python的垃圾回收机制可以足够智能去释放这些对象并回收它们占用的内存空间。可是这不可能,由于全部的引用计数都是1而不是0。Python的引用计数算法不可以处理互相指向本身的对象。你的代码也许会在不经意间包含循环引用而且你并未意识到。事实上,当你的Python程序运行的时候它将会创建必定数量的“浮点数垃圾”,Python的GC不可以处理未使用的对象由于应用计数值不会到零。
 这就是为何Python要引入Generational GC算法的缘由! 

『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把全部的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间经过引用(指针)连在一块儿,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

标记清除算法做为Python的辅助垃圾收集技术主要处理的是一些容器对象,好比list、dict、tuple,instance等,由于对于字符串、数值对象是不可能形成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描全部对象。

标记清除算法在申请内存时,全部容器对象的头部又加上了PyGC_Head来实现“标记-清除”机制。任何一个python对象都分为两部分: PyObject_HEAD + 对象自己数据

源码以下:

// objimpl.h
typedef union _gc_head { struct { union _gc_head *gc_next; union _gc_head *gc_prev; Py_ssize_t gc_refs; } gc; long double dummy;  /* force worst-case alignment */ } PyGC_Head;

 

 

三、分代回收算法

  • Python将全部的对象分为0,1,2三代;
  • 全部的新建对象都是0代对象;
  • 当某一代对象经历过垃圾回收,依然存活,就被纳入下一代对象。

python在建立对象以前,会建立一个链表,零代链表,只不过这个链表是空的。每当你建立一个对象,python便会将其加入到零代链表。

 

python隔代回收的核心:对链子上的那些明明没有被引用但引用计数却不是零的对象进行引用计数减去一,看看你是否是垃圾。若是被引用屡次减去一以后仍不为零,那么会在零代链表当中继续被清理,直至引用计数为零。由于若是没有变量指向它,或者做为函数的参数,列表的元素等等,那么它就始终是零代链表中被清理的对象。当零代链表被清理达到必定次数时,会清理一代链表。一代链表被清理达到必定次数时,会清理二代链表。

所以清理的频率最高的是零代链表,其次是一代链表,再是二代链表。

 

注:python内存管理机制

原理:

  1. Python提供了对内存的垃圾收集机制,可是它将不用的内存放到内存池而不是返回给操做系统;
  2. Pymalloc机制:为了加速Python的执行效率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放;
  3. 对于Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说若是你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。

 

内存池(金字塔):

  • 第3层:最上层,用户对Python对象的直接操做
  • 第1层和第2层:内存池,有Python的接口函数PyMem_Malloc实现-----若请求分配的内存在1~256字节之间就使用内存池管理系统进行分配,调用malloc函数分配内存,可是每次只会分配一块大小为256K的大块内存,不会调用free函数释放内存,将该内存块留在内存池中以便下次使用。
  • 第0层:大内存-----若请求分配的内存大于256K,malloc函数分配内存,free函数释放内存。
  • 第-1,-2层:操做系统进行操做
相关文章
相关标签/搜索