三、如何优化垃圾回收机制?
一.垃圾回收机制
在 Java 开发中,开发人员是无需过度关注对象的回收与释放的,JVM 的垃圾回收机制可以减轻不少工作量。但完全交由 JVM 回收对象,也会增加回收性能的不确定性。在一些特殊的业务场景下,不合适的垃圾回收算法以及策略,都有可能导致系统性能下降。
面对不同的业务场景,垃圾回收的调优策略也不一样。例如,在对内存要求苛刻的情况下,需要提高对象的回收效率;在 CPU 使用率高的情况下,需要降低高并发时垃圾回收的频率。
1、回收发生在哪里?
Jvm运行时空间包含:堆、栈(方法区,元空间)、PC寄存器、本地方法栈、虚拟机栈。
其中PC寄存器、本地方法栈、虚拟机栈三个区域都是线程私有的,随着线程的创建而创建,死亡而死亡。栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。
因此垃圾回收的重点就在于堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。
2、对象在什么时候可以被回收?
一般一个对象不再被引用,就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收。
1.引用计数算法
通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。
2.可达性分析算法(JDK8中HotSpot)
GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。
3.可回收对象的类型
3、如何回收这些对象?
JVM 垃圾回收遵循以下两个特性。
1.自动性
Java 提供了一个系统级的线程来跟踪每一块分配出去的内存空间,当 JVM 处于空闲循环时,垃圾收集器线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空闲的内存块。
2.不可预期性
一旦一个对象没有被引用了,该对象是否立刻被回收呢?答案是不可预期的。我们很难确定一个没有被引用的对象是不是会被立刻回收掉,因为有可能当程序结束后,这个对象仍在内存中。
垃圾回收线程在 JVM 中是自动执行的,Java 程序无法强制执行。唯一能做的就是通过调用 System.gc 方法来"建议"执行垃圾收集器,但是否可执行,什么时候执行?仍然不可预期。
二.GC算法
1、常用的回收算法类型(没有最好的算法,只有最合适的算法,根据区域的不同)
回收算法类型 | 优点 | 缺点 |
---|---|---|
标记清除算法(Mark-Sweep) | 不需要移动对象,简单有效 | 标记清除过程效率低,GC会产生内存碎片 |
复制算法(Copying) | 简单有效,不会产生内存碎片 | 内存使用率低,可能产生频繁复制问题 |
标记整理算法(Mark-Compact) | 综合了前两种算法的优点 | 仍需要移动局部对象 |
分代收集算法(Generational Collection) | 分区回收 | 对于长时间存活对象的场景回收效果不明显,甚至会产生反作用 |
1.标记清除算法(Mark-Sweep)
标记清除算法是非常基础和常见的垃圾回收算法,当堆中的有效空间快被耗尽的时候,就会停止整个程序,然后进行标记和清除。
- 标记:从引用的根节点开始遍历,标记所有被引用的对象,标记的方法是在对象的Header中记录为可达对象。
- 清除:对堆内存从头到尾的进行遍历,如果发现某个对象在Header中没有被标记为可达对象,则将其回收。
- 缺点:效率不高,因为是从头到尾的遍历标记清除,所以被清除的对象不是连续的,会产生内存碎片。在进行GC的时候,会停止整个应用程序导致用户体验极差。
2.复制算法(应用于堆空间新生代FORM & TO)
复制算法的核心思想是,将内存空间分为两块,每次只用其中的一块,在垃圾回收的时候将正在使用的内存中的存活对象复制到未被使用的内存块中,之后再清楚正在使用的内存块中的所有对象,进行角色交换,完成垃圾回收。
- 优点:没有清楚和标记的过程,实现简单,运行高效。
- 缺点:缺点也很明显,需要更大的内存空间,复制也需要一定的开销。
这种算法应用于堆空间新生代FORM & TO,一次可以回收70%-99%的内存空间,回收性价比很高。
3.标记-压缩算法(应用于堆空间老年代)
标记压缩算颁发的高效性是建立在存活对象少,垃圾对象多的前提下。
第一阶段:标记,和标记清除算法一样。
第二阶段:将所有存活的对象压缩到内存的一端,按序排放,之后清理边界外的所有空间。
标记压缩的本质是:标记-复制-压缩-清除,与标记清除最大的区别就在于是否压缩,因为压缩所以不会有内存碎片。
4.分代收集算法(Generational Collection,一个综合的名称)
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
2、常用的回收器类型
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
回收器类型 | 回收算法 | 特点 |
---|---|---|
Serial New/Serial Old回收器 | 复制算法/标记整理算法 | 单线程复制回收,简单高效,但是回收时候会暂停导致程序卡顿 |
ParNew New/ParNew Old回收器 | 复制算法/标记整理算法 | 多线程复制回收,降低了停顿时间,但是容易增加上下文切换 |
Parallel Scavenge回收器 | 复制算法 | 并行回收器,追求高吞吐量,高效利用CPU |
CMS回收器 | 标记清理算法 | 老年代回收器,高并发、低停顿,追求最短GC回收停顿时间,CPU占用比例较高,响应时间快、停顿时间短 |
G1回收器 | 标记整理+复制算法 | 高并发、低停顿、可预测停顿时间 |
1.Serial New/Serial Old回收器
最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程。Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老生代的收集器,采用Mark-Compact算法。优点是简单高效,缺点是需要暂停用户线程。
2.ParNew New/ParNew Old回收器
Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。
3.Parallel Scavenge回收器
新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。
4.CMS回收器
Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。
5.G1回收器
G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。
3、GC性能衡量指标(在最大吞吐量优先的情况下,降低停顿时间)
1.吞吐量
吞吐量是指应用程序所花费的时间和系统总运行时间的比值,GC 的吞吐量:系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。
2.停顿时间
指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
3.垃圾回收频率
多久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。
三.查看 & 分析 GC 日志
1、查看日志
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
2、分析日志
四.GC调优策略
1、降低 Minor GC 频率
通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此可以通过增大新生代空间来降低 Minor GC 的频率。
2、降低 Full GC 的频率
通常情况下,由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销
1.减少创建大对象(或者将大的对象进行拆解成不同的类)
超大的对象出房间出来,如果超过年轻代最大对象阈值,会直接被创建在老年代,即使被创建在了年轻代,也会因为年ing带空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。
2.适当增大堆内存空间
过大的堆内存虽然会降低GC的频率,但是会增长GC的时间。
在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。选择合适的 GC 回收器
3、选择合适的 GC 回收器
垃圾收集器的种类很多,可以将其分成两种类型,一种是响应速度快,一种是吞吐量高。通常情况下,CMS 和 G1 回收器的响应速度快,Parallel Scavenge 回收器的吞吐量高。
4、改动Jvm垃圾回收器一定要进行大量的测试
通常情况,JVM 是默认垃圾回收优化的,在没有性能衡量标准的前提下,尽量避免修改 GC 的一些性能配置参数。如果一定要改,那就必须基于大量的测试结果或线上的具体性能来进行调整。