1 垃圾回收算法
常用的垃圾回收算法有:
- 引用计数法
- 标记压缩法
- 标记清除法
- 复制算法
- 分代、分区思想
1.1 引用计数法(Reference Counting)
最经典也是最古老的一种垃圾回收算法。
思想:
- 对于对象A,只要有任何一个对象引用了A,则A的引用计数器就加一。
- 引用失效则减一。
- 当对象A的引用计数器的值为0时,回收A对象。
实现:
为每一个对象配备一个整型的计数器即可。
存在问题:
- 无法处理循环引用。因此Java的垃圾回收器没有使用该方法。
- 引用产生和消除产生额外操作,对性能有一定影响。
1.2 标记清除法(Mark-Sweep)
标记清除法是现代垃圾回收算法的思想基础。
思想:
将垃圾回收分为两个阶段:
- 标记阶段。
- 清除阶段。
实现:
一种可行的实现方式:
- 在标记阶段,通过根节点,标记所有从根节点开始的可达对象。未被标记的即是垃圾对象。
- 在清除阶段,清除所有不可达对象。
问题:
最大问题是空间碎片,回收后的空间是不连续的,尤其影响对大对象的内存分配。
1.3 复制算法(Copying)
思想:
将内存空间分为两块,每次只使用其中一块。当发生垃圾回收时:
- 将正在使用的内存中的存活对象复制到未使用的内存块中。
- 清除正在使用的内存块中的所有对象。
- 交换两个内存块的角色。
优点:
- 若内存中垃圾对象很多,此时复制的效率是较高的。
- 没有内存碎片。
问题:
单纯的复制算法,浪费了一半的内存空间。
JVM中的实现:
在Java的新生代串行垃圾回收器中,使用了复制算法的思想。
研究统计表明,绝大多数的对象都是“朝生夕死”的,因此不需要1:1划分内存空间。
HotSpot将内存空间分为新生代和老年代,其中新生代继续划分为一块较大的Eden区和两块较小的Survivor区。默认是8:1:1。
新生代:存放年轻对象的堆空间。年轻对象指刚刚创建的,或者经理垃圾回收次数不多的对象。
老年代:存放老年对象的堆空间。老年对象指经历过多次垃圾回收依然存活的对象。
回收过程:
- 将存活对象从Eden和工作中的Survivor中复制到另一个Survivor中。若Survivor已满,直接进入老年代。
- 清除Eden和工作中的Survivor中所有的对象。
- 交换两个Survivor的角色。
改进后的优点:
即保证空间的连续性,又避免浪费空间。
1.4 标记压缩法(Mark-Compact)
复制算法的高效性是建立在垃圾对象多、存活对象少的前提下,常发生于新生代。
而对于老年代来说,情况恰好相反,垃圾对象少、存活对象多。
思想:
在标记清除算法的基础上进行优化。在标记可达对象之后,将可达对象压缩到内存的一端,然后再清楚边界外的空间。
优点:
- 无碎片。
- 无空间浪费。
1.5 分代算法(Generational Collecting)
复制、标记清除、标记压缩等垃圾回收算法,没有一种可以完全替代另一种,适用于不同场景。
思想:
将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的垃圾回收算法,提高垃圾回收的效率。
实现:
此外,新生代的回收频率较高,每次回收的耗时较短;老年代的回收频率较低,但会消耗更多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表(Card Table)的数据结构。
Card Table为一个比特集合,每一个比特位代表一个老年代区域中的对象是否持有新生代对象的引用。只有标记为1的才有必要扫描。这样减少了扫描老年代对象的数量,加快新生代的回收速度。
1.6 分区算法(Region)
分代算法将堆空间按照对象的生命周期长短划分为两个部分,分区算法将整个堆空间划分成连续的不同小区间。每个小区间独立使用,独立回收。
好处是可以控制以此回收多少个小区间。
2 判断可触及性
2.1 可触及性
垃圾回收的思想是考察每一个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以表明这个对象正在被使用,如果不可以,则说明已经不再使用。
但有一个例外,对象可能复活。因此可将可触及性的状态分为以下3种:
- 可触及的:从根节点开始可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但对象可能在finalize()函数中复活。
- 不可触及的:对象的finalize()函数被调用,并且没有复活,那么进入不可触及的状态。
对象只有在不可触及时,才可以被回收。
2.2 引用和可触及性的强弱
在Java中提供了4个级别的引用类型:
- 强引用
- 软引用
- 弱引用
- 虚引用
2.2.1 强引用
强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。
相对而言,软引用、弱引用、虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下都是可回收的。
特点:
- 强引用可以直接访问到对象。
- 强引用所指的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会强制回收强引用所指的对象。
- 强引用可能导致内存泄漏。
2.2.2 软引用
软引用是比强引用弱一点的引用类型。一个对象只持有软引用,那么当堆空间不足时,就会被回收。
特点:
- 软引用不会造成内存溢出。
2.2.3 弱引用
弱引用是一种比软引用更弱的引用类型。在GC时,只要发现是弱引用,不管系统堆空间是否紧张,都会将该对象回收。
但是由于垃圾回收器的线程通常优先级很低,因此并不一定能很快地发现弱引用。
应用场景:
软引用、弱引用都适合保存那些可有可无的缓存数据,堆空间紧张时回收,堆空间充足时存在时间较长,起到加速系统的作用。
2.2.4 虚引用——对象回收跟踪
虚引用是所有引用类型中最弱的一种。一个持有虚引用的对象,和没有引用几乎是一样的,随时可能会被垃圾回收器回收。
当垃圾回收器准备回收一个对象时,如果发现对象还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
3 垃圾收集器
3.1 串行回收器
串行回收器是所有垃圾回收器中最古老的一种,也是最基本的一种,特点如下:
- 使用单线程进行垃圾回收。
- 独占式垃圾回收。
存在Stop-The-World现象。
3.1.1 新生代串行回收器
虚拟机在Client模式下的默认垃圾回收器,使用复制算法。
3.1.2 老年代串行回收器
使用标记压缩算法,回收时间比新生代要长,可作为CMS回收器的备用回收器。
3.2 并行回收器
在串行回收器的基础上进行改进,使用多个线程同时进行垃圾回收。
3.2.1 新生代ParNew回收器
ParNew回收器是一个工作在新生代的垃圾回收器。知识简单地将串行回收器多线程化,回收策略、算法(复制算法)以及参数和新生代串行回收器是一样的。
3.2.2 新生代ParallelGC回收器
新生代ParallelGC回收器是使用复制算法的回收器,特点是关注系统的吞吐量。
ParallelGC回收器和ParNew回收器的另一个不同是它还支持一种自适应的GC调节策略,可以动态调节新生代的大小、eden和survivor的比例、老年代对象的年龄参数,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
3.2.3 老年代ParallelOldGC回收器
ParallelOldGC回收器也是一种多线程并发的回收器,和ParallelGC回收器一样关注吞吐量。
3.3 CMS回收器
CMS回收器与ParallelGC回收器和ParallelOldGC回收器不同,CMS回收器主要关注系统停顿时间。CMS是Concurrent Mark Sweep的缩写,以为并发标记清除。从名字上可得知使用的是标记清除算法。
CMS的主要工作过程有:
- 初始标记:标记根对象
- 并发标记:标记所有对象
- 预清理:清理前准备和控制停顿时间
- 重新标记:修正并发标记数据
- 并发清理:清理垃圾
- 并发重置
其中,初始标记和重新标记是独占系统资源的,而并发标记、预清理、并发清理和并发重置是可以和用户线程一起执行的。
CMS在回收过程中,用户线程也在工作,此时会有新的垃圾产生。因此CMS不是在堆饱和后再进行垃圾回收,而是在达到一定阈值时便开始进行回收,默认是在老年代的空间使用率达到68%,会进行一次CMS回收。
CMS使用标记清除算法,因此会产生碎片。此时堆内存可能仍有很大的剩余空间,但也可能会为了分配一个连续内存而被迫进行一次垃圾回收。为解决这个问题,CMS回收器还提供了几个用于内存压缩整理的参数,例如指定进行多少次CMS回收后进行一次内存压缩。
小结: CMS回收器是一个关注停顿的垃圾回收器,CMS回收器的部分工作流程可以和用户程序用时进行,从而降低应用程序的停顿时间。
3.4 G1回收器
3.4.1 概述
G1回收器(Garbage-First)是在JDK1.7中使用的全新的垃圾回收器,从长期来看是为了取代CMS回收器。
从分代上看,G1回收器依然属于分代垃圾回收器。
但从堆结构上来看,它并不要求整个eden区、年轻代或者老年代都连续,使用了分区算法的思想。
特点:
- 并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说不会在整个回收期间完全阻塞应用程序。
- 分代GC:G1依然是一个分代回收器,但兼顾新生代和老年代。
- 空间整理:G1在回收过程中,会进行适当的对象移动,每次回收都会有效地复制对象。
- 可预见性:由于分区的原因,G1可以只对部分区域进行内存回收,缩小了范围,对全局停顿能有更好的控制。
3.4.2 工作过程
G1收集器将堆进行划分,划分为一个个的区域,每次进行垃圾回收时,只收集其中几个区域,以此来控制垃圾回收产生的一次停顿的时间。
G1的收集过程可能有4个阶段:
- 新生代GC
- 并发标记周期
- 混合收集
- 如果有需要,可能会进行Full GC
新生代GC
新生代GC的主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。
并发标记周期
G1的并发阶段和CMS类似,都是为了降低一次停顿时间。可以分为以下几个步骤:
混合回收
在并发标记周期中虽然有部分对象被回收,但总体上来说回收比例是较低的。但是在并发标记周期后,G1已经明确知道那些区域含有较多的垃圾对象,在混合回收阶段就可以专门针对这些区域进行回收。此时G1会优先回收垃圾比例较高的区域,这也是G1全称Garbage First Garbage Collector的由来。
这个阶段交混合回收,是因为既会执行正常的年轻代回收,又会选取一些被标记的老年代区域进行回收。
G1收集的整体过程可能如下图所示:
Full GC
和CMS类似,并发收集由于让应用程序和GC线程交替工作,因此不能避免在特别繁忙的场合会出现内存不充足的情。此时G1也会转入一个Full GC的阶段。
4 其他细节
4.1 对象什么时候进入老年代
在堆中分配的对象首先会进入Eden区,如果没有GC则不会离开。那么对象何时会进入老年代,情况如下:
- 老年对象进入老年代:达到一定GC次数,晋升至老年代。
- 大对象进入老年代:新生代无论Eden还是Survivor都无法容纳该对象。
4.2 栈上分配和TLAB
4.3 GC Root对象有哪些
- 虚拟机(JVM)栈中引用对象
- 方法区中的类静态属性引用对象
- 方法区中常量引用的对象(final 的常量值)
- 本地方法栈JNI的引用对象
参考
主要参考《实战Java虚拟机——JVM故障诊断与性能优化 》