垃圾收集算法主要分为两类,分别是引用计数算法和可达性分析算法。引用计数算法原理简单、回收效率高,但无法解决循环引用的问题。可达性分析算法是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不活跃的,可以被回收。
JVM的垃圾回收采用了可达性分析算法。
JVM垃圾回收主要回收区域为Java堆和方法区,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,不需要专门进行回收。
当前商业虚拟机的垃圾收集器,大多都遵循“分代收集”理论。它建立在两个分代假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的;
- 强分代假说:熬过越多次垃圾收集过程的对象越难以消亡。
根据这两个分代假说,垃圾收集器将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域中存储。现代垃圾收集器至少会将Java堆划分为新生代(Yong Generation)和老年代(Old Generation)两个区域。于是就有了“Minor GC”、“Major GC”、“Full GC”这样的回收类型的划分,并对不同区域对象的特点采用了不同的垃圾收集算法:“标记-复制算法”、“标记-清除算法”、“标记-整理算法”。
- 标记-清除算法:标记出所有存活的对象,标记完成后,统一回收掉所有未被标记的对象。它的执行效率不稳定,如果有大量需要回收的对象,需要进行大量标记和清除,同时会导致内存空间的碎片化。主要用于老年代的垃圾回收,由于不需要移动对象,垃圾回收延迟少,主要使用在CMS垃圾回收器上。
- 标记-复制算法:也称为半区复制,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完,就将存活对象复制到另外一块上面,然后将已使用过的内存空间一次清理掉。如果内存中存活对象较多,会产生大量内存复制开销,内存使用率只有原来的一半。新生代存活对象少,通过将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次回收时将Eden区的对象和Survivor区的存活对象复制到另一块Survivor区域,可以减少空间的浪费,同时需要复制的对象也较少。如果Survivor区域的空间较少,不足以存放存活的对象,将通过分配担保将对象直接复制到老年代。
- 标记—整理算法:将对象移动到内存空间的一端,然后清除掉边界以外的内存。内存整理可以提升对象分配效率,在关注吞吐的Parallel Scavenge收集器上使用。
垃圾收集器有几个实现细节,主要是根节点枚举、安全点、安全区域、记忆集和卡表、写屏障、并发的可达性分析(三色标记)等。
G1收集器
开创了收集器面向局部收集的设计思路和基于Regin的内存布局形式。它的设计目标不再是追求一次把整个Java堆全部清理干静,而是垃圾回收的速度能应付应用内存分配速率。它建立起一套可预测的停顿时间模型,能够支持在指定的一个长度M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。在延迟可控的情况下获得尽可能高的吞吐量,适用于多核处理器、大内存容量的系统。
G1不再是回收整个新生代或老年代,而是面向堆内存任何部分组成回收集(CSet)来进行回收,衡量标准是哪块内存中存放的垃圾数量最多,回收收益最大,优先处理收集收益最大的Regin。
G1将连续的Java堆划分为多个大小相等的独立Regin区域,每个Regin都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。
- 新生代收集是指对全部新生代分区进行垃圾回收;
- 混合收集不仅仅回收新生代分区,也回收部分老年代分区,通常发生在并发标记之后;
- Full GC指内存不足时需要对全部内存进行垃圾回收。
G1垃圾回收不同于其它垃圾回收器的地方在于:
- G1会根据预测时间动态改变新生代的大小;
- G1老年代的收集不会为了释放老年代空间对整个老年代做回收。相反,在任意时刻只有一部分老年代分区会被回收,并且这部分老年代分区将在下一次增量回收时与所有新生代分区一起被收集,这就是所说的混合回收。
G1的垃圾收集过程可分为四个步骤:
- 初始标记:仅标记GC Root能直接关联的对象。这个阶段需要停顿用户线程,但耗时很短。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,耗时较长,但可以与用户线程并发执行。通过原始快照(SATB)记录下在并发时有引用变动的对象。
- 最终标记:有短暂的停顿,用于处理并发阶段仍遗留下来的少量SATB记录。
- 筛选回收:更新Regin的统计数据,对各个Regin的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划。将Regin中存活的对象复制到空的Regin中,再清理掉旧Regin的全部空间,需要暂停用户线程。
另外,G1垃圾收集器的内存占用和负载都比较高。
G1垃圾收集算法的重要参数配置:
- -Xms/-Xmx:指定堆空间的最小值/最大值。这个值一定要正确设置,否则会影响分区大小的推断;
- UseG1GC:使用G1收集器,是JDK9后的Server模式的默认值;
- G1HeapReginSize:指定堆分区的大小,上限为32M,下限为1M。可以不指定,由内存管理器启发式推断分区大小;
- MaxNewSize/NewSize/NewRatio/-Xmn:在G1中不要设置。第一,G1对内存的管理不是连续的,重新分配堆分区代价不高;第二,G1需要根据停顿时间动态调整收集的分区,如果设置了固定的分区数,G1不能调整新生代的大小,无法满足停顿时间的要求;
- GCTimeRatio:GC与应用程序之间的时间比,默认为9,表示GC与应用程序时间占比为10%。增大该值将减少GC占用的时间,会导致动态扩展内存更容易发生,可以将该值设置为19,表示GC时间不超过5%;
- MaxGCPauseMillis:指期望的停顿时间。需要配合新生代大小进行设置;
- UseTLAB:是否使用线程本地分配缓冲(即快速分配),默认打开。TLAB通过为每个线程分配一个缓冲区来避免和减少锁的竞争;
- G1ConcRefinementThreads:G1 Refine线程的个数,默认为0,由G1启发示推断。Refine线程用于处理分区间的引用,快速识别活跃对象;
- ParallelGCThreads:并行执行GC的线程个数,默认为0,由G1通过CPU的个数进行推断。
- MaxTenuringThreshold:晋升到老年代的对象年龄,默认值为15;
- SurvivorRatio:Eden和一个Survivor分区的比例,默认为8;
- InitiatingHeapOccupancyPercent(简称IHOP):默认为45,这是启动并发标记的先决条件,只有当老年代内存占总空间45%之后才会启动并发标记任务,增加该值,会导致并发标记可能花费更多时间,也会导致YGC或混合GC收集时分区变少;
- ConcGCThreads:并发线程数,默认为0,如果并发标记耗时较多可以增大该值,增大该值会导致Mutator执行的吞吐量变小。