JVM中的垃圾回收主要围绕三个问题展开

  • 哪些对象需要回收?
  • 什么时机回收?
  • 如何回收?

本文将就这三方面进行讨论

1、哪些对象需要回收

1.1 分代模型

上图是我们熟知的分代模型,这里有一个问题,为什么要建立这样一个分代模型

笔者认为因为我们的日常的系统中会不断生产大量的对象,其中主要包括两种类型:快速消亡和长期存在;那么不同类型的对象根据其特性需要采取不同的回收策略,因此用新生代和老年代区分。

1.2 如何判断对象是垃圾对象

  • 计数器引用法

就是给对象添加一个引用计数器,每当有一个地方引用这个对象时,计数器值加1,每当一个引用失效时,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。在java虚拟机中,并没有使用这个算法来管理内存,其中最主要的原因就是它很难解决对象之间循环引用的问题。

  • 可达性分析算法

基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的,

GCroots对象

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
  2. 方法区中的类静态属性引用的对象 ;
  3. 方法区中的常量引用的对象 ;
  4. 本地方法栈中的引用的对象;

2、 垃圾回收算法

2.1 标记清除算法

  • 触发垃圾回收时,直接标记出待回收对象,随后将其清除。

标记清除算法有何弊端呢?

如上图所示,随着垃圾对象被清除,可用空间东一个西一个,显得十分凌乱,造成了大量的内存碎片。继而造成内存浪费,同时在分配大对象时,很可能因为找不到连续的内存空间,频繁触发垃圾回收。

2.2 复制算法

在认识到标记清除算法的弊端以后,我们明确了优化的路线:在垃圾回收以后避免造成大量的"内存碎片"。

  • 首先将新生代内存分为两块区域,一块用于存储对象,另一块为空白内存,当触发垃圾回收时,对存活对象进行标记,并将其复制到空白区域并紧凑地排列,并将原来的区域一扫而空。两块区域就这么循环使用。

复制算法有什么弊端呢?

显然如果新生代内存设为1个G,那么实际可用内存只有512M,触发垃圾回收的时间缩短了一半,内存利用率太低。

2.3 复制算法的优化

根据上述复制算法的弊端,我们明确了优化的目标:需要提高内存的利用率。

其实绝大多数的对象都是存活周期非常短的对象,假设一次垃圾回收,存活的对象不到1%,我们根本不需要50%的空间来存储它们。

  • 如图所示,将新生代内存分为三块,1个Eden区+2个survivor区,当Eden区和survivor1区内存占满后触发垃圾回收,将存活对象复制到survivor2区中,然后将Eden区和survivor1区清扫干净,此时survivor1和survivo2循环使用。此时我们可以发现只有10%的内存是闲置的,大大提高了内存的使用效率。

此时我们可能会疑惑,当survivor区的对象堆满了放不下了,或者来一个超大对象survivor区放不下,在这种情况我们如何处理,因此后面将介绍新生代对象是如何晋升到老年代的。

2.4 标记整理算法(老年代)

规则如下:

先标记老年代中的幸存对象,将其紧凑地排列至一侧,然后统一回收垃圾对象。然而老年代的回收速度比新生代的垃圾回收速度慢10倍。

3、老年代的晋升策略

3.1 年龄阈值判断法

在系统运行过程中总有一些对象是长期存在的,这些对象每次躲过一次minorGC,就假定它的年龄+1,在默认情况下

当对象的年龄达到15岁时,也就是躲过了15次GC过后,该对象就被转移到老年代。

JVM参数:“-XX:MaxTenuringThreshold”

思考

比如我们程序中的Controller和Service对象都是长期存在的对象,其实它们本来就应该待在老年区,有什么办法可以让他们尽快地进入老年代呢?

3.2 动态年龄判断法

动态年龄判断法的规则如下

将survivor区的一半作为容量阈值,一旦survivor区的容量超过了50%(可设置阈值比例),就将年龄的最大的家伙赶到老年代去,这样就可以尽可能早地让长期存在的对象进入老年代

3.3 大对象直接进入老年代

规则如下

可以设置新生代对象的最大占用内存,一旦超过就会被移入到老年代,这样可以避免大对象频繁躲过minorGC,造成大对象被不停地倒来倒去,浪费时间。

3.4 survivor区超载

规则如下

 

随着时间的推移,新生代的幸存对象大于survivor区的容量大小,此时这些对象直接转移到老年代去了。

为什么老年代不采用复制算法?

因为老年代的存活对象通常比较多,如果采用复制算法,每次都移动大量对象,效率较慢。

3.5 老年代空间不足

如果随着新生代的幸存对象,不断进入老年代,老年代的对象越来越多,出现了老年代剩余空间无法存放新生代新晋升的对象。

规则如下

1、当老年代剩余内存>新生代对象,则放心大胆地进行minorGC,因为即使GC过后没有回收任何垃圾,老年代也足以存放新生代的幸存对象。

2、当老年代剩余内存<新生代对象,这时候需要看 “HandlePromotionFailure”参数是否设置,如果设置了那么

会判断老年代剩余空间是否大于之前minorGC后进入老年代对象的平均大小

3、如果大于平均大小则进行minorGC。

4、如果没有设置担保策略,或者剩余空间小于平均大小,则触发Full GC,然后再执行Minor GC。

5、在设置担保策略后,进行一次minor GC,此时会出现三种可能性:(1)minorGC过后幸存对象小于survivor区的大小,此时皆大欢喜,直接进入survivor区;(2)monorGC过后大于survivor区,但是小于老年带的剩余空间,直接进入老年代;(3)minorGC过后既大于survivor区又大于老年代剩余空间,这时候会触发FullGC

6、如果fullGC过后,仍然没有足够的空间存放剩余对象,就会导致OOM

总结

本文主要讨论了以下三点:

1、JVM的分代模型以及垃圾对象的判定方法

2、四种垃圾回收算法的演进及适用场景

3、老年代的5种晋升策略

欢迎进行交流沟通,文章不足及有误之处敬请指正!
原文链接:https://juejin.cn/post/7051859001793298469