JVM 垃圾回收的工作原理

开发
本文讨论了各种不同的 JVM 内存“代”,以及它们是如何在分代垃圾回收算法中起作用的。对于程序员来说,掌握 Java 的内存管理机制并不是必须的,但它能够帮助你更好地理解 JVM 处理程序中的变量和类实例的方式。

对于程序员来说,掌握 Java 的内存管理机制并不是必须的,但它能够帮助你更好地理解 JVM 是如何处理程序中的变量和类实例的。

Java 之所以能够如此流行,自动 垃圾回收Garbage Collection(GC)功不可没,它也是 Java 最重要的几个特性之一。在这篇文章中,我将说明为什么垃圾回收如此重要。本文的主要内容为:自动的分代垃圾回收、JVM 划分内存的依据,以及 JVM 垃圾回收的工作原理。

Java 内存分配

Java 程序的内存空间被划分为以下四个区域:

  • 堆区Heap:对象实例就是在这个区域分配的。不过,当我们声明一个对象时,堆中不会发生任何内存分配,只是在栈中创建了一个对象的引用而已。
  • 栈区Stack:方法、局部变量和类的实例变量就是在这个区域分配的。
  • 代码区Code:这个区域存放了程序的字节码。
  • 静态区Static:这个区域存放了程序的静态数据和静态方法。

什么是自动垃圾回收?

自动垃圾回收是这样一个过程:首先,堆中的所有对象会被分类为“被引用的”和“未被引用的”;接着,“未被引用的对象”就会被做上标记,以待之后删除。其中,“被引用的对象”是指程序中的某一部分仍在使用的对象,“未被引用的对象”是指目前没有正在被使用的对象。

许多编程语言,例如 C 和 C++,都需要程序员手动管理内存的分配和释放。在 Java 中,这一过程是通过垃圾回收机制来自动完成的(尽管你也可以在代码中调用 system.gc(); 来手动触发垃圾回收)。

垃圾回收的基本步骤如下:

1、标记已使用和未使用的对象

在这一步骤中,已使用和未使用的对象会被分别做上标记。这是一个及其耗时的过程,因为需要扫描内存中的所有对象,才能够确定它们是否正在被使用。

标记已使用和未使用的对象

标记已使用和未使用的对象

2、扫描/删除对象

有两种不同的扫描和删除算法:

简单删除(标记清除):它的过程很简单,我们只需要删除未被引用的对象即可。但是,后续给新对象分配内存就会变得很困难了,因为可用空间被分割成了一块块碎片。

标记清除的过程

标记清除的过程

删除压缩(标记整理):除了会删除未被引用的对象,我们还会压缩被引用的对象(未被删除的对象)。这样以来,新对象的内存分配就相对容易了,并且内存分配的效率也有了提升。

标记整理的过程

标记整理的过程

什么是分代垃圾回收,为什么需要它?

正如我们在“扫描删除”模型中所看到的,一旦对象不断增长,我们就很难扫描所有未使用的对象以回收内存。不过,有一项实验性研究指出,在程序执行期间创建的大多数对象,它们的存活时间都很短。

既然大多数对象的存活时间都很短,那么我们就可以利用这个事实,从而提升垃圾回收的效率。该怎么做呢?首先,JVM 将内存划分为不同的“代”。接着,它将所有的对象都分类到这些内存“代”中,然后对这些“代”分别执行垃圾回收。这就是“分代垃圾回收”。

堆内存的“代”和分代垃圾回收过程

为了提升垃圾回收中的“标记清除”的效率,JVM 将对内存划分成以下三个“代”:

  • 新生代Young Generation
  • 老年代Old Generation
  • 永久代Permanent Generation

Hotspot 堆内存结构

Hotspot 堆内存结构

下面我将介绍每个“代”及其主要特征。

新生代

所有创建不久的对象都存放在这里。新生代被进一步分为以下两个区域:

  1. 伊甸区Eden:所有新创建的对象都在此处分配内存。
  2. 幸存者区Survivor,分为 S0 和 S1:经历过一次垃圾回收后,仍然存活的对象会被移动到两个幸存者区中的一个。

对象分配

对象分配

在新生代发生的分代垃圾回收被称为 “次要回收Minor GC”(LCTT 译注:也称为“新生代回收Young GC”)。Minor GC 过程中的每个阶段都是“停止世界Stop The World”(STW)的,这会导致其他应用程序暂停运行,直到垃圾回收结束。这也是次要回收更快的原因。

一句话总结:伊甸区存放了所有新创建的对象,当它的可用空间被耗尽,第一次垃圾回收就会被触发。

填充伊甸区

填充伊甸区

次要回收:在该垃圾回收过程中,所有存活和死亡的对象都会被做上标记。其中,存活对象会被移动到 S0 幸存者区。当所有存活对象都被移动到了 S0,未被引用的对象就会被删除。

拷贝被引用的对象

拷贝被引用的对象

S0 中的对象年龄为 1,因为它们挺过了一次次要回收。此时,伊甸区和 S1 都是空的。

每当完成清理后,伊甸区就会再次接受新的存活对象。随着时间的推移,伊甸区和 S0 中的某些对象被宣判死亡(不再被引用),并且伊甸区的可用空间也再次耗尽(填满了),那么次要回收 又将再次被触发。

对象年龄增长

对象年龄增长

这一次,伊甸区和 S0 中的死亡和存活的对象会被做上标记。其中,伊甸区的存活对象会被移动到 S1,并且年龄增加至 1。S0 中的存活对象也会被移动到 S1,并且年龄增加至 2(因为它们挺过了两次次要回收)。此时,伊甸区和 S0 又是空的了。每次次要回收之后,伊甸区和两个幸存者区中的一个都会是空的。

新对象总是在伊甸区被创建,周而复始。当下一次垃圾回收发生时,伊甸区和 S1 都会被清理,它们中的存活对象会被移动到 S0 区。每次次要回收之后,这两个幸存者区(S0 和 S1)就会交换一次。

额外年龄增长

额外年龄增长

这个过程会一直进行下去,直到某个存活对象的年龄达到了某个阈值,然后它就会被移动到一个叫做“老年代”的地方,这是通过一个叫做“晋升”的过程来完成的。

使用 -Xmn 选项可以设置新生代的大小。

老年代

这个区域存放着那些挺过了许多次次要回收,并且达到了某个年龄阈值的对象。

晋升

晋升

在上面这个示例图表中,晋升的年龄阈值为 8。在老年代发生的垃圾回收被称为 “主要回收Major GC”。(LCTT 译注:也被称为“全回收Full GC”)

使用 -Xms 和 -Xmx 选项可以分别设置堆内存大小的初始值和最大值。(LCTT 译注:结合上面的 -Xmn 选项,就可以间接设置老年代的大小了。)

永久代

永久代存放着一些元数据,它们与应用程序、Java 标准环境以及 JVM 自用的库类及其方法相关。JVM 会在运行时,用到了什么类和方法,就会填充相应的数据。当 JVM 发现有未使用的类,就会卸载或是回收它们,从而为正在使用的类腾出空间。

使用 -XX:PermGen 和 -XX:MaxPerGen 选项可以分别设置永久代大小的初始值和最大值。

元空间

Java 8 引入了元空间Metaspace,并用它替换了永久代。这么做的好处是自动调整大小,避免了 内存不足OutOfMemory(OOM)错误。

总结

本文讨论了各种不同的 JVM 内存“代”,以及它们是如何在分代垃圾回收算法中起作用的。对于程序员来说,掌握 Java 的内存管理机制并不是必须的,但它能够帮助你更好地理解 JVM 处理程序中的变量和类实例的方式。这种理解使你能够规划和排除代码故障,并理解特定平台固有的潜在限制。

责任编辑:未丽燕 来源: Linux中国
相关推荐

2022-03-21 11:33:11

JVM垃圾回收器垃圾回收算法

2022-01-20 10:34:49

JVM垃圾回收算法

2017-08-04 10:53:30

回收算法JVM垃圾回收器

2009-12-30 10:14:29

JVM垃圾回收

2023-08-08 10:29:55

JVM优化垃圾回收

2010-09-25 15:19:01

2021-11-05 15:23:20

JVM回收算法

2009-12-25 16:15:31

JVM垃圾回收算法

2010-09-25 15:33:19

JVM垃圾回收

2024-03-11 16:27:02

垃圾回收器JVM

2010-09-26 16:42:04

JVM内存组成JVM垃圾回收

2010-09-27 09:01:26

JVM分代垃圾回收

2010-09-25 15:26:12

JVM垃圾回收

2010-09-16 15:10:24

JVM垃圾回收机制

2017-04-25 14:39:55

JVM内存Java

2022-06-07 07:10:40

MinorGCMajorGCFullGC

2009-04-28 13:48:09

2011-12-05 12:51:58

JVMJava

2010-01-06 09:28:08

JVM分代垃圾回收

2012-01-09 17:06:16

JavaJVM
点赞
收藏

51CTO技术栈公众号