通常来说,要写Java代码,你基本上都没必要听说垃圾回收这个概念的。这不,对于已经写了多年Java代码的我来说,我还没有哪次经历说是需要使用垃圾回收方面的知识来解决问题的。但是,我依然督促自己花了几天时间系统性地(也比较浅显地)学习了Java垃圾回收机制。我认为学习Java垃圾回收机制至少可以得到以下几方面的好处:
- 对于系统调优有直接帮助
- 增加和同行聊天或者下一份工作面试时的谈资
- 在追求技术卓越上更进一步
(一)Java堆内存的分代管理
Java垃圾回收是需要消耗CPU和内存资源的,其速度随着内存的变大而减慢,这将严重影响系统的性能。同时,Java系统中存在着这么一种现象:大多数Java对象都是“短命”的。基于此,Java采用了分代的内存管理方式,并在不同的内存代中采用不同的垃圾回收算法,从而达到对内存更细粒度的管理,最大限度地减小垃圾回收对系统本身的影响。
由上图所示,Java的堆空间被分为了三个区域,分别是新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)。新创建出来的对象首先存放在新生代,经过新生代中多次垃圾回收(在Survivor 0和Survivor 1之间来回复制),存活下来的对象将被转移到老年代。新生代中垃圾回收很频繁,这样多数“短命”的对象将得到及时清理;又由于新生代内存空间通常不大,回收速度也相对较快。在老年代中,存放着从新生代中经历了多次垃圾回收后仍然存活的对象,这些对象相对较少,而老年代内存一般很大,并不容易塞满,因此老年代的垃圾回收频率要远远低于新生代,从而减少了对系统性能的影响。永久代中主要存放Java类本身的数据信息,当Java类不再被使用时,也会被垃圾回收掉。开发者们通常无法预测永久代的大小,导致程序经常出现 “java.lang.OutOfMemoryError: Permgen space”错误,因此在Java 8中,使用jvm进程原生内存空间的Metaspace代替了永久代。在默认情况下,Metaspace将使用jvm进程所有可用的内存。
在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
上图展示了不同垃圾收集器的Stop-The-World情况,可以看出Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。
(二)垃圾回收算法
最早的垃圾回收算法有引用计数法,但由于其性能不好以及无法回收循环引用对象的问题,工程上并没有得到使用。当前Java的垃圾回收主要基于标记-清除(Mark-Sweep)算法,该算法大致包括两个步骤:
- 从GC ROOT对象开始标记所有可达对象,GC ROOT包括局部变量、静态变量及运行中的线程对象等。
- 清除掉未被标记的对象
标记-清除算法是Java垃圾回收的基本原则,在此基础上,Java还提供了几种变种算法,包括标记-压缩(Mark-Sweep-Compact)算法和标记-复制(Mark-Copy)等。
标记清除算法(Mark Sweep)
标记清除算法的原理即上文中提到的两个步骤,这种算法的优点是可以减少Stop-The-World的时间,缺点是会造成内存碎片,如下图所示:
标记压缩算法(Mark Sweep Compact)
为了解决内存碎片问题,标记压缩算法(如下图所示)在回收内存之后会将存活的对象集体压到内存的一端。压缩过程需要更新对象的引用,如前文所述,这将增加系统Stop-The-World时间。
标记复制算法(Mark Copy)
标记复制算法是一种效率相对较高的算法,因为它不涉及对无用对象的删除,只需要将标记存活的对象从一个内存区拷贝到另一个内存区。但是标记复制算法不适用于存活对象较多的老年代,因为大量的对象拷贝会降低系统性能。Java在新生代中主要采用了标记复制算法,其中包括从Eden区到Survivor区的复制和两个Survivor区之间的复制。
(三)垃圾收集器
在Java中主要有4种垃圾收集器,他们各自对于不同的内存代采用不同的算法。Java会根据当前系统的基本配置确定一个默认的垃圾收集器,你可以通过以下命令查看:
java -XX:+PrintCommandLineFlags -version
在笔者的电脑上输出为:
-XX:InitialHeapSize=268435456 -XX:+PrintCommandLineFlags -XX:+UseParallelGC
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
由红色部分可以看出,默认情况下使用了Parallel收集器,这也是多数Java机器(特别是服务器)默认的垃圾收集器。
串行收集器(Serial Collector)
顾名思义,串行收集器指采用单线程进行垃圾回收,回收时会导致长时间的Stop-The-World,主要用于单机程序。该收集器在新生代采用复制算法,在老年代采用标记-压缩算法。可以通过-XX:+UseSerialGC命令行选项激活该收集器。
并行收集器(Parallel Collector)
该收集器同样在新生代采用复制算法,在老年代采用标记-压缩算法,只是使用了多线程的方式进行垃圾回收,从而大大提高了回收效率,但是回收过程中同时需要Stop-The-World。可以通过-XX:+UseParallelGC激活该收集器。多数情况下,并行收集器是Java的默认收集器。
并发标记清除收集器(Concurrent Mark Sweep Collector,CMS)
该收集器在在新生代中采用复制算法,在老年代采用标记-清除算法(不是标记-压缩)。之所以叫“并发”,是因为在回收过程的某些阶段,回收线程和用户线程同时执行,当然不是整个回收过程都可以和用户线程并行的,该收集器也存在Stop-The-World的时候,只是相比于其他收集器来说Stop-The-World持续时间较少而已。可以通过-XX:+UseConcMarkSweepGC激活该收集器。
G1收集器(Garbage First Collector)
G1收集器是Java世界最新的收集器,在Java 9中,它将成为默认的垃圾收集器。该收集器采用与上文中提到的收集器不同方式来对待Java对内存,如下图所示。可以通过-XX:+UseG1GC激活该收集器。