跳到主要内容

Java 垃圾回收基础:GC Roots、分代模型、收集器与日志分析

· 阅读需 21 分钟
Apache王也道长
软件开发者与技术作者

目的​

本教程介绍垃圾收集如何与 Hotspot JVM 配合使用的基础知识。了解垃圾收集器的功能后,了解如何使用 Visual VM 监控垃圾收集过程。最后,了解 Java SE 7 Hotspot JVM 中可用的垃圾收集器。 理解 GC 应先区分对象是否可达、何时触发回收以及收集器如何移动对象。实际优化的第一步是采集日志和指标,而不是盲目增大堆内存。

简介​

本 OBE 涵盖了 Java 中的 Java 虚拟机 (JVM) 垃圾收集 (GC) 的基础知识。在 OBE 的第一部分中,提供了 JVM 的概述以及垃圾收集和性能的介绍。接下来的学生将获得关于垃圾收集如何在 JVM 中工作的分步指南。接下来为学习者提供了一个动手活动,以试用 Java JDK 中提供的一些监控工具,并将他们刚刚学到的关于垃圾收集的知识付诸实践。最后,提供了一节介绍 Hotspot JVM 中可用的垃圾收集方案选项。

前提条件​

Java技术与JVM

Java概览​

Java 是 Sun Microsystems 于 1995 年首次发布的一种编程语言和计算平台。它是支持 Java 程序(包括实用程序、游戏和业务应用程序)的底层技术。 Java 在全球超过 8.5 亿台个人计算机和全球数十亿台设备上运行,包括移动设备和电视设备。 Java 由许多关键组件组成,它们作为一个整体创建了 Java 平台

JRE (Java Runtime Edition)​

下载 Java 时,您将获得 Java 运行时环境 (JRE)。 JRE 由 Java 虚拟机 (JVM)、Java 平台核心类和支持 Java 平台库组成。这三个都是在您的计算机上运行 Java 应用程序所必需的。在 Java 7 中,Java 应用程序作为操作系统中的桌面应用程序运行,作为桌面应用程序运行,但使用 Java Web Start 从 Web 安装,或者作为浏览器中的 Web 嵌入式应用程序(使用 JavaFX)运行。

Java编程语言 (Java Programming Language)​

Java 是一种面向对象的编程语言,包括以下特性。

  • 平台独立性——Java 应用程序被编译成字节码,存储在class文件中并加载到 JVM 中。由于应用程序在 JVM 中运行,因此它们可以在许多不同的操作系统和设备上运行。
  • 面向对象——Java 是一种面向对象的语言,它采用了 C 和 C++ 的许多特性并对其进行了改进。
  • 自动垃圾收集 - Java 自动分配和释放内存,因此程序不会承担该任务。
  • 丰富的标准库 - Java 包含大量可用于执行诸如输入/输出、网络和日期操作等任务的预制对象。

JDK (Java Development Kit)​

Java Development Kit (JDK) 是一组用于开发 Java 应用程序的工具。使用 JDK,您可以编译用 Java 编程语言编写的程序并在 JVM 中运行它们。此外,JDK 提供了用于打包和分发应用程序的工具。

JDK 和 JRE 共享 Java 应用程序编程接口 (Java API)。 Java API 是开发人员用来创建 Java 应用程序的预打包库的集合。 Java API 通过提供工具来完成许多常见的编程任务,包括字符串操作、日期/时间处理、网络和实现数据结构(例如,列表、映射、堆栈和队列),从而使开发更容易。

JAVA虚拟机(Java Virtual Machine)​

Java 虚拟机 (JVM) 是一种抽象计算机。 JVM 是一个程序,对于编写在其中执行的程序来说,它看起来就像一台机器。通过这种方式,Java 程序被写入同一组接口和库。特定操作系统的每个 JVM 实现都将 Java 编程指令转换为在本地操作系统上运行的指令和命令。这样,Java 程序就实现了平台独立性。

Java 虚拟机的第一个原型实现由 Sun Microsystems, Inc. 完成,它在类似于当代个人数字助理 (PDA) 的手持设备托管的软件中模拟 Java 虚拟机指令集。 Oracle 当前的实现在移动、桌面和服务器设备上模拟 Java 虚拟机,但 Java 虚拟机不采用任何特定的实现技术、主机硬件或主机操作系统。它本身不是解释的,但也可以通过将其指令集编译为硅 CPU 的指令集来实现。它也可以用微码或直接用硅实现。

Java 虚拟机对 Java 编程语言一无所知,只知道一种特定的二进制格式,即class文件格式。class文件包含 Java 虚拟机指令(或字节码)和符号表,以及其他辅助信息。

为了安全起见,Java 虚拟机对类文件中的代码施加了强大的句法和结构约束。但是,任何具有可以用有效类文件表示的功能的语言都可以由 Java 虚拟机托管。被一个普遍可用的、独立于机器的平台所吸引,其他语言的实现者可以将 Java 虚拟机作为他们语言的交付工具。

探索JVM架构

Hotspot 架构​

HotSpot JVM 的架构支持强大的特性和能力基础,并支持实现高性能和大规模可扩展性的能力。例如,HotSpot JVM JIT 编译器生成动态优化。换句话说,它们在 Java 应用程序运行时做出优化决策,并生成针对底层系统架构的高性能本机机器指令。此外,通过其运行时环境和多线程垃圾收集器的成熟演变和持续工程,HotSpot JVM 即使在最大的可用计算机系统上也能产生高可扩展性。 HotSpot 架构 JVM 的主要组件包括类加载器、运行时数据区和执行引擎。

关键Hotspot组件​

下图中突出显示了与性能相关的 JVM 关键组件。 image.png

JVM 的三个组件在调优性能时需要重点关注。堆是存储对象数据的地方。然后,该区域由启动时选择的垃圾收集器管理。大多数调整选项都与堆大小和为您的情况选择最合适的垃圾收集器有关。 JIT 编译器对性能也有很大影响,但很少需要使用较新版本的 JVM 进行调整。

性能基础

通常,在调整 Java 应用程序时,重点是两个主要目标之一:响应性或吞吐量。随着教程的进行,我们将参考这些概念。

响应能力 (Responsiveness)​

响应性是指应用程序或系统响应请求的数据的速度。示例包括:

  • 桌面 UI 响应事件的速度
  • 网站返回页面的速度
  • 返回数据库查询的速度

对于专注于响应能力的应用程序,大的暂停时间是不可接受的。重点是在短时间内做出反应

吞吐量 (Throughput)​

吞吐量侧重于在特定时间段内最大化应用程序的工作量。如何衡量吞吐量的示例包括:

  • 在给定时间内完成的事务数。
  • 批处理程序在一小时内可以完成的作业数。
  • 一个小时内可以完成的数据库查询数。

对于专注于吞吐量的应用程序来说,较长的暂停时间是可以接受的。由于高吞吐量应用程序在较长时间内专注于基准测试,因此无需考虑快速响应时间。

描述垃圾收集

什么是自动垃圾收集?​

自动垃圾回收是查看堆内存,识别哪些对象正在使用,哪些没有,并删除未使用的对象的过程。使用中的对象或引用的对象意味着程序的某些部分仍然维护指向该对象的指针。程序的任何部分都不再引用未使用的对象或未引用的对象。因此可以回收未引用对象使用的内存。

在像 C 这样的编程语言中,分配和释放内存是一个手动过程。在 Java 中,释放内存的过程由垃圾收集器自动处理。基本过程可以描述如下。

第 1 步:标记​

该过程的第一步称为标记。这是垃圾收集器识别哪些内存正在使用,哪些没有使用的地方。 Marking

引用的对象以蓝色显示。未引用的对象以金色显示。在标记阶段扫描所有对象以做出此决定。如果必须扫描系统中的所有对象,这可能是一个非常耗时的过程。

第 2 步:正常删除​

正常删除删除未引用的对象,留下引用的对象和指向可用空间的指针。 Normal Deletion

内存分配器保存对可以分配新对象的可用空间块的引用。

步骤 2a:使用压缩删除​

为了进一步提高性能,除了删除未引用的对象外,还可以压缩剩余的引用对象。通过将引用的对象一起移动,这使得新的内存分配更加容易和快捷。 Deletion with Compacting

为什么要进行分代垃圾收集?​

如前所述,必须标记和压缩 JVM 中的所有对象是低效的。随着越来越多的对象被分配,对象列表越来越多,导致垃圾收集时间越来越长。然而,应用的经验分析表明,大多数对象都是短暂的。

这是此类数据的示例。 Y 轴显示分配的字节数,X 访问显示随时间分配的字节数。 Byte allocated

如您所见,随着时间的推移,分配的对象越来越少。事实上,大多数对象的寿命都很短,如图左侧的较高值所示。

JVM堆分代​

从对象分配行为中学到的信息可以用来增强 JVM 的性能。因此,堆被分解成更小的部分或代。堆部分是:年轻代(Young Generation), 老年代(Old or Tenured Generation), 和永久代(Permanent Generation) Hotspot Heap Structure

年轻代是所有新对象被分配和老化的地方。当年轻代填满时,这会导致Minor GC。假设对象死亡率很高,可以优化次要集合。一个充满死亡对象的年轻代很快就会被收集起来。一些幸存的对象被老化并最终移动到老年代。

Stop the World Event——所有Minor GC都是“STW”事件。这意味着所有应用程序线程都将停止,直到操作完成。Minor GC始终是 STW 事件。

老年代用于存储长期存活的对象。通常,为年轻代对象设置一个阈值,当满足该年龄时,该对象将移动到老年代。最终需要收集老年代。此事件称为Major GC。

Major GC也是 STW 事件。通常一个Major GC要慢得多,因为它涉及所有活动对象。因此,对于响应式应用程序,应尽量减少Major GC。另请注意,Major GC的 STW 事件的长度受用于老年代空间的垃圾收集器类型的影响。

永久代包含 JVM 所需的元数据,用于描述应用程序中使用的类和方法。 JVM 在运行时根据应用程序使用的类填充永久代。此外,Java SE 库类和方法可能存储在这里。

如果 JVM 发现不再需要这些类并且其他类可能需要空间,则这些类可能会被收集(卸载)。永久代包含在完整的垃圾收集中。

分代垃圾收集过程

现在您了解了为什么堆被分成不同的代,是时候看看这些空间究竟是如何相互作用的了。下面的图片介绍了 JVM 中的对象分配和老化过程。

  1. 首先,任何新对象都被分配到伊甸园空间。两个幸存者空间一开始都是空的。 Object Allocation

  2. 当Eden空间填满时,会触发minor GC。 Filling the Eden Space

  3. 被引用的对象被移动到第一个Survivor空间。当Eden空间被清除时,未引用的对象将被删除。 Copying Referenced Objects

  4. 在下一次minor GC 中,同样的事情发生在Eden空间。未引用的对象被删除,被引用的对象被移动到survivor空间。然而,在这种情况下,它们被移动到第二个Survivor空间(S1)。此外,来自第一个survivor空间 (S0) 上的最后一次minor GC 的对象的年龄增加并被移动到 S1。一旦所有幸存的对象都被移动到 S1,S0 和 eden 都将被清除。请注意,我们现在在survivor空间中有不同年龄的对象。 Object Aging

5.在下一次minor GC 中,重复相同的过程。然而,这一次survivor空间切换。被引用的对象被移动到 S0。幸存的物体会老化。 Eden 和 S1 被清除。 Additional Aging

  1. 在一次 minor GC 之后,当老化对象达到某个年龄阈值(本例中为 8)时,它们会从年轻代提升到老年代。 Promotion

  2. 随着 Minor GC 的不断发生,对象将继续被提升到老年代空间。 Promotion 2

  3. 所以这几乎涵盖了年轻一代的整个过程。最终,将在老年代执行一次major GC,清理并压缩该空间。 GC Process Summary

进行自己的观察

概览​

您已经使用一系列图片看到了垃圾收集过程。现在是时候现场体验和探索这个过程了。在本活动中,您将运行 Java 应用程序并使用 Visual VM 分析垃圾收集过程。 Visual VM 程序包含在 JDK 中,允许开发人员监控正在运行的 JVM 的各个方面。

动手活动​

第 1 步:初始设置​

下载demo

第 2 步:启动演示应用程序​

安装 Java JDK 和演示后,您现在可以运行将在本活动中分析的演示应用程序。对于此示例,演示安装在 c:\javademos 中。

演示应用程序是一个 2D 图形演示。要执行它,请键入: java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar 2D Demo

第 3 步:启动 VisualVM​

要启动 Visual VM,请使用 C:\Program Files\Java\jdk1.7.0_XX\bin 目录中的 jvisualvm 可执行文件。我建议您将此目录添加到您的路径中。在 Windows 7 上,步骤如下:Start -> Computer -> System Properties -> Advanced System Settings -> Environment Variables.然后在系统变量下选择路径并添加 C:\Program Files\Java\jdk1.7.0_XX\bin;到路径(其中 XX 是 JDK 的当前版本)。单击确定几次,然后关闭对话框。

完成此操作后,您应该能够打开命令提示符窗口并键入 jvisualvm。

Visual VM 第一次运行时,它会对您的系统进行校准。您将看到如下消息:

然后是这样的消息:

最后加载主用户界面。

从这里,我们可以监控正在运行的 JVM。但是,对于这个活动,我们需要添加 VisualGC 插件。

第 4 步:安装 Visual GC​

Visual VM 的 Visual GC 插件提供了 JVM 中垃圾收集器活动的图形表示。它是免费的,可以按照以下步骤添加到您的安装中。

  1. 要安装 Visual GC,请从菜单中选择Tools --> Plugins。您应该得到以下对话框。 2.选择 Visual GC,然后选择安装按钮。 3.插件安装程序启动。点击下一步。
  2. 阅读并接受许可协议。单击安装。现在将安装插件。 5.该插件现已安装。单击完成以完成安装。 6.单击关闭按钮关闭插件对话框。 安装了 VisualGC。现在我们可以检查 Java2Demo 以了解正在发生的事情。

第 5 步:分析 Java2Demo​

是时候分析我们在第 2 步中启动的应用程序了。

  1. 首先,在 Applications 选项卡的 Local applications 列表中右键单击 Java2Demo 应用程序。选择打开。
  2. 应用程序被加载到 Visual VM 中。请注意,界面右侧加载了许多选项卡。单击可视 GC 选项卡。 3.Visual GC 选项卡显示垃圾收集器中正在进行的所有活动。您可以看到 Eden 空间被填满,数据在两个survivor空间 S1 和 S0 之间移动。此外,右侧显示了与垃圾收集器相关的图表和统计信息。 随意尝试其他选项卡并查看有关 JVM 的信息。此外,您可以尝试更改示例应用程序中显示的字符串或图像的数量,以了解这对垃圾收集过程有何影响。

Java 垃圾收集器

您现在了解了垃圾收集的基础知识,并且已经观察了垃圾收集器在示例应用程序中的作用。在本节中,您将了解可用于 Java 的垃圾收集器以及选择它们所需的命令行开关。

常见的堆相关开关​

Java 可以使用许多不同的命令行开关。本节介绍了本 OBE 中也使用的一些更常用的开关。

开关描述
-Xms设置 JVM 启动时的初始堆大小。
-Xmx设置最大堆大小。
-Xmn设置年轻代的大小。
-XX:PermSize设置永久代的起始大小。
-XX:MaxPermSize设置永久代的最大大小。

串行收集器(Serial GC)​

串行收集器是 Java SE 5 和 6 中客户端样式机器的默认设置。使用串行收集器,次要和主要垃圾收集都是串行完成的(使用单个虚拟 CPU)。此外,它使用了一种标记紧凑的收集方法。此方法将较旧的内存移动到堆的开头,以便在堆的末尾将新的内存分配变成一个连续的内存块。这种内存压缩可以更快地将新的内存块分配给堆。

使用案例​

对于大多数对暂停时间要求不高且在客户端式机器上运行的应用程序来说,串行 GC 是首选的垃圾收集器。它仅利用单个虚拟处理器进行垃圾收集工作(因此,它的名字)。尽管如此,在今天的硬件上,串行 GC 可以有效地管理大量具有几百 MB Java 堆的重要应用程序,并且在最坏情况下的暂停时间相对较短(完全垃圾收集大约需要几秒钟)。

Serial GC 的另一个流行用途是在同一台机器上运行大量 JVM 的环境中(在某些情况下,JVM 比可用处理器多!)。在这样的环境中,当 JVM 进行垃圾收集时,最好只使用一个处理器来最大限度地减少对剩余 JVM 的干扰,即使垃圾收集可能会持续更长时间。串行 GC 非常适合这种权衡。

最后,随着具有最小内存和少数内核的嵌入式硬件的普及,串行 GC 可能会卷土重来。

命令行开关​

要启用串行收集器,请使用:-XX:+UseSerialGC 下面是启动 Java2Demo 的示例命令行: java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

并行垃圾收集器(Parallel GC)​

并行垃圾收集器使用多个线程来执行年轻代垃圾收集。默认情况下,在具有 N 个 CPU 的主机上,并行垃圾收集器在收集中使用 N 个垃圾收集器线程。垃圾收集器线程的数量可以通过命令行选项控制:

-XX:ParallelGCThreads=<所需数量>

在具有单个 CPU 的主机上,即使已请求并行垃圾收集器,也会使用默认垃圾收集器。在具有两个 CPU 的主机上,并行垃圾收集器的性能通常与默认垃圾收集器一样好,并且可以预期在具有两个以上 CPU 的主机上减少年轻代垃圾收集器的暂停时间。 Parallel GC 有两种风格。

使用案例​

并行收集器也称为吞吐量收集器。因为它可以使用多个 CPU 来加快应用程序的吞吐量。当需要完成大量工作并且可以接受长时间的停顿时,应该使用此收集器。例如,打印报告或账单或执行大量数据库查询等批处理。

-XX:+UseParallelGC​

使用此命令行选项,您将获得一个多线程年轻代收集器和一个单线程老年代收集器。该选项还对老年代进行单线程压缩。

下面是启动 Java2Demo 的示例命令行: java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

-XX:+UseParallelOldGC​

使用 -XX:+UseParallelOldGC 选项,GC 既是多线程年轻代收集器,也是多线程老年代收集器。它也是一个多线程压缩收集器。 HotSpot 只在老年代进行压缩。 HotSpot 中的年轻代被认为是一个副本收集器;因此,不需要压缩。

压缩描述了以对象之间没有孔的方式移动对象的行为。垃圾收集扫描后,活动对象之间可能会留下空洞。压实移动对象,以便没有剩余的孔。垃圾收集器可能是非压缩收集器。因此,并行收集器和并行压缩收集器之间的区别可能是后者在垃圾收集扫描后压缩空间。前者不会。

下面是启动 Java2Demo 的示例命令行: java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseParallelOldGC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

并发标记扫描收集器 (CMS)​

Concurrent Mark Sweep (CMS) 收集器(也称为并发低暂停收集器)收集老年代。它试图通过与应用程序线程同时进行大部分垃圾收集工作来最小化由于垃圾收集而导致的暂停。通常并发低暂停收集器不会复制或压缩活动对象。垃圾收集是在不移动活动对象的情况下完成的。如果碎片成为问题,请分配更大的堆。

注意:年轻代的 CMS 收集器使用与并行收集器相同的算法。

使用案例​

CMS 收集器应该用于需要低暂停时间并且可以与垃圾收集器共享资源的应用程序。示例包括响应事件的桌面 UI 应用程序、响应请求的 Web 服务器或响应查询的数据库。

命令行开关​

要启用 CMS 收集器,请使用: -XX:+UseConcMarkSweepGC 并设置线程数使用: -XX:ParallelCMSThreads=<n>

下面是启动 Java2Demo 的示例命令行: java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseConcMarkSweepGC -XX:ParallelCMSThreads=2 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

G1 垃圾收集器​

Garbage First 或 G1 垃圾收集器在 Java 7 中可用,旨在长期替代 CMS 收集器。 G1 收集器是一个并行、并发和增量压缩的低暂停垃圾收集器,它的布局与前面描述的其他垃圾收集器完全不同。但是,详细讨论超出了本 OBE 的范围。

命令行开关​

要启用 G1 收集器,请使用: -XX:+UseG1GC

下面是启动 Java2Demo 的示例命令行: java -Xmx12m -Xms3m -XX:+UseG1GC -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

总结

在本 OBE 中,您将了解 Java JVM 上的垃圾收集系统。首先,您了解了堆和垃圾收集器如何成为任何 Java JVM 的关键部分。自动垃圾收集是使用分代垃圾收集方法完成的。一旦你了解了这个过程,你就可以使用 Visual VM 工具观察它。最后,您查看了 Java Hospot JVM 中可用的垃圾收集器。

在本教程中,您学习了:

  • Java JVM 的组件
  • 自动垃圾收集的工作原理
  • 分代垃圾收集过程
  • 如何使用 Visual VM 监控 JVM
  • JVM 上可用的垃圾收集器类型

相关阅读​

进一步了解现代低停顿收集器,可以阅读 Java G1 垃圾收集器详解;需要系统学习类加载、字节码和运行时数据区,可以阅读 深入理解 JVM。

本文阅读量:--

总访问量 -- · 访客数 --