目录
一、垃圾定位
1.引用计数法
2.可达性算法
二、垃圾回收算法
1.标记清除算法
2.复制算法
三、常见垃圾回收器及其组合
1.serial
2.parallel Scavenge(ps)
3.parnew
4.serial old
5.parallel old(po)
6. CMS
7.G1
五、GC日志分析
垃圾回收主要是发生在堆里面,在1.8以后FULLGC也会发生在meta space中。在上一篇内容中,堆可以分为新生代和老年代两部分,其中新生代发生的为YGC,老年代发生的为FULLGC。在G1之前,新生代和老年代既为逻辑分区又是物理分区,但是g1中的新生代和老年代只是逻辑分代,而到了ZGC已经去除了新生代和老年代的区分。本文主要对垃圾回收的相关内容进行一个介绍。
在进行垃圾回收之前,需要首先进行垃圾定位,知道哪些对象可以进行回收。那么什么样的对象算是可回收对象呢,指的是没有任何引用指向的对象。
针对可回收对象定位有以下两种算法:
在对象头中维护一个几个计数器,当有一个引用指向它以后counter++,当引用不再指向以后counter--,当counter为0的时候该对象就是可回收对象。但是引用计数法存在一个巨大的问题,就是循环依赖,例如:
针对上图这种情况,对象ABC之间相互引用,他们的counter永远不可能为0,造成他们永远无法被回收,因此目前引用计数算法在垃圾回收中基本不会使用。
先明确根节点,然后从根节点出发,所有可以通过根节点到达的对象都是有效对象,而其它不可达的对象都为可回收对象,这种算法可以解决上面的循环引用的问题。那么什么样的对象可以作为根节点呢?线程栈变量,静态变量,类的常量池(class文件中的常量池)指向的对象,JNI指针(调用C C++对象)这些对象为根节点。
上图大致描述了根可达算法的方式,其中ABC为有效对象,AB可以作为根对象,DEF为 可回收对象。
在进行根可达性分析的时候,是来判断哪些对象可以被回收,这里就涉及到了强软弱虚四种引用类型。
强引用:我们平常进行普通对象创建时,例如T t = new T(),这种就是强引用方式。只要对象存在这种强引用,就不会被垃圾回收。
软引用:软引用的创建方式如下:
SoftReference softReference = new SoftReference<>(new Integer[1]);
其中new Integer[1]就是软引用对象,针对软引用对象,当内存足够时,是不会被垃圾回收的,只有当内存不够时,才会被回收。软引用一般用作缓存信息。
弱引用:弱引用的创建方式如下:
WeakReference weakReference = new WeakReference<>(new Integer[1]);
其中new Integer[1]就是弱引用对象,针对弱引用对象,只要发生垃圾回收,就会被回收。一般用在容器中。ThreadLocal的实现中就应用到了WeakReference,在调用ThreadLocal的set(Object)方法的时候,其实就是往thread中的threadLocals中存放信息,而Thread中的threadLocals是ThreadLocal.ThreadLocalMap,而这个Map中的Entry就是继承自WeakReference。ThreadLocal的详细信息将会在后续并发编程的文章中做详细介绍。详见以下代码:
// ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread类中的threadLocals对象
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocal.ThreadLocalMap的map对象
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap中的key是通过一个弱引用指向对象。
虚引用:虚引用的创建方式如下:
PhantomReference phantomReference =
new PhantomReference<>(new Integer[1], new ReferenceQueue<>());
虚引用的创建有一个很特殊的点,在创建需您用对象的时候,构造函数中需要传入一个ReferenceQueue。在我们的开发过程中很少用到虚引用对象,虚引用对象在垃圾回收的时候会马上被回收,但是在回收以后会将这个虚引用放入到创建虚引用时传入的那个队列中。虚引用用来管理对外内存,例如上文提到过的直接内存。
针对jvm中产生的垃圾,也就是可回收对象,回收他们的算法主要有以下三种:
会进行两次扫描,第一次扫描为扫描所有有效对象,第二次扫描为找到垃圾对象然后进行清除。这种算法存在一个缺点,就是空间碎片,会造成内存中产生许多的不连续存储空间。
假如上面为一块内存空间,绿色的方框表示为垃圾,那么在回收阶段就会将上图中的绿色方框进行回收,如果此时来了一个较大的对象,相当于是两个绿色方框的大小,在上图的内存空间中就无法进行存储了。这种回收算法适用于存活对象较多的情况。
在内存空间中最少准备两个空间,每次使用其中的一个空间,回收的过程,根据根可达算法定位有效对象,并且将这些对象复制并移动到新的一个空间中,等复制移动完毕以后,将刚刚使用的空间进行整体回收。
其中图1是垃圾回收之前,存在上下两个内存空间,首先使用上面的内存空间,绿色为有效对象,然后在进行垃圾回收的时候,将上面内存空间的绿色对象复制移动到下方的内存空间中,然后将上方内存空间中的所有对象进行回收,就变成了图2回收后的样子。通过上面的图示就可以看到使用这种算法会造成空间浪费,并且会造成对象引用的调整,因此这种算法适合于存活对象较少的场景。
3.标记压缩算法
这种算法相当于是标记删除算法的升级版,也是需要进行两次扫描,第一次扫描的时候扫描出有效对象并且需要进行对象移动,然后第二次扫描的时候,进行垃圾对象的回收。
图1为垃圾回收之前的情况,其中绿色块为有效对象,在垃圾回收第一次扫描的时候,扫描到有效对象,然后将有效对象和垃圾对象进行位置替换,等扫描完有效对象以后,再将白色块无效对象进行回收,这样整个内存空间就都是连续的,不会出现空间碎片,但是由于进行两次扫描并进行了对象移动,效率较慢。
单线程垃圾回收器,新生代,采用的垃圾回收算法是复制算法。jvm的产生以后的第一代垃圾回收器。适用于单cpu并且内存较小的机器,如果内存过大,那么stw(stop the world)的时间就会很长。serial垃圾回收器的工作线程如下图:
工作线程指的就是当前程序运行的业务代码,在业务代码运行时并不是可以随时进行垃圾回收,需要先到达safe point这个点以后,才能进行垃圾回收,在垃圾回收的时候需要stw,即工作线程需要暂时进行停止,等垃圾回收线程完成垃圾回收以后,工作线程才能恢复运行。
多线程垃圾回收器,新生代,采用的垃圾回收算法为复制算法,工作流程如下图:
它和serial的区别主要就是在垃圾回收的时候采用多线程的处理方式。1.8版本默认的新生代垃圾回收器。
多线程垃圾回收器,新生代,使用复制垃圾回收算法,类似于ps,但是它为了适配cms做了相关的调整,工作流程同ps。
单线程垃圾回收器,老年代,采用的垃圾回收算法是标记整理或者标记删除算法,他的工作流程同serial。
多线程垃圾回收器,老年代,使用标记整理算法,工作流程同ps。
多线程垃圾回收器,老年代,使用标记清楚垃圾回收算法,他开创了工作线程和回收线程同时运行的先例,但是由于自身问题原因,他没有被作为过默认的垃圾回收器。他的工作流程如下:
上图中黑色箭头代表工作线程,绿色箭头代表垃圾回收线程,其中初始标记需要stw,用来标记根对象;并发标记,大部分的对象标记是在该阶段完成;重新标记,需要进行stw;并发清除,与工作线程同时运行,进行垃圾清除。cms的这种设计虽然减少了stw的时间,但是在垃圾回收的过程中会同时产生垃圾,那么就会产生一种情况,当回收的垃圾量低于新产生的垃圾的时候,这时候会产生浮动垃圾,当浮动垃圾过多时,会触发serial old垃圾回收器进行FULLGC,由于现在内存都很大,serial old属于单线程垃圾回收器,并且在进行垃圾回收的时候会stw,可能会让应用程序停顿很长时间,这是cms垃圾回收器的一个很大的缺点。
由于CMS垃圾回收使用的是标记清除算法,那么就会产品碎片空间,这些碎片空间需要进行整理,是使用的serial old回收器进行的,那么当内存过大时就会导致stw时间过长,因此CMS比较适合小内存的应用。可以通过设置jvm参数来控制合适进行碎片整理:-XX:CMSFullGCsBeforeCompacton默认值为0,这个参数代表的含义是经过多少次FGC以后,进行整理。
G1本身包含YGC,MixedGC,FGC,G1垃圾回收器与上述垃圾回收器有一个很大的不同,在G1中已经没有了物理上的分代信息,只是在逻辑上有分代的概念。在了解G1垃圾回收器之前,需要先了解几个概念:
1)region,G1将内存划分为一个一个的region区域,对象都存储在一个个的reigon中,每个region既可以是年轻代也可以是老年带,但是在同一时间,一个region只能是年轻代和老年代中的一种,每个region的大小最大为32M,可以对region的大小进行设置,但是需要注意的是,必须为2的n次幂。下图为对region的一个大致描述。
2)humongous,大对象,超过单个region大小的50%。
3)Cset(collection set),垃圾回收集合,在Cset中存储的是需要进行垃圾回收的region集合,它不会超过整个堆大小的1%。
4)Rset(Remeber set),在每个region里面都有一个Rset,里面记录的是其它region中的对象指向该region的引用,是通过写屏障来实现的,当有新的指向引用时,就记录到Rset里面。
5)card table,内存区域划分成一个个card,card table中记录的是老年代中的card是否引用年轻代的card,他是由数据结构bitmap实现,防止YGC时对老年带进行全区扫描。
G1垃圾回收器是在1.8以后出现的,在1.9的时候为默认的垃圾收集器,它具有以下特点:1)并发收集,类似于CMS;2)压缩空闲空间不会延长GC的停顿时间,g1在回收old区的时候也是将存活对象移动到另外一个region中,也就实现了压缩整理;3)更易预测GC的停顿时间,G1年轻代和老年带的的比例为5%和60%,他会根据上一次垃圾回收所用时间,动态的调整年轻代大小,已达到GC预计要达到的时间,这个比例不要进行手动指定,不然会影响G1所用时间的预测;4)适用于需要高响应但是吞吐量要求不高的场景,它的吞吐量大约比ps+po低10%~15%。
G1垃圾回收器也会产生FGC,但是FGC对性能的影响很大,在jdk10以前为串行单线程,jdk10以后为并行多线程可以通过两个方面来避免发生FGC,硬件方面,增加内存和提升CPU性能;JVM方面,调低mixedGC的阈值(默认值为45%)。mixedGC的流程类似于CMS,主要分为四个步骤:1)初始标记stw;2)并发标记;3)最终标记stw;4)筛选回收,回收Cset中的Region。
在G1和CMS中,都有并发标记的阶段,那么在并发标记以后的最终标记(重新标记),又应该标记哪些对象呢,这就涉及到了并发标记算法,G1和CMS使用的是三色标记算法,ZGC使用的是颜色指针算法。
三色标记算法:使用黑灰白三种颜色对对象进行标记,黑色标记对象代表当前对象本身以及成员变量已经完成标记,灰色标记对象表示对象本身完成标记但是成员变量未完成标记,白色标记对象表示对象还未被标记。
在并发标记和最终标记的时候,可能会产生漏标的情况,即在并发标记的时候,某个白色对象与灰色对象的引用被删除,然后这个白色对象又和黑色对象增加了新的引用,此时就会产生漏标,导致白色对象被回收。
对于漏标有以下两种解决方案,也是CMS和G1采用的解决方案:
1)incremental update:增量标记算法,当出现上述场景的时候,将黑色对象重新变为灰色对象,然后再最终标记的时候,重新进行扫描。因此会造成对已经标记完的对象还要进行一次全量扫描,造成消耗时间变长。这是CMS解决漏标的处理方式。
2)SATB:snapshot at the beginning,将删除的引用信息压入堆栈,然后再最终扫描的时候去扫描这个堆栈,找到被删除引用的那个对象,然后对它进行标记。这是G1解决漏标的方式,因为G1的每个region中有一个Rset记录其它region对象到本region的引用,只需找到被漏标对象所在region的Rset,就能找到它的引用关系。
常见垃圾回收期组合:1)serial+serial old;2)ps+po,jdk1.8以后的默认垃圾回收器;3)parnew+cms(+serial old)。
使用垃圾回收器的jvm指令:
- XX:+UseSerialGC 使用serial+serial old垃圾回收器组合
-XX:+UserParallelGC 使用ps+po垃圾回收器组合
-XX:+UseParNewGC 使用parnew+serial old垃圾回收器组合,这种组合已经基本弃用
-XX:+UseConcMarkSSweepGC 使用parnew+CMS+serial old垃圾回收器组合
-XX:+UseG1GC 使用G1垃圾回收器
可以使用下面命令进行gc日志的打印:
java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGCdetails (可加上一个指定的class)
-Xmn:年轻代初始大小
-Xms:堆初始大小
-Xmx:堆最大大小
-XX:+PrintGCdetails:打印GC详细信息
在生产环境上最好让堆的初始大小和最大大小保持一致,这样可以避免堆的动态扩容。
在执行完上述指令以后,如果发生垃圾回收就会进行日志打印,打印日志如下图:
针对上面的gc日志做一下解释:
1. [GC(Allocation Failure) [PSYoungGen: 7436->1152K(8704K)] 4125972K->4125839K(4137472K), 0.0108142 secs] [Times: user=0.08 sys=0.02, real=0.01 secs]
GC:发生的垃圾回收类型,GC代表YGC
Allocation Failure:发生GC的原因
PSYoungGen: 7436->1152K(8704K):年轻代在回收之前使用7436K,回收以后使用1152K,年轻代总大小为8704K
4125972K->4125839K(4137472K):堆内存在回收之前使用4125972K,回收以后使用4125839K,堆的总大小为4137472K。
user=0.08:垃圾回收用户态消耗时间
sys=0.02:垃圾回收内核态消耗时间
2.PSYoungGen total 8704K, used 6403K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
PSYoungGen:所属区域,年轻代
total 8704K:总共8704K,年轻代总大小=eden + 1个survivor
used 6403K:使用了6403K
0x00000007bf600000:8704K(6403K)内存空间开始的地址
0x00000007c0000000:6403K内存空间结束的地址
0x00000007c0000000:8704K内存空间结束的地址
3. Metaspace used 2748K, capacity 4486K, committed 4864K , reserved 1056768K
Metaspace: 区域,元数据区
used 2748:目前元数据区已经使用的内存大小
capacity 4486K:目前元数据区的总大小
committed 4864K:为元数据区划分的内存大小
reserved 1056768K:为元数据区预留的内存大小
补充信息:
在上面提到了年轻代和老年代的概念,那么年轻代对象如何进入老年带呢?是通过对象年龄来控制,对象年龄指的是年轻代对象存活了几次YGC,每存活一次岁数加1,存储于对象头中,当对象年龄达到了设置的年龄,就会进入老年带中。
通过-XX:MaxTenuringThreshold来指定升代年龄。如果不指定的话,ps的默认值为15,cms的默认值是6,G1的默认值是15。
还有一种情况会造成对象从年轻代进入老年代,动态年龄,在进行完YGC以后,存活对象超过一个survivor区的50%的时候也会触发升代,年龄大的对象会进入老年代。