JVM性能优化之垃圾回收机制
文章目录
前言
我们都知道JVM虚拟机有垃圾自动回收机制,这样一般水平的程序员就不需要去考虑内存问题,这时候虚拟机的内存对我们来说就像个黑匣子,但是如果我们想让自己的程序在某些环境中达到更高的性能就需要考虑垃圾回收问题了,本文对JVM的垃圾回收机制做了个简单的讲解。
一、垃圾回收机制
引用计数法
引用计数法就是判断一个对象是否被引用,如果没有被引用就视作垃圾。如图:给每一个对象一个引用计数器,每被引用一次+1,引用失效-1,引用计数器为0则是垃圾清理,红色为0的下次垃圾回收的就会被回收掉。
缺点:相互引用是无法回收的,如下:
Obj a = new Obj();
Obj b = new Obj();
a.next = b;
b.next = a
这种情况下即使我们把a和b都设置为空也是无法回收的,因为他们存在引用,计数器就不为0,引用计数法是无法被回收的。但是这种相互引用被虚拟机回收了,说明虚拟机没有采用这种回收算法。
可达性分析算法
可达性算法是以一系列的“GC Roots”为根起点向下搜索,搜索路径为引用链,如果一个节点没有任何引用链说明对象是垃圾,有引用链没有Root也是垃圾,如下图所示,假设外面的是 GC Root引用,其中只有一个节点不存在引用链和两个节点引用不存在GC Root的被红色标记了,这两个都是下次垃圾回收的时候要被收走的。
注意事项:即使不可达对象也不是直接回收的,类似于淘汰赛,需要经过两次标记后才能真正确认是否回收,第一次标记看对象是否有必要执行finalize()方法,没有必要的咱就不管了,有必要的会放到一个队列中,此时也不是立即回收的,这中间是有个过程的,第二次标记就在这个等待的过程中,如果在等待的过程中,这个对象建立了引用链就会被移除队列,剩下的就真的被回收了。
二、垃圾回收算法
标记清除法
标记算法就是遍历所有的可达对象做一个标记,在清除的时候再遍历一遍没有做标记的就被回收。如下图,绿色的是标记的,红色的是没被标记回收的。
缺点:回收时需要挂起虚拟机STW(stop the world),产生很多内存碎片,注意存东西是需要连续的空间的。
场景:一般应用于老年代,因为老年代的对象生命周期比较长。
标记复制法
标记复制算法就是把内存分成两块,from和to,每次回收时候清理from把可达对象放到to里面,把剩下的不可达对象清除掉,此时To变成了From,下一次垃圾回收的时候再来一次上面的操作。
缺点:标记复制法我们可以看到一个明显的缺点就是浪费内存,两块内存同时只有一块在使用,搭档可达对象多的时候,需要复制的对象较多耗时长,所以这种算法不会应用在老年代,用在了新生代的Survivor区中。
场景:新生代中的S1和S0区。
标记清除压缩法
在标记清除法的时候有一个明显的缺点就是剩余内存不连续,而标记复制算法浪费内存,所以又提出了标记清除压缩算法,压缩标记算法和清除标记算法在标记的时候原理是相同的,但是压缩标记算法最后会把可达对象放到堆空间的一端,这样剩下的空间就是连续的了。
缺点:因为移动之后对象的位置变了,这时候虚拟机需要重新建立引用关系。
场景:老年代中就使用了该算法。
垃圾收集器
上面介绍了多种算法,每种算法都有自己的特点,而我们的堆又分了多个部分,每个部分根据其存储数据的特性使用对应的算法这样就达到了最优。当然这只是理论,具体的垃圾回收操作是由垃圾收集器去实现的。
垃圾收集器分为串行收集器、并行收集器、CMS收集器和G1收集器四种。
根据其使用的位置是新生代还是老年代有又可以分为下面情况:
Serial串行收集器
JDK1.3的时候我们只有这种回收器,是单线程执行的,这里的单线程说明其在收集的时候其他线程是不允许进行的,所以会STW,但是在单线程的情况下其效率非常高,所以单线程服务器一般使用这个。可以通过 -XX:+UseSerialGC 来设置串行收集器。新生代中的串行收集采用标记复制算法,而老年代的串行收集采用标记清除压缩算法。
ParNew并行收集器
相较于串行收集器来说,两者的回收机制是相同的,但是并行收集器可以多线程执行。所以其在多核CPU的场景下效率比较高,现在我们的服务器基本都是多核的了。可以通过下面设置来设置服务器使用并行收集器。老年代的并行收集Parallel跟其原理一样。
-XX:+UseParNewGC 设置使用并行收集器
-XX:+UseParallelOldGC 老年代的并行收集
-XX:ParallelGCThreads 设置并行线程数量
Parallel Scavenge并行清除收集器
Parallel Scavenge收集器又称为大吞吐优先收集器,是JDK8默认的收集器。它是以吞吐量优先的,意思是它的吞吐量是可控的,并会自动调整新生代中Eden、Form、To之间的比例来达到吞吐量。其吞吐量是这样计算的:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),假设垃圾收集时间为1分钟,代码总时长为100分钟,吞吐量就是99%。
可以通过下面参数来调整:
-XX:+UseParallelGC 使用并行收集器
-XX:MaxGCPauseMillis 最大垃圾收集时间
-XX:GCTimeRatio 吞吐量的大小
XX:ParllGCThreads 新生代中的线程数
XX:+UseAdaptiveSizePolicy 自动调节新生代比例
CMS收集器
CMS(concurrent mark sweep)是以获取最短垃圾收集停顿时间为目标的收集器,CMS收集器的关注点尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短就越适合与用户交互的程序,目前很大一部分的java应用几种在互联网的B/S系统服务器上,这类应用尤其注重服务器的响应速度,系统停顿时间最短,给用户带来良好的体验,CMS收集器使用的算法是标记-清除算法实现的。
CMS收集器的过程分为初始标记、并发标记、重新标记和并发清除四步。
- 初始标记: 初始标记阶段主要对GC Root直接关联的对象进行标记,执行速度非常快,这个过程会触发STW机制。
- 并发标记: 这个标记过程是对关联对象后面的引用链进行标记,初始标记只标记了第一个,并发标记是标记后面的引用对象,这个过程是并发执行的不会执行STW,耗时较长。
- 重新标记: 因为并发标记过程是并发执行的,队列中的回收对象可能在标记过程中又被使用,所以重新对回收对象进行标记分析,同样会STW。
- 并发清除: 这个过程是用来清除未被标记的对象的,是多线程并发执行的,清除完成后释放对应内存。
三色标记法: 我们一般用三色标记法来判断对象的可达性,其中未被访问过的对象用白色标记;被访问过而且该对象后面被引用的那个对象也被访问了的对象采用黑色标记;被访问过但是后面引用的那个对象还没被访问的采用灰色标记。到结束过程中一直被标记未白色的对象就是不可达对象。
CMS收集器默认启动的回收线程数=(处理器核心数量 +3)/4,所以以四核为界限,当CPU的核心数量比四大时,并且越大回收占用的性能越低,当比四小时,越小占用的性能就越高。
G1收集器
G1收集器是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
G1收集器将内存划分为多个个区域,每个区域都是2的幂数大小,虽然也保留了新生代和老年代,但是他们没有隔离,看做一部分区域,每个区域中再根据需要划分代;G1收集器整体使用的标记压缩算法,每个区域中采用了复制算法;G1收集器会对区域进行优先级排序,根据时间优先回收来保证在有限时间内回收的高效性。
G1收集器提供Young GC和Mixed GC两种GC模式,两种模式都会出现STW。
Young GC: 选定所有年轻代里的区域。通过控制年轻代中区域的个数,即年轻代内存大小,来控制young GC的时间开销。
Mixed GC:选定所有年轻代里的区域,外加根据全局并发标记统计得出收集收益高的若干老年代区域。再用户指定的开销目标范围内尽可能选择收益高的老年代Region。
G1收集器同样经过四个阶段:初始标记、并发标记、最终标记和筛选回收四个阶段。G1收集器常用参数配置如下:
-XX:+UseG1GC: 使用 G1 垃圾收集器。
-XX:MaxGCPauseMillis=200: 设置期望达到的最大GC停顿时间指标。
-XX:InitiatingHeapOccupancyPercent=45: mixedGC中也有一个阈值参数 ,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixedGC. 默认值为 45。
-XX:NewRatio=n: 新生代与老生代的大小比例. 默认值为2。
-XX:SurvivorRatio=n: eden/survivor空间大小的比例. 默认值为 8。
-XX:MaxTenuringThreshold=n: 提升年老代的最大临界值. 默认值为 15。
-XX:ParallelGCThreads=n: 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平
台不同而不同。
-XX:ConcGCThreads=n:并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同。
-XX:G1ReservePercent=n :设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是10。
-XX:G1HeapRegionSize=n:使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指
定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb。