【日常学习记录】JVM 分代垃圾回收是如何进行的?

JVM 的垃圾收集算法主要包括 4 种:标记-清除算法,标记-整理算法,复制算法,分代收集算法,相比而言,分代收集算法是最常用的,也相对复杂一点,所以在此整理记录一下,加深记忆。


垃圾收集的是哪里?

首先抛出一个问题,垃圾收集,收集的到底是哪里?

我们来看一下 Java 的内存区(又叫做运行时数据区),如下图所示:
【日常学习记录】JVM 分代垃圾回收是如何进行的?
可以看出,按照线程共享和线程私有可以分为两大块,各自的组成如下:

线程共享
堆,方法区(HotSpot 的永久代);

线程私有
程序计数器,Java 虚拟机栈,本地方法栈;

在内存结构中,论大小,堆可以称得上是 JVM 中所管理的最大的一块内存,所以它就是垃圾收集器管理的主要目标了。

为什么要分代?

一般来说,我们将堆分为新生代老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法。

新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成垃圾收集的过程。

老年代的对象存活率是比较高的,且没有额外的空间对它进行分配,所以我们必须选择 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。

回收是如何进行的?

从下图中可以看出,堆被分为新生代和老年代,它们之间的空间大小比例为 8:1:1,其中的新生代又被划分为 3 个部分,分别为 Eden 区,From Survivor 区和 To Survivor 区。
【日常学习记录】JVM 分代垃圾回收是如何进行的?
回收过程如下:

  • 当新创建一个对象后,它会被默认分配到新生代的 Eden 区域
  • 当 Eden 区满了之后,会把 Eden 和 From Survivor 区域的存活对象统一移动到 To Survivor 区,并且给移动对象的年龄 + 1
  • 之后会回收死掉的对象,并把原来的 From Survivor 区改成 To Survivor 区To Survivor 区改为 From Survivor 区
  • To Survivor 区满了后,就会将所有的对象移动到老年区

其中提及到了一个概念叫移动对象的年龄,它又是指什么呢?

实际上,虚拟机给每个对象都设置了一个对象年龄(Age)计数器,如果对象在 Eden 区域出生并经过新生代垃圾回收(Minor GC) 后仍然存活,且能被 Survivor 区域所容纳的话,就会被移动到 Survivor 空间中,并将对象年龄设为 1。

之后,对象在 Survivor 中每熬过一次新生代垃圾回收(Minor GC),年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中(对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置)。

此时突然有了一个新的问题,为什么 GC 的分代年龄要设置为 15,16 不可以么?

查阅了网上的资料后了解到,对象的 GC 年龄是保存在对象的头信息里的,头信息又分为两个部分:Mark Word 和 Klass Pointer。其中的 Mark Word 主要用于存储对象自身的运行时数据,我们的 GC 分代年龄就存储 Mark Word 里面。

看到一篇博客对这个问题分析的很到位,附上链接,大家可以自行查看。

https://javap.blog.csdn.net/article/details/103721326

摘录部分内容:

我们可以通过实际操作,来验证 GC 分代年龄是如何进行存储的:OpenJDK 提供了一个工具包,可以输出对象的结构布局信息。

引入依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

编写一段测试代码如下:

/**
 * @Author: pch
 * @Date: 2019/12/23 09:08
 * @Description: 查看对象的头信息
 */
public class HeaderTest {
	public static void main(String[] args) throws InterruptedException {
		Person p = new Person();
		//不调用 hashCode() 不会记录哈希码
		int hashCode = p.hashCode();
		//转16进制输出,与头信息中的hashCode进行比较
		String hex = Integer.toHexString(hashCode);
		System.out.println("HashCode十六进制:"+hex);
		print(p);
	}

	static void print(Person p){
		System.err.println(ClassLayout.parseInstance(p).toPrintable());
	}
}
class Person{
	private boolean flag;
}

控制台输出如下:
【日常学习记录】JVM 分代垃圾回收是如何进行的?
从图片中可以看到,因为 Object Header 采用 4 个 bit 位来保存年龄,而 4 个 bit 位能表示的最大数就是 15,所以分代年龄最大值只能是 15,16 的话就存不下了。

到此,年龄最大值的问题就搞明白了,我们分析了半天,都是在说堆的回收,那么方法区没人管了么?这就要说到回收的类型了。

回收分为哪些类型?

垃圾回收分为部分收集 (Partial GC)整堆收集 (Full GC),我们刚刚提到的方法区,就是通过整堆收集来进行垃圾回收的。

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC): 收集整个 Java 堆和方法区。


整理到这里,分代垃圾回收的过程基本就清晰了,保持学习,坚持记录,本篇博客参考了以下资料,在此表达感谢:

https://blog.csdn.net/xiaozhaoshigedasb/article/details/85568596
https://javap.blog.csdn.net/article/details/103721326

上一篇:java内存模型与volatile关键字


下一篇:我滴个鬼鬼,精选JVM垃圾回收机制全面分析,聊聊你眼中的JVM