GC(Garbage Collector,垃圾收集器)就是JVM中自动内存管理机制的具体实现。在HotSpot中,GC的工作任务主要可以划分为两大块,分别是内存的动态分配和垃圾回收。而在执行内存分配之前,GC首先会对内存空间进行划分,考虑到JVM中存活对象的生命周期会具有两极化,因此应该采取不同的垃圾收集策略,分代收集由此诞生。目前几乎所有的GC都是采用分代收集算法执行垃圾回收的。
当GC执行垃圾回收时,不可避免地会产生一些内存碎片,因为被回收的内存空间极有可能是一些不连续的内存块,这样一来将会导致没有足够的连续可用内存分配给较大的对象,不过可以使用压缩算法消除内存碎片。
评估一款GC的性能:
- 吞吐量:程序的运行时间/(程序的运行时间+内存回收的时间);
- 垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例;
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间;
- 收集频率:相对于应用程序的执行,收集操作发生的频率;
- 堆空间:Java堆区所占的内存大小;
- 快速:一个对象从诞生到被回收所经历的时间。
垃圾标记:根搜索算法
在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,这个过程称之为垃圾标记阶段。那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
目前有两种比较常见的垃圾标记算法:
- 引用计数算法:(目前很少有JVM使用)
由于引用计数算法会为程序中的每一个对象都创建一个私有的引用计数器,当目标对象被其他存活对象引用时,引用计数器中的值则会加1,不再引用时便会减1,当引用计数器中的值为0的时候,就意味着该对象已经不再被任何存活对象引用,可以被标记为垃圾对象。采用这种方式看起来似乎没有任何问题,但是如果一些明显已经死亡了的对象尽管没有被任何的存活对象引用,但是它们彼此之间却存在相互引用时,引用计数器中的值则永远不会为0,这样便会导致GC在执行内存回收时永远无法释放掉无用对象所占用的内存空间,极有可能引发内存泄露。
-
根搜索算法:(在HotSpot和大部分JVM中使用)
相对于引用计数算法而言,根搜索算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中一些已经死亡的对象因相互引用而导致的无法正确被标记的问题,防止内存泄露的发生。简单来说,根搜索算法是以根对象集合作为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活对象都会被根对象集合直接或者间接连接着),如果目标对象不可达时,就意味着该对象已经死亡,将其标记为垃圾对象。
在根搜索算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象,那么根对象集合中所存储的内容究竟是什么呢?在HotSpot中,根对象集合中包含了以下5个元素:
- Java栈中的对象引用;
- 本地方法栈中的对象引用;
- 运行时常量池中的对象引用;
- 方法区中类静态属性的对象引用;
- 与一个类对应的唯一数据类型的Class对象。
垃圾回收:分代收集算法
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
JVM中比较常见的三种垃圾收集算法:
- 标记—清除算法(Mark-Sweep)
标记—清除算法是一种非常基础和常见的垃圾收集算法,它将垃圾回收任务划分为两个阶段执行,分别是垃圾标记和内存释放。相对于另外两种内存回收算法而言,标记—清除算法不仅执行效率低下,更重要的是,由于被执行内存回收的无用对象所占用的内存空间有可能是一些不连续的内存块,不可避免地会产生一些内存碎片,从而导致后续没有足够的可用内存空间分配给较大的对象。
-
复制算法(Copying)
由于JVM中的绝大多数对象都是瞬时状态,生命周期非常短暂,所以复制算法被广泛应用于新生代中。之前笔者曾经提及过,基于分代的概念,Java堆区如果还要更进一步细分的话,还可以划分为新生代(YoungGen)和老年代(OldGen),其中新生代又可以划分为Eden空间、From Survivor空间和To Survivor空间。在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1。当执行一次Minor GC(新生代的垃圾回收)时,Eden空间中的存活对象会被复制到To空间内,并且之前已经经历过一次Minor GC并在From空间中存活下来的对象如果还年轻的话同样也会被复制到To空间内。在此大家需要注意,在满足两种特殊情况下,Eden和From空间中的存活对象将不会被复制到To空间内。首先是如果存活对象的分代年龄超过选项“-XX:MaxTenuringThreshold”所指定的阈值时,将会直接晋升到老年代中;其次当To空间的容量达到阈值时,存活对象同样也是直接晋升到老年代中,如图6-7所示。当所有的存活对象都被复制到To空间或者晋升到老年代后,剩下的均为垃圾对象,这就意味着GC可以对这些已经死亡了的对象执行一次Minor GC,释放掉其所占用的内存空间。
当执行完Minor GC之后,Eden空间和From空间将会被清空,而存活下来的对象则会被全部存储在To空间内,接下来From空间和To空间将会互换位置,如图6-8所示。其实复制算法无非就是使用To Survivor空间作为一个临时的空间交换角色,务必需要保证两块Survivor空间中一块必须是空的,这就是复制算法。尽管复制算法能够高效执行Minor GC,但是它却并不适用于老年代中的内存回收,因为老年代中对象的生命周期都比较长,甚至在某些极端的情况下还能够与JVM的生命周期保持一致,所以如果老年代也采用复制算法执行内存回收不仅需要额外的时间和空间,而且还会导致较多的复制操作影响到GC的执行效率。
-
标记—压缩算法
当成功标记出内存中的垃圾对象后,该算法会将所有的存活对象都移动到一个规整且连续的内存空间中,然后执行Full GC(老年代的垃圾回收,或者称之为Major GC)回收无用对象所占用的内存空间。当成功执行压缩之后,已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,则可以使用指针碰撞(Bump the Pointer)技术修改指针的偏移量将新对象分配在第一个空闲内存位置上,为新对象分配内存带来便捷。
指针碰撞:已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer),反之则只能使用空闲列表(Free List)执行内存分配。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合新生代和老年代等各自的特点,简单来说,就是针对不同的代空间,从而组合使用不同的垃圾收集算法。

