我们知道之所以java比较容易上手,很大的原因是由于我们不需要关注对象的回收和释放,可以减少不少的工作量,但是完全交由虚拟机回收,也会带来回收性的不确定性。
面对不同的业务场景,垃圾回收的调优策略也是不一样的,例如在内存要求苛刻的情况下,需要提高回收策略,在CPU使用率高的情况下,需要降低高并发量时垃圾回收的频率。所以垃圾回收调优是一项必备技能
垃圾回收机制
首先,我们要弄明白三件事
回收发生在哪里
JVM内存模型中,程序计数器,虚拟机栈和本地方法栈这个三个区域是线程私有的,随着线程的创建而创建,销毁而销毁,栈中的帧栈随着方法的进入和退出进行入栈和出栈,每个帧栈中分配多少内存基本是类机构确定下来的时候就已经确定了,因此这三个区域内存的分配和回收是确定的
但是堆和方法区就是垃圾回收的重点,堆中回收的主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收
对象什么时候可以回收
JVM如何判断一个对象是否可以被回收,一般一个对象不再被引用,就代表这对象可以被回收,目前有两种方式判断对象时候被回收
引用计数器,这种算法是根据对象的引用计数器来判断对象是否被应用,每当对象被引用,引用计数器就会加1,当对象引用时效,引用计数器就减1,当引用计数器值为0时候,就说明该对象不在被引用,可以被回收,虽然此算法简单,但是无法解决循环引用的问题
可达性分析算法,GC Roots是该算法的基础,GC Roots是所有对象的根对象,在JVM加载时候,会创建一些普通对象引用正常对象,这些对象作为正常对象的起始点,在垃圾回收时候,从这个GC Roots开始向下搜索,当一个对象到GC Roots没有任何引用链相连时,就证明这个对象不可应,目前HotSpot虚拟机就是采用这个算法
在JDK1.2之后,java对引用的概念进行了扩展分为了四种
如何回收对象
了解完回收的条件,那么垃圾回收线程又是如何回收这些对象的,垃圾回收遵循下面两点特性
自动型,Java提供一个系统级的线程来跟踪每一块分配出去的内存,当JVM处于空闲循环时,垃圾收集器线程会自动检查每一块分配出去的内存空间,然后自动回收每一个空闲的内存块
不可预期性,一旦一个对象没有被引用了,该对象是否立刻被回收呢,答案是不可预期的,因为有可能程序结束后,这个对象扔在内存中。
垃圾收集线程在JVM中是自动执行的,java程序无法强制执行,我们唯一能做的就是调用system.gc方法来建议执行垃圾收集器,但是是否立刻执行,仍然是不可预期的
GC算法
回收算法 | 优点 | 缺点 |
---|---|---|
标记-清除 | 不需要移动对象,简单高效 | 效率低,GC产生内存碎片 |
复制 | 简单高效,不会产生内存碎片 | 内存使用率低,产生频繁的复制问题 |
标记-整理 | 结合上面两种算法优点 | 仍需移动局部对象 |
分代收集算法 | 分区会后 | 对于长时间存活对象的场景的回收效果不明显,甚至起到分作用 |
垃圾收集器就是内存回收的具体实现,下面就是具体的垃圾收集器
回收类型 | 回收算法 | 特点 |
---|---|---|
Serial New/Serial Old | 复制算法/标记-整理 | 单线程复制回收,简单高效,但会暂停程序导致停顿 |
ParNew New/ParNew Old | 复制算法/标记整理 | 多线程复制回收,降低停顿时间,但容易增加上下切换 |
Parallel Scavenge | 复制算法 | 并行回收期,追求高吞吐量,高效利用CPU |
CMS | 标记-清除 | 老年代回收期,高并发,低停顿,追求最短GC回收停顿时间,CPU占用比较高,响应时间快,停顿耗时间短 |
G1 | 标记-整理+复制算法 | 高并发,低停顿,可预测停顿时间 |
GC性能衡量指标
吞吐量:这里吞吐量是指应用程序所花费的时间和系统总运行时间比值,系统总运行时间=应用程序耗时+GC耗时,比如系统运行了100分钟,GC耗时1分钟,吞吐量就是99%,一般吞吐量一般不低于95%
停顿时间:指垃圾收集器正在运行时,应用程序暂停时间,对于串行回收期而言,停顿时间较长,并行回收期,停顿时间较短。但是效率很可能不如串行垃圾收集器,系统的吞吐量也可能降低
垃圾回收效率:通常垃圾回收的频率越低越好,增大对内存空间可以有效降低垃圾回收发生的频率,但是同时意味着堆积的回收对象越多,最终会增加回收时的停顿时间,所以我们只要适当的增大堆内存空间,保证正常的垃圾回收频率即可
GC调用策略
降低Minor GC频率
通常情况下,由于新生代空间较小,Eden区很快填满,就会导致频繁Minor GC,因此增加新生代空间来降低Minor GC的频率
此时我们就有了疑问,扩容Eden区虽然可以降低Minor GC,但是是不是增加了单次Minor GC.
我们知道,单次Minor GC时间是有两部分组成T1(扫描新生代)T2(复制存活对象),假设一个对象的存活对象为500ms,Minor GC的时间间隔是300ms,那么正常情况下,Minor GC时间T1+T2
而当我们增大新生代空间,Minor GC时间间隔会扩大到600ms,此时一个存活的随想就会在Eden会被回收,此时就不存在复制存在对象,所以在发生Minor gc时间就是两次扫描新生代即2T1
可见,扩容后,Minor GC即增加了T1但省去了T2的时间,通常复制对象的成本要远高于扫描成本。
如果在堆内存中存在较多的长期存活的对象,我们扩大新生代的空间,反而会增加Minor GC的时间,如果堆中的短期对象很多,那么扩容新生代空间,单次Minor GC时间不会明显增加,因此单次Minor GC时间更多取决于GC后存活的对象数量,并非Eden区的大小
降低Full GC频率
由于堆内存空间不足或老年代对象太多,会频繁的发生Full GC,因此会带来上下文切换,增加系统的性能开销.我们可以使用下面方式降低Full GC的频率