JVM性能调优-垃圾回收及算法
概述
引用计数法
对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1。引用计数为0 的对象可被回收。
对象相互引用时,很难判断对象是否该回收。
可达性分析(Java)
通过一系列的GC Roots对象作为起始点,往下搜索,走过的路径称之为搜索链(Reference chain),当一个对象到Root时没有任何引用链相连则证明该对象不可用。
作为Roots对象的有:
- 虚拟机栈中局部变量表引用的对象
- 本地方法栈中局部变量表引用的对象
- 类静态属性引用的对象
- 常量引用的对象
引用类型
强引用
Object test = new Object(),这类属于强引用。
(如果有GC Roots 的强引用)垃圾回收器绝对不会回收它,当内存不足时抛出OOM 错误,使得程序异常停止。
软引用
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
public static void main(String[] args) {
Node node = new Node(1);
SoftReference<Node> softReference = new SoftReference<>(node);
}
static class Node{
int id;
public Node(int id){
this.id = id;
}
}
弱引用
垃圾回收器无论内存充足与否,都会回收该对象的内存。
可以用来创建不是很重要的数据缓存。
实际运用(WeakHashMap、ThreadLocal)
虚引用
如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收,主要用来跟踪对象被垃圾回收器回收的活动。
Minor GC
发生在新生代,较频繁执行速度快
Eden空间不足,空间分配担保
Full GC
主要发生在老年代(新生代也会回收),较少执行速度慢
老年代空间不足
空间分配担保失败
垃圾回收算法
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使
用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,
实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
新生代中的对象98%是“朝生夕死”的,所以一般来说回收占据10%的空间够用了,并不需要按照1:1 的比例来划分内存空间,而是将内存分为一块较大的Eden 空间和两块较小的Survivor 空间,每次使用Eden 和其中一块Survivor[1]。当回收时,将Eden 和Survivor 中还存活着的对象一次性地复制到另外一块Survivor 空间上,最后清理掉Eden 和刚才用过的Survivor 空间。
Hot Spot 虚拟机默认Eden 和Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。
标记-清除(Mark-Sweep)
首先标记所有需要回收的对象,统一回收被标记的对象
缺点:
- 标记和清除效率都不高
- 清除之后会有大量不连续的内存碎片,会导致之后分配大对象时无法找到连续的内存空间而不得不触发另一次的GC
标记-整理(Mark-Compact)
首先标记所有需要回收的对象,之后让所有不被回收的对象向一端移动,然后清理掉这端的内存。
分代回收
根据各代的特点选择不同的算法:
- 新生代使用复制算法
- 老年代使用标记-清除或者标记-整理算法
jps -v显示当前使用的垃圾回收器
CMS
-XX:+UseConcMarkSweepGC
一般新生代使用ParNew,老年代的用CMS
基于“标记—清除”算法实现的
过程:
- 初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快,(STW)
- 并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 (STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
- 并发清除:不需要停顿
缺点:
- CPU资源:并发阶段多线程占用CPU资源,如果CPU不足效率会降低
- 浮动垃圾:并发清除过程中会产生垃圾,这部分垃圾需要在下一次GC中清除
- 产生垃圾碎片
G1
-XX:+UseG1GC
内存格局变化:把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
算法:标记-整理和复制算法
Young GC
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制Young GC 的时间开销。(复制回收算法)
Mixed GC
选定所有年轻代里的Region,再根据global concurrent marking 统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收
益高的老年代Region。
Mixed GC 不是full GC,它只能回收部分老年代的Region。如果mixed GC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以G1 是不提供full GC 的。
过程
- 初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region 中创建对象,此阶段需要停顿线程 (STW),但耗时很短。
- 并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的
Remembered Set Logs 里面,最终标记阶段需要把Remembered Set Logs 的数据合并到Remembered Set 中。这阶段需要 (STW),但是可并行执行。 - 筛选回收::首先对各个Region 中的回收价值和成本进行排序,根据用户所期望的GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
- 空间整合:不会产生垃圾碎片
- 可预测的停顿:G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1 跟踪各个Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。