JVM之垃圾回收

521 阅读14分钟

一、垃圾回收

1、如何判断对象是否存活

1、引用计数法

在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就会+1,当引用失效的时候,计数器的值就会-1,等于0的时候说明没有被引用,则会被JVM判定为垃圾。

image-20201116233238269

注意:引用计数器无法解决实例对象循环引用的问题,对于循环引用在主观上其实它已经属于垃圾了,但是由于引用计数器的值不为0,导致JVM不会回收,目前没有JVM使用引用计数器法了。

打印GC的两个参数配置:-verbose:gc -XX:+PrintGCDetails

2、可达性分析算法

选择某个对象作为GC ROOTS,也就是根,从上往下查找,形成一条引用链,当某个对象不在这个引用链上的时候,就判断这个对象为垃圾。这种方式也解决了实例对象循环引用的问题。

可以作为GC ROOTS的对象:

​ 虚拟机栈中的局部变量表所引用的对象。

​ 本地方法栈中的局部变量表所引用的对象。

​ 方法区中的类变量所引用的对象。

​ 方法区中的常量所引用的对象。

image-20201116234824268

2、对象引用分类

1、强引用

类似Object obj = new Object();,只要这类引用存在,垃圾收集器就不会回收掉被引用的对象;

2、软引用

有用但非必须的对象,可以通过SoftReference类来实现,在系统将要发生内存溢出之前,会将这类引用的所引用的对象进行回收,如果还是内存不足,则会抛出OOM;

3、弱引用

非必须的对象,可以通过WeakReference类来实现,被弱引用所引用的对象只能存活到下一次垃圾回收之前,无论内存是否充足;

4、虚引用

最弱的一种引用关系,可以通过PhantomReference类来实现,每次GC都会被回收,设置虚引用的目的是:垃圾回收的时候会收到一个系统通知子;

3、java.lang.Object#finalize()二次标记

一个对象是否应该在GC的时候被回收,至少要经历两次标记过程;

第一次使用引用计数法或可达性分析算法进行标记;

第二次对未被标记的对象,判断是否重写了java.lang.Object#finalize方法以及是否从未被调用过,如果是,则此次GC逃过一劫,下次GC的时候就失效了,会被回收;

4、如何回收

1、回收算法

1、标记-清除算法

从这个算法的名称可以见名知意,分为两步:1、标记;2、清除。而标记就是上面的如何判断对象是否存活,标记完成之后,通过标记-清除算法对未被标记的对象进行清除。

内存分配方式:空闲列表的方式;

经常使用在JVM的老年代;

优点:

​ 实现简单;

缺点:

​ 效率问题(标记、清除过程效率不高)。

​ 空间问题(由于标记的实例对象都是散列的,那么清除之后可用的地址空间也是散列的,当有一个新的大对象过来的时候可能在这么多散列的小的地址空间中会出现找不到符合这么大的地址空间,导致再次触发GC,提高了触发GC的频率)。

image-20210111234856985

2、复制算法

物理上把地址空间进行划分成两半,每次新的对象只在一半地址空间上进行分配,在GC的时候进行标记,把标记的存活对象复制到另外一半的地址空间,然后清空之前的地址空间;

内存分配方式:指针碰撞的方式;

经常使用在JVM的新生代;

优点:

​ 解决了标记-清除算法的效率问题;

​ 解决了空间碎片的问题;

缺点:

​ 浪费了地址空间,属于用空间获取时间的一种做法;

image-20210111235717975

复制算法在JVM中的使用就是新生代,因为新生代是JVM垃圾回收关注的主要区域。

image-20201117234637334

3、标记-整理算法

标记-整理算法在标记清除算法的基础上增加了整理的一步,将所有被标记的存活的对象移动到空闲空间的一端,然后清除另外一端;

注意:没有复制算法那种物理分界线,只是逻辑上的;

常用于老年代;

内存分配方式:指针碰撞

优点:

​ 解决了标记-清除算法空间地址不连续的问题;

缺点:

​ 整理需要时间;

image-20210112231336640

4、分代收集算法

该算法严格意义上来说并不是一个真正的算法,而是描述了新生代和老年代使用不同的回收算法。目前在JVM中,新生代需要回收的对象多,存活的对象较少,一般不会超过10%,从而使用复制算法,提升效率;老年代一般回收的对象很少,存活的对象会很多,使用复制算法效率并不好,因此使用标记-整理算法。

image-20201118233149479

2、垃圾收集器

image-20210117115807794

串行回收:同一时间段内只允许有一个CPU用于执行垃圾回收操作;

并行回收:同一时间段内允许又多个CPU用于执行垃圾回收操作;

并发回收:同一时间段内允许用户线程和垃圾回收线程同时运行;

1、Serial/Serial Old收集器

(1)Serial(串行)收集器是基于复制算法的新生代收集器,最基本、历史最悠久的收集器;

单线程垃圾收集器,在进行垃圾回收的时候会STW,也就是停止应用程序,当收集完了,应用程序继续运行;

常用于客户端,因为客户端一般JVM都比较小,使用Serial收集器速度也很快,客户端基本感知不到应用程序停止;

在单核CPU环境下的效率并不低于并行收集;

优点:

​ 单线程下简单高效;

缺点:

​ STW

参数:

​ -XX:+UseSerialGC

(2)Serial Old(串行)收集器是基于标记-整理算法的老年代收集器;

可以作为CMS收集器发生的Concurrent Mode Failure时的Full GC收集器;

参数:

​ -XX:+UseSerialOldGC

image-20210117114818062

2、ParNew收集器

ParNew(并行)收集器是基于复制算法的新生代收集器;

ParNew收集器相对于Serial收集器,就是在原来单线程的基础上变成了多线程收集;其他的与Serial收集器并无太大区别;

优点:

​ 相对于Serial收集器,在多核CPU环境下缩短了STW的时间;

缺点:

​ 占用了更多的CPU资源,默认ParNew收集器使用和CPU数量相同的垃圾回收线程数量;

参数:

​ -XX:+UseParNewGC

​ -XX:ParallerGCThreads:设置垃圾回收线程数量

image-20210117111732646

3、Parallel Scavenge/Parallel Old收集器

(1)Parallel Scavenge(并行)收集器是基于复制算法的新生代收集器;

和其他收集器最大的区别在于关注点不同:其他收集器如Serial、CMS等关注于STW的时间,降低STW时间,提升用户体验;而Parallel Scavenge收集器关注于服务端的高并发以及高可用、吞吐量,而不仅仅着眼于降低STW的时间;

吞吐量=(运行用户代码的时间)/(运行用户代码的时间+垃圾回收的时间),如虚拟机运行了100分钟,运行用户代码的时间的为99分钟,那么吞吐量就为99%;

优点:

​ 可控制的吞吐量;

缺点:

​ 占用了CPU资源;

参数:

​ +XX:+UseParallelGC

​ +XX:MaxGCPauseMillis 垃圾收集器最大停顿时间,这个值并不是越小越好,假设相同的比较大的一块新生代内存空间,STW分别为1、100:

​ 在STW为1的情况下,由于每次都清理不干净,不一会新生代又满了,则会频繁的触发GC

​ 在STW为100的情况下,每次都能完整的清理内存空间,则触发GC的次数会小的多

​ 在这样的对比情况下,STW为1的性能还不如SWT为100的性能,因此选择一个合适STW的时间是至关重要的。

​ +XX:GCTimeRatio 吞吐量大小,取值范围:(0,100),默认是99;

​ +XX:+UseAdaptiveSizePolicy:GC自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量;

(2)Parallel Old(并行)收集器是基于标记-整理算法的老年代垃圾收集器;

只能配合Parallel Scavenge使用;

​ 参数:

​ -XX:UseParallelOldGC:使用Parallel Scavenge+Parallel Old收集器;

image-20210117120112792

4、CMS收集器

Concurrent Mark Sweep基于标记-清除算法的老年代收集器;

它是一种以获取最短回收停顿时间为目标的收集器,说白了就是降低STW;

真正的实现了一边清理垃圾,一边产生垃圾;也正是这个原因会导致Concurrent Mode Failure;

在分代收集算法中只能配合ParNew或Serial使用,不能配合Paralle Scavengel垃圾收集器;

注意:CMS并不是等到了老年代满了才触发,而是到达了参数-XX:CMSInitiatingOccupancyFraction设置的占用百分比之后就会触发;这个其实是CMS GC,而Full GC是Serial Old执行的,在CMS中,CMS GC和Full GC并不是同一种性质

CMS收集器的工作流程:

  • 初始标记(Initial Mark):仅仅是标记GC Roots能直接关联的对象,速度很快,STW;
  • 并发标记(Concurrent Mark):进行GC Roots Tracing的过程,在整个过程中耗时最长;
  • 重新标记(Remark):为了修正并发标记阶段因用户线程继续运行而导致的已被标记产生变动的对象,这个阶段的停顿时间会比初始标记要长,但是比并发标记要短,STW;
  • 并发清除(Concurrent Sweep):对未被标记的对象进行清除;

整个标记-清除算法的性能消耗瓶颈就在标记和清除的两个过程,CMS通过把这两个过程弄成并发的来提升性能;

优点:

  • 并发收集;

  • 低停顿;

缺点:

  • 占用大量的CPU资源,默认会使用(CPU数量+3)/4个线程;

  • 无法处理浮动垃圾(所谓的浮动垃圾就是在本次GC过程中产生的新的对象,新产生的那些对象只能在下一次GC才可能被回收),可能会导致Concurrent Mode Failure(CMS在垃圾回收的过程中,会在老年代中预留出一部分内存放置新产生的对象或者到达年龄从存活区到达老年代的对象),从而触发Full GC(使用Serail Old作担保);

​ Concurrent Mode Failure的常见原因:

​ -XX:CMSInitiatingOccupancyFraction参数设置占用的阈值太高,导致预留的空间太少, 不足以存放从存活区到达老年代的的对象,

触发Full GC;

​ 用户线程在CMS的过程中产生了大对象,导致预留的空间不足以存放,触发Full GC;

  • 因为使用的是标记-清除算法(因为并发清除,用户线程也在使用,没办法使用标记-整理算法),会导致空间碎片问题;

参数:

  • +XX:UseConcMarkSweepGC
  • +XX:ParallelCMSThreads:设置CMS的线程数量
  • -XX:CMSInitiatingOccupancyFraction:设置老年代触发CMS的阈值
  • -XX:+UseCMSInitiatingOccupancyOnly:是否一直使用设定的阈值,如果不设置,只在第一次使用设定的阈值,后续则自动调整;
  • -XX:+UseCMSCompactAtFullCollection:触发Full GC之后对内存进行整理,默认开启;
  • -XX:CMSFullGCsBeforeCompaction:设置触发多少次Full GC之后对内存进行整理,默认是0,表示每次Full GC都进行内存整理;

image-20210117143909749

5、G1收集器

目前最牛逼的垃圾收集器,拥有以上收集器的所有优势,目地是为了取代CMS收集器;应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求;

最大的特点:可预测的停顿时间模式;

Region:

​ G1的堆内存模型和之前的堆内存模型不一样,G1将整个堆内存划分为大小相同的Region,在逻辑上划分了新生代和老年代,但是并不存在真实的物理隔阂;

image-20210117154025411

​ 这里相对之前多了一个Humongos(巨型对象),所谓的巨型对象是指占用的内存空间大于等于Region一半的对象,如果一个Region不足以存放H-obj,则通过连续的Region来存放H-obj;H-obj是直接进入老年代的;

Card Table:

​ Card Table将每个Region划分为多个连续的地址空间,每个连续的地址空间称为一个Card,可以简单理解为一个数组,如果这个Card引用了别的对象,则把这个索引对象的值置为0;这是一种point-out结构(我引用了谁);

image-20210117160239990

Rset(Remembered Set):

​ Rset是基于Card Table实现的,理论上每个Region都有一个自己的Rset,它记录哪个Region中的哪个对象引用了当前这个Region内的对象,是一种point-in结构(谁引用了我);Rset是一个Hash Table结构,key是Region的其实地址,value是一个集合,存放的是Card Table中的索引;

使用Rset的最大好处就是标记的时候避免了全堆的扫描,只需要扫描Rset中记录的堆内存空间就可以完成GC Tracing;

image-20210117160212132

Rset和Card Table整体图

Cset:

​ Collection Set是收集集合,表示每次GC时需要回收的Region;这个Cset是具备优先级的,根据用户设备的STW的时间通过G1的可预测停顿模型来选择Cset中回收时间短以及回收效益高的Region,从而实现满足用户设置的STW时间;

分代收集:

​ 在G1收集器中,也分为两种收集;young GC和mixed GC,young GC只回收Eden区的Region,而mixed GC回收所有分区的Region,但无论是哪种GC,都是从Cset选择优先级高的Region进行回收,两者本质上没有什么区别,只是选择的Cset发生了变化;

​ young gc采用的是复制算法,mixed gc采用标记-整理算法;

image-20210117163133564

注意:mixed GC不等同于Full GC,G1收集器也是使用的Serial Old来做Full GC,这点和CMS是一致的;

G1垃圾回收过程:

  • young gc
  • 并发标记阶段(mixed gc)
    • 初始标记:标记一下GC Roots能直接关联的对象;STW;
    • 并发标记:从GC Roots开始对堆中对象进行可达性分析,找到存活对象,耗时较长;
    • 最终标记:为了修正在并发标记阶段因用户线程继续运行而导致变动的哪部分的对象;STW,并行;
    • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划;
    • image-20210117170255777
  • full gc

因为mixed gc每次只会回收收益高的Region,那么长时间下去,就可能会导致堆空间不足,那么就需要Serial Old来做Full GC;

G1中Full GC触发的场景:

  • 从Eden复制到Survivor时,无法找到可用的空闲空间;
  • 从Old整理存活对象时,无法找到可用的空间空间;
  • H-obj在老年代无法找到连续的Region;

对G1收集器的最大的优化点就在于降低Full GC的次数;

常用参数:

  • -XX:UseG1GC,java9默认
  • -XX:G1HeapRegionSize=n:设置分区大小,必须2的倍数;
  • -XX:MaxGCPauseMillis:期望的STW时间;
  • -XX:InitiatingHeapOccupancyPercent:设置触发并发标记阶段的阈值;
  • -XX:ParallelGCThreads:STW期间,并行线程数;
  • -XX:ConcGCThreads=n:并发线程数;
  • -XX:G1ReservePercent=n:空闲空间的预留百分比;
  • -XX:G1NewSizePercent=n:新生代占比最小值;
  • -XX:G1MaxNewSizePercent=n:新生代占比最大值;

参考文献:

Java Hotspot G1 GC的一些关键技术

【jvm】垃圾收集器

深入理解JVM(3)——7种垃圾收集器