垃圾回收

垃圾: 程序中已经不被使用的那部分内存区域。

进程结束,操作系统会回收资源;而对于正在运行运行的程序,需要用户实现垃圾回收, 一般为堆区

堆上分配的内存是用户管理的,如果用户程序不主动回收,堆上的内存早晚被消耗一空。

从大类来说分为:引用计数算法 和 可达性算法 两大类

垃圾回收模块的3个基本要求:

  1. 无内存泄漏
  2. 能够自动回收无用内存
  3. 能够内存整理或内存紧缩

几个概念

内存泄漏: 程序中所有已分配的内存都有其对应的指针指向它,若一块内存没有任何指针指向它,称为内存泄漏。即该块内存是无用内存块且不可达。

根集: 在程序运行的任意状态中,寄存器(考虑多核CPU在多线程下)、程序栈和静态数据段中所有指针的集合叫根集。

可达: 这块内存从根集出发,可以找到一个指向它的指针,不可达就是找不到指向它的指针

常用的算法有:

  • 引用计数
  • 标记-清除算法

引用计数

引用计数(Reference Counting)算法是每个对象计算指向它的指针的数量,当有一个指针指向自己时计数值加1;当删除一个指向自己的指针时,计数值减1,如果计数值减为0,说明已经不存在指向该对象的指针了,所以它可以被安全的销毁了

引用计数算法的优点在于内存管理的开销分布于整个应用程序运行期间,非常的“平滑”,无需挂起应用程序的运行来做垃圾回收;而它的另外一个优势在于空间上的引用局部性比较好,当某个对象的引用计数值变为0时,系统无需访问位于堆中其他页面的单元,而后面我们将要看到的几种垃圾回收算法在回收前都回遍历所有的存活单元,这可能会引起换页(Paging)操作;最后引用计数算法提供了一种类似于栈分配的方式,废弃即回收,后面我们将要看到的几种垃圾回收算法在对象废弃后,都会存活一段时间,才会被回收。

缺点:

  • 时间上的开销,每次在对象创建或者释放时,都要计算引用计数值,这会引起一些额外的开销;
  • 空间上的开销,由于每个对象要保持自己被引用的数量,必须付出额外的空间来存放引用计数值;
  • 引用计数算法最大的缺点就在于它无法处理环形引用。 比如现在内存块X引用内存块Y,Y内存块的计数器加1,然后Y内存块引用Z内存块,Z的计数加1,后来z又引用Y,Y的计数加1得到2。假如现在X不再引用Y了,Y的计数器成为1。而由于Y,Z互相引用,形成一个环回导致内存块Y,Z永远无法被回收。即无法释放作为循环数据结构的一部分的结构。

标记-清除(make-sweep)

标记-清除(Mark-Sweep)算法依赖于对所有存活对象进行一次全局遍历来确定哪些对象可以回收,遍历的过程从根出发,找到所有可达对象,除此之外,其它不可达的对象就是垃圾对象,可被回收。整个过程分为两个阶段:标记阶段找到所有存活对象;清除阶段清除所有垃圾对象。在标记阶段,垃圾回收器扫描每个内存块,对被引用的内存块进行标记。在标记完成后,统一检测内存集中所有内存,将未被标记的内存块进行回收

缺点在于标记-清除算法是一种“停止-启动”算法,在垃圾回收器运行过程中,应用程序必须暂时停止,所以对于标记-清除算法的研究如何减少它的停顿时间

标记清除算法的核心问题是如何标记所有已经被引用的内存块。当内存分配时,可能超过某个阀值,然后触发垃圾回收。首先垃圾回收器建立一些根集合在静态数据段、寄存器和栈中。然后垃圾回收器会从根集出发查找所有的内存块引用,然后在被引用的内存块上作标记(mark),当垃圾回收器遇上一个已经被标记的对象后就不再扫描了,以防止造成环回。这个阶段就是所谓的标记阶段(Mark Phase)。当标记阶段结束后,所有被标记的内存块就称为可达对象(Reachable Object)或活对象(Live Object),而所有没有被标记的内存块则被认为是垃圾,可以被回收。这个阶段进行完就进入了清除阶段(Sweep Phase)。垃圾回收器检测内存集,逐一释放未被标记的内存。整个过程分为2个阶段:找到所有存活内存块的标记阶段;清除所有未标记的内存块的清除阶段

标记-缩并

标记-缩并算法是为了解决内存碎片问题而产生的一种算法。它的整个过程可以描述为:标记所有的存活对象;通过重新调整存活对象位置来缩并对象图;更新指向被移动了位置的对象的指针

节点拷贝

节点拷贝算法是把整个堆分成两个半区(From,To), GC的过程其实就是把存活对象从一个半区From拷贝到另外一个半区To的过程,而在下一次回收时,两个半区再互换角色。在移动结束后,再更新对象的指针引用。

文档更新时间: 2021-03-14 22:16   作者:周国强