JVM垃圾收集机制

深入学习Java基础知识
2021-05-17 14:29 · 阅读时长5分钟
小课

垃圾收集机制是JVM一个非常重要的部分,它可以实现在Java程序运行中自动回收不用的内存,以提高内存利用率。在Java程序运行中会不断创建对象,而某些对象随着程序的运行,可能不再需要,那么这部分对象就应该被回收以释放内存,如何判断对象是否应该回收以及如何回收这部分对象呢?

一、可达性分析算法和GC Roots

Java垃圾收集器采用可达性分析算法来判断一个对象是否应该被回收,其基本思路是从一系列GC Roots为起点,遍历内存中的对象,如果一个对象能从GC Roots出发被访问到,说明该对象还存活,否则说明该对象应该被回收

JVM垃圾收集机制

我们可以简单理解GC Roots是一些内定的存活对象,只要跟这些存活对象有关联的其它对象都不应该被回收,在JVM中GC Roots主要包括以下部分

  • 虚拟机栈中的变量
  • 本地方法栈中的变量
  • 常量、静态变量
  • 存活的线程

当找出可以回收的对象时,如何去高效的回收这些内存呢?JVM 规范没有明确定义如何去回收这些可回收的内存,因此不同的JVM虚拟机有不同的垃圾收集实现算法,下面介绍几种常见的垃圾收集算法。

二、常见的垃圾收集算法

1、标记清除算法

该算法分为两步,首先找出并标记所有可回收的内存区域,然后将这些区域清空,恢复可用状态,如下图所示

JVM垃圾收集机制

标记清除算法逻辑简单清晰,但是有一个缺点,就是会造成内存碎片,使得可用内存不连续,不利于内存分配,比如当需要大块连续内存时,回收的小片内存就无法使用。

2、标记复制算法

标记复制算法是由标记清除算法改进而来,解决了内存碎片的问题,它的基本思路是,将内存分为两块区域,正常使用时,只在其中一块区域进行内存分配,当这块内存区域快满时进行垃圾收集,首先标记出所有存活对象,然后将存活的对象全部复制到另一区域,最后将之前的那块内存区域全部清除,恢复为可用状态。

JVM垃圾收集机制

标记复制算法和标记清除算法一样简单清晰,解决了内存碎片的问题,但是也有一个缺点,那就是造成内存浪费,实际只有一半内存可用

3、标记整理算法

标记整理算法是基于以上两种垃圾收集算法改进而来,它的基本思路是,首先将存活的对象标记出来,然后把所有存货的对象移动到同一端,最后将不包含存活对象的那一端清除,恢复可用状态。

JVM垃圾收集机制

标记整理算法解决了内存碎片和内存浪费的问题,但是由于它会在内存区域中频繁地移动存活对象,使得效率大大降低

4、分代垃圾收集算法

上面提到的三种垃圾收集算法都有各自的优缺点,有没有一种办法能够尽量发挥它们的优点,并且尽量避免它们的缺点呢?

  • 比如说,如果仅在某块很小的内存区域使用标记复制算法,那么造成的内存浪费是不是就可以接受了。
  • 比如说,如果某块内存区域,当发生垃圾收集时只有很少的对象被回收,那么标记整理算法的效率问题是不是就降低了很多。

分代垃圾收集算法基本思路就是内存分区,将上面这三种垃圾收集算法合理运用,不同的算法应用不同的内存区域和场景。堆内存是垃圾收集发生最频繁的内存区域,这里主要分析Java的堆内存的内存模型和垃圾收集策略。

JVM垃圾收集机制

从图中可以看出,堆内存被分为了新生代区(1/3)和老年代区(2/3),新生代又分为了Eden区(8/10)和Survivor区(2/10),Survivor区又分为from区(1/2)和to区(1/2),为什么是这样划分堆内存?垃圾收集器是如何在这些区域中进行内存回收的?

Eden区

IBM 发表的一份专业研究表明,接近98%的对象都是存活期很短。新创建的对象主要分配在Eden区,当Eden区的内存不足时会触发Minor GC,在垃圾收集器执行完成之后,Eden区将被清空,其中的大部分对象都将被回收,少数存活的对象会被移动到Survivor区,from区还是to区?后面会解释,如果Survivor区的内存不足则直接移动到老年代区。这不就是标记复制算法的应用吗?

Survivor区

Survivor区又分为from区(1/2)和to区(1/2),当执行Minor GC时,Survivor区同样也会进行垃圾收集,与Eden区不同的是,Survivor区的from区和to区会交替执行标记复制算法,比如说如果第n次垃圾收集,标记from区的存活对象,将其移动到to区,然后清除from区,那么第n+1次就会是标记to区的存活对象,将其移动到from区,然后清除to区。Eden区的存活对象移动到from区还是to区,与当时Survivor区内部的移动方向一致

那么问题来了,from区和to区交替移动,什么时候才会从Survivor区移动到老年代区呢?当一个对象在from区和to区之间移动的次数超过设置的阈值(-XX:MaxTenuringThreshold,默认15)时就会移动到老年代区。

老年代区

老年代区占了整个堆内存的2/3,只有在触发Major GC时,才会对老年代的对象进行垃圾收集,老年代区使用的是标记整理算法,运行效率低,耗时较长,会造成卡顿(并非都会卡顿,可以参考右边👉的问答),但是老年代的对象存活率比较高,所以综合而言,在老年代区使用标记整理算法仍然要优于使用另外两种垃圾收集算法。

为什么要划分Survivor区?

如果触发Minor GC就直接将存活对象从Eden区域移动到老年代的话,那么老年代区的内存会更容易耗尽,从而更频繁触发Major GC,降低了垃圾收集效率,而且很多对象可能在第一次Minor GC没有被回收,但是在第二、第三次、...次Minor GC时被回收了,使用Survivor区作为缓冲区,可以确保进入老年代的是拥有较长生命周期的对象。

gcgc rootsjava内存模型垃圾收集器垃圾收集算法