JVM垃圾回收与调优详解
1、JVM内存分配与回收
1.1 对象有限在Eden区分配
大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够空间分配是,虚拟机会发起一次Minor GC。
新生代GC (Minor GC):指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC经常伴随至少一次Minor GC(并非绝对),Major GC的速度一般会比Minor GC 慢10倍以上。
分配担保机制:当大对象分配到Eden区,Eden区存储不下时,会直接分配到老年代中。其中是通过分配担保机制进行的,参考文献:https://cloud.tencent.com/developer/article/1082730
1.2 大对象直接进入老年代
大对象就是需要大量的连续存储空间的对象,这里的连续的存储空间,指的是逻辑上连续的空间,而非物理上连续的存储空间,当Eden区没有足够的空间存储下,该对象是,对象会直接进入到老年代;为什么会这样做呢?主要是为了避免大对象分配内存时由于分配担保机制带来的赋值而降低效率。
1.3 长期存活的对象进入老年代
java虚拟机采用分代收集的思想来管理内存,当内存回收时应该能够清楚的判断哪些对象应该放入到新生代,哪些对象应该放在老年代,为了做到这一点,虚拟机给每个对象一个对象年龄计数器。
如果对象在Eden区,经历第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor中每熬过一次MinorGC年龄就会增加1岁,当他的年龄增加到一定程度(默认是15岁),就会被晋升到老年代中,对象晋升到老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold来设置。
2、如何判断对象可以被回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断这些对象是否还活着,如果已经死亡,则垃圾回收期需要考虑是否要将这些死亡的对象干掉!
2.1、引用计数法
给每个对象都增加一个引用计数器,当有引用指向他时,则引用计数器的值就会+1,当指向该对象的引用断开时,引用计数器的值就会-1。当引用计数器的值为0时,表示没有引用指向该对象,则java垃圾回收期将会考虑将她干掉。
但是引用计数法有一个明显的缺点,就是不能解决循环引用的问题;向下方的循环引用,将会逃过垃圾回收器的回收;
package com.itmuch.cloud; public class ReferenceCountingGC { private Object instance; public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; } }
2.2、根部可达性分析算法
由“GC Roots”作为对象的起点,从这个节点开始向下检索,节点所走过的路径称之为因用力按,当一个对象的GC Roots没有任何引用链相连接的话,则证明此对象是不可用的。
GC Roots 根节点:类加载器,Thread,虚拟机的本地变量表,static 成员,常量引用,本地方法栈的变量等等。

2.3 finalize()方法最终判定对象是否存活
即使可达性分析算法中不可达的对象,也并不是会被垃圾回收器马上回收,只是有可能会被回收
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用连。
2.3.1 第一次标记并进行一次筛选
筛选的条件是判断此对象是否有必要执行finalize()方法
当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”,对象被回收;
2.3.2 第二次标记
如果这个对象被判定为有必要执行finalize方法,这个对象就会被放在F-Queue队列中,并在稍后的一条虚拟机自动建立的,低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会出发这个方法,但并不承诺会等待他运行结束,这样做的原因是,如果一个对象finalize方法中执行缓慢,或者发生死循环(更极端的情况)。将很可能导致F-Queue队列中的其他对象永久处于等待状态,最终导致整个内存系统崩溃,finalize方法是对象逃逸的最后一次机会,如果finalize方法中,重新给将要回收的对象建立引用,那么该对象将会跳过垃圾回收器的回收;演示示例如下:
package com.current.demo; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * 垃圾回收实例 * -Xms10M 设置最大对大小 * -Xmx10M 设置初始堆大小 * -XX:+PrintGCDetails 打印垃圾回收情况 */ class User{ private int age; private String uuid; public User(int age,String uuid) { this.age = age; this.uuid = uuid; } @Override public String toString() { return "User [age=" + age + ", uuid=" + uuid + "]"; } @Override protected void finalize() throws Throwable { System.out.println(this.toString()); super.finalize(); } } public class OOMTest { public static void main(String[] args) throws InterruptedException { List<User> list = new ArrayList<>(); int i=0,j=0; while(true) { Thread.sleep(10); list.add(new User(i++, UUID.randomUUID().toString())); new User(j--, UUID.randomUUID().toString()); } } }
垃圾回收镜像:

由此可以总结:没有引用的对象,在垃圾回收是,会被垃圾回收期干掉。
2.4 如何判断一个常量是废弃常量
运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
如果常量池中存在字符串“hello word”,如果当前没有任何String的对象引用指向他,那么可以判定该对象是废弃常量,在随后的垃圾回收的过程中,将会被回收掉;
2.5 如何判断一个类是无用的类
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader也被垃圾回收器回收了
- 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
虚拟机可以对满足以上3个条件的无用类进行回收,但不是绝对的,只是有可能会被回收掉。
3、垃圾回收算法
3.1 标记-清除算法
分两个阶段:
- 标记阶段:标记处垃圾回收器需要回收的对象
- 清除阶段:垃圾回收器收集掉标记过得对象
存在问题:
- 效率问题
- 空间碎片
3.2 复制算法
内存空间分成两个大小相同的凉快,每次只是用其中一块,当一块使用完后,就把还存活的对象复制到另一块去,此时将该内存区域全部回收掉;然后反复进行,回想一下,堆空间中survivor区域,是多么适合该算法;

3.3 标记-整理算法
根据老年代的特点推出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但是后续的步骤不是直接回收可回收的对象,而是让所有存活的对象想一段移动,然后直接清理掉边界以外的内存;

3.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块,一般将Java堆分为新生代,老年代,这样我们就可以根据各个年代的特点选择适合的垃圾收集算法。
比如新生代中,每次收集都会有大量的对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率比较高,而且没有额外的空间对他进行分配担保,所以我们必须选择“标记-清除”或者“标记-整理”算法进行垃圾收集;
4、垃圾回收器
收集算法是理论基础,垃圾回收器才是具体实现。
4.1 Serial 收集器
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World"),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法。

虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
4.2 ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
l 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适合科学计算、后台处理等弱交互场景。
l 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。适合Web应用。
4.3 Parallel Scavenge收集器
Parallel Scavenge 收集器类似于ParNew 收集器,是Server 模式(内存大于2G,2个cpu)下的默认收集器,那么它有什么特别之处呢?
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。

4.4 Serial Old 收集器
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
4.5 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
l 初始标记: 暂停所有的其他线程(STW),并记录下直接与root相连的对象,速度很快 ;
l 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
l 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
l 并发清除: 开启用户线程,同时GC线程开始对未标记的区域做清扫。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
l 对CPU资源敏感(会和服务抢资源);
l 无法处理浮动垃圾(在java业务程序线程与垃圾收集线程并发执行过程中又产生的垃圾,这种浮动垃圾只能等到下一次gc再清理了);
l 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
CMS的相关参数
- -XX:+UseConcMarkSweepGC 启用cms
- -XX:ConcGCThreads:并发的GC线程数(并非STW时间,而是和服务一起执行的线程数)
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次(因压缩非常的消耗时间,所以不能每次FullGC都做)
- -XX:CMSInitiatingOccupancyFraction:触发FulGC条件(默认是92)
- -XX:+UseCMSInitiatingOccupancyOnly:是否动态调节
- -XX:+CMSScavengeBeforeRemark:FullGC之前先做YGC(一般这个参数是打开的)
- -XX:+CMSClassUnloadingEnabled:启用回收Perm区(jdk1.7及以前)
4.7 G1 收集器
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.


G1将Java堆划分为多个大小相等的独立区域(Region),虽保留新生代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
分配大对象(直接进Humongous区,专门存放短期巨型对象,不用直接进老年代,避免Full GC的大量开销)不会因为无法找到连续空间而提前触发下一次GC。
被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点:
l 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
l 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
l 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
l 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内完成垃圾收集。
G1收集器的运作大致分为以下几个步骤:
l 初始标记(initial mark,STW):在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
l 并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。
l 最终标记(Remark,STW):该阶段是 STW 回收,帮助完成标记周期。
l 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率。
G1垃圾收集分类
YoungGC
- 新对象进入Eden区
- 存活对象拷贝到Survivor区
- 存活时间达到年龄阈值时,对象晋升到Old区
MixedGC
- 不是FullGC,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)
- global concurrent marking (全局并发标记)
- Initial marking phase:标记GC Root,STW
- Root region scanning phase:标记存活Region
- Concurrent marking phase:标记存活的对象
- Remark phase :重新标记,STW
- Cleanup phase:部分STW
- 相关参数
- G1MixedGCLiveThresholdPercent Old区的region被回收的时候的存活对象占比
- G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数
- G1OldCSetRegionThresholdPercent 一次Mixed GC中能被选入CSet的最多old区的region数量
- 触发的时机
- InitiatingHeapOccupancyPercent:堆占有率达到这个值则触发global concurrent marking,默认45%
- G1HeapWastePercent:在global concurrent marking结束之后,可以知道区有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到了此参数,只有达到了,下次才会发生Mixed GC
5、如何选择垃圾收集器
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
- 如果允许停顿时间超过1秒,选择并行或者JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
下图有连线的可以搭配使用,官方推荐使用G1,因为性能高

浙公网安备 33010602011771号