Java垃圾回收

1 回收哪些内存

垃圾收集主要是针对堆和方法区进行;程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。

1.1 判断对象存活

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡,有可达性分析和引用计数两种方法。

  1. 引用计数法

    给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

    其缺点在于:存在循环引用问题,即两个相互引用的对象永远不会被回收。

    Java中引用分为4中:强引用、软引用、弱引用和虚引用,4者区分如下:

    • 强引用:我们平时使用new方法创建的就是强引用, 被强引用关联的对象不会被回收 。

    • 软引用:使用SoftReference类来创建软引用, 被软引用关联的对象只有在内存不够的情况下才会被回收。

    • 弱引用: 使用 WeakReference 类来实现弱引用 ,被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

    • 虚引用: 使用 PhantomReference 来实现虚引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。

      其唯一目的就是能在这个对象被回收时收到一个系统通知。

  2. 可达性分析法

    通过GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

    Java中的GC Roots一般包含:

    • 虚拟机栈中引用的对象;

    • 本地方法栈中引用的对象;

    • 方法区中类静态属性引用的对象;

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

需要注意的是:即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

1.2 方法区回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载

在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

2 什么时候回收

通常是不可预测的。

1、在程序空闲的时候;

2、执行代码:system.gc()时候;

3、Java堆内存空间不足的时候。

(1)minor GC: 当新对象生成,并且在Eden申请空间失败时,就会触发minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区

(2)Full GC: 对整个堆进行整理,包括Young、old 和Perm。

3 怎么回收

3.1 垃圾回收算法

一、标记-清除算法

算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  • 效率问题,标记和清除的效率都不高
  • 空间问题,GC后产生大量不连续碎片

二、标记-复制算法

将内存分成两个大小相等的块,每次只使用其中一个块。当一个块内存用完后,将存活的对象复制到另外一个块中,然后把已使用的内存空间全部清理掉。

实现简单、运行高效,且不容易产生碎片,但内存利用率低。而且算法的效率和存活对象的数目有关,如果存活对象很多,那么copying算法效率会大大降低

现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

三、标记-整理算法

标记完所有对象后,不直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

四、分代收集算法

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代:

  • 新生代:复制算法
  • 老年代:标记-清除或标记-整理算法

3.2 垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

  • 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
  • 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

一、Serial收集器

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

它是 Client 模式下的默认新生代收集器, 新生代采用标记-复制算法,老年代采用标记-整理算法。 因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

二、ParNew收集器

它是 Serial 收集器的多线程版本。 新生代采用标记-复制算法,老年代采用标记-整理算法。

是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

三、Parallel Scavenge收集器

Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用标记-复制算法,老年代采用标记-整理算法。

四、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。分为以下四个流程:

  1. 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。

  2. 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。

  3. 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。

    这里有个问题,为什么要重新标记?

    被回收类可能重写了finalize方法,在该方法中,把该对象和安全对象建立上关联。这种情况下,这个对象就不再需要被回收。

  4. 并发清除: 不需要停顿。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感,默认启动(核心数+3)/4个线程,当核心数少于4个时,用户程序可能变得比较慢;
  • 无法处理浮动垃圾(并发清理期间产生的垃圾);
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

五、G1收集器

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记

  • 并发标记

  • 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。

  • 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

G1收集器具有以下特征:

  • 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的 ,这意味着运行期间不会产生内存空间碎片。

  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型, 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

六、ZGC收集器

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,它的设计目标包括:

  • 停顿时间不超过10ms;
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
  • 支持8MB~4TB级别的堆(未来支持16TB)。
  1. ZGC原理

    与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

    ZGC垃圾回收周期如下图所示:

    ZGC只有三个STW阶段:初始标记再标记初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

  2. ZGC关键技术

    ZGC通过着色指针读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。

    关于着色指针读屏障技术,可参考https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html。

参考资料

  1. https://javaguide.cn/java/jvm/jvm-garbage-collection
  2. https://pdai.tech/md/java/jvm/java-jvm-gc.html
  3. https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信