04_jvm_06_garbage_collection

第五十二章 Java虚拟机的垃圾回收机制(Garbage Collection)

1 概述

Java语言的一个著名特性就是对象自动回收机制,即开发人员只需要在需要时创建对象即可,而无需担心对象如何释放、在何时释放等问题。Java虚拟机中有一个部件专门处理对象的自动释放。这个部件叫垃圾回收器(Garbage Collector)。

在Java语言问世以来,垃圾回收器也在不断的改善。因为Java虚拟机的标准文档并没有限制垃圾回收器的实现方式和算法,所以,各个Java虚拟机的实现版本都有着不同的实现。但是,总而言之,这些垃圾回收器的演化经历了以下五个阶段,我们也可以把它们看作五种常见的垃圾回收算法。

垃圾回收器运行在一个独立的守护线程中。在Java虚拟机初始化时,会启动多个系统线程,垃圾回收线程就是其中之一。垃圾回收过程可自动触发(由Java虚拟机触发),也可由程序触发。在自动触发的方式下,每个垃圾回收算法实现的触发条件均不相同,但是,它们大致上遵守以下几个准则。其一,当整体内存使用率达到一个阈值时触发垃圾回收,此时会扫描全部对象。其二,当某区域中的内存使用率达到一个阈值时触发垃圾回收,此时可能会扫描全部对象,也可能只扫描该区域内的对象。其三,当创建对象失败时会触发垃圾回收,此时会扫描全部对象。

当需要在程序中主动触发垃圾回收时,程序可以调用Runtime.gc()或者System.gc()方法。调用这两个方法的效果是相近的。它们都会向Java虚拟机传递运行垃圾回收的意愿,但是,并不能保证Java虚拟机一定会运行垃圾回收。即使运行了垃圾回收过程,也不能保证有多少对象会被回收。Java虚拟机会按照垃圾回收算法自行决定。

2 Sweep-and-Mark算法

目前,绝大多数的垃圾回收器都是基于Sweep-and-Mark算法的。这个算法可分为两个步骤。在第一个步骤中,该算法从根对象(Root Object)开始,逐个扫描被根对象引用的对象。然后,从这些对象出发,扫描更多的、被这些对象引用的对象。反复执行这个过程,直到没有更多的对象被扫描为止。在第二个步骤中,每当一个对象被扫描后,会标记该对象。在扫描结束后,未被标记的对象为可释放对象。垃圾回收器可以自行决定何时释放这些未标记的对象。在第一个步骤中,根对象(起始对象)包括:全局变量、当前调用栈上的局部变量、程序的输入参数,当前激活状态下的线程对象、和被本地函数(Native Method)使用的对象等。与引用计数算法相比(另一种常见的垃圾回收算法),Sweep-and-Mark算法能检测并释放循环引用对象,因为在扫描过程中,可被释放的循环引用对象会形成一个“孤岛”,无法从根对象扫描到。

如下图所示:Sweep-and-Mark从左侧的根对象出发,沿着引用的方向扫描。最后发现,紫色的对象未被扫描到,因此,它们是可释放对象。

图一 Sweep-and-Mark算法扫描结果

图一 Sweep-and-Mark算法扫描结果。

在得到扫描结果后,释放对象的过程可采取以下几种策略。

  1. 直接释放未被引用的对象。这种方法实现简单,直接。对象占用的空间被释放。然而,这种方法会导致内存碎片化。碎片化的问题在于,总的可用内存并不低,但是,当创建较大对象时,因为,空闲的内存都是小的碎片,无法满足连续大片的内存请求,因此,会导致内存申请失败。
  2. 第二种和第三种策略是为了解决内存碎片化而设计的。当对象被释放后,垃圾回收器还会将使用的内存“压”至(Compacting)一片连续的内存中,以使得剩余的空闲内存也是连续的。这种策略被称为Sweep-and-Compacting策略。

图二 Sweep-and-Compacting策略

图二 Sweep-and-Compacting策略。

  1. 第三种策略并不使用“压缩”策略,而是采用拷贝的方式,将所有对象移至另一个内存区域。在拷贝的过程中,已存在的对象可存放在一片连续的区域里,剩余的空闲区域也是连续的。这种策略被称为Sweep-and-Copying策略。

图三 Sweep-and-Copying策略

图三 Sweep-and-Copying策略。

3 并行Sweep-and-Mark(Concurrent Mark Sweep, CMS)算法

因为当垃圾回收器运行时,Java程序的所有线程都得暂停运行,以免发生访问冲突。为了缩短Java程序暂停的时间,Java设计者提出了并行Sweep-and-Mark算法,简称CMS算法。其背后的思想就是把Sweep-and-Mark算法并行化。Sweep-and-Mark算法分为两个步骤。第一个步骤实际上是从多源点出发遍历一个有向图(Directed Graph)。第二个步骤是多个数据拷贝的过程。这两个步骤都已有成熟的并行算法可用。

4 Serial Garbage Collection算法

在此基础之上,经过对Java对象生存周期的研究,Java设计者发现两个重要现象。其一:许多新创建的对象生存周期很短,在创建之后不久就可以被释放了。例如:临时变量/局部变量。其二:许多对象生存周期很长,当它们被创建后,它们一直处于使用状态。例如:静态对象。所以,Java设计者为了进一步加快垃圾回收的过程,将所有的对象分为新生代和老年代(Young Generation and Old Generation)。这也是我们在介绍Java虚拟机堆区域内存布局的时候讲解的对象分类的方法。

在Serial Garbage Collection算法中,它针对新生代对象使用Sweep-and-Copying策略,而对老对象使用Sweep-and-Compacting策略。这是因为,老对象存活的时间较久,一旦被“压实”了,就不需要再“压”了,可以节省数据拷贝的开销。

图四 Java虚拟机堆区域内存布局

图四 Java虚拟机堆区域内存布局。

5 Parallel Garbage Collection(Parallel GC)算法

在Serial Garbage Collection的基础上,Parallel Garbage Collection算法并行化了Serial Garbage Collection算法。这个算法非常适合运行在多核的机器上。在Java 8版本中,Parallel Garbage Collection是默认使用的垃圾回收算法。在Java 9版本中,被G1 Garbage Collection算法取代。

6 Garbage-First Garbage Collection(G1 GC)算法

G1 Garbage Collection算法是在Java 7版本中作为长期替代CMS算法引入的。G1 Garbage Collection算法有诸多特点:其一,它是一个并行算法,可以有效利用计算资源;其二,它是增量回收对象的,所以,它需要暂停程序的时间非常短。

G1 Garbage Collection算法将堆切分为小的内存区域。每次运行垃圾回收时,它仅仅完成部分内存区域的回收,因此,这种方式被称为增量回收,所以,每次垃圾回收运行的时间也较短。该算法会跟踪每个区间内的对象信息,以便于决定什么时候对该区域进行垃圾回收。当垃圾回收运行时,该算法只会运行那些最“迫切”的区域,所以,这个算法被称为Garbage-First Garbage Collection (G1 Garbage Collection)。

图五 G1 Garbage Collection算法的内存分布

图五 G1 Garbage Collection算法的内存分布。

7 Java程序存在内存泄漏吗?

从Java技术的角度看,对象在使用完毕后,都会被垃圾回收器自动回收。那么,Java程序是不是就不会出现内存泄漏呢?

不是,因为垃圾回收器只能回收未被使用的对象,在如下几种情况下,Java回收器是不会回收这些对象的。

  1. 被根对象引用的对象不会被回收。例如,程序定义了一个静态对象。如果该静态对象是一个容器对象的话(例如:链表),向该容器添加的对象不会被自动回收,即使程序再也不会使用这些对象了。
  2. String.intern()方法会将字符串加载到String类的一个私有内存中,垃圾回收器不会回收这个私有内存中的对象。
  3. 未关闭的Stream对象或者Connection对象。因为这些是底层操作系统管理的对象。垃圾回收器只能回收Java对象,而不能释放操作系统管理的资源。

所以,开发人员需要明确“未被使用”的对象和“未被引用”的对象之间的区别。垃圾回收器只能自动回收未被引用的对象。

8 总结

本章逐一介绍了目前流行的几种Java垃圾回收算法。目前,Garbage-First Garbage Collection是Java最新版本的默认使用的垃圾回收算法,因为它采用的是并行增量回收的策略,所以,Java程序暂停的时间很短。垃圾回收器只能自动回收未被引用的对象。即使程序不再使用某一对象,如果该对象仍被引用的话,垃圾回收器也是不会回收该对象的。所以,开发人员需要理解“未被引用”和"未被使用"的区别。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.