在上一章中,我们研究了 Java Shell(JShell)、Java 的 读取求值打印循环(REPL)命令行工具。我们从介绍该工具的信息开始,仔细研究了 REPL 概念。我们花了大量时间来检查 JShell 命令和命令行选项。我们的报道包括反馈模式的实用指南、素材列表和 Shell 中的编辑。我们还获得了使用脚本的经验。
在本章中,我们将深入了解垃圾收集以及如何在 Java 中处理它。我们将从垃圾收集的概述开始,然后看看 Java9 之前的领域中的细节。有了这些基本信息,我们将研究 Java9 平台中特定的垃圾收集更改。最后,我们将研究一些即使在 Java11 之后仍然存在的垃圾收集问题。
本章包括以下主题:
- 垃圾收集概述
- Java9 之前的垃圾收集模式
- 用新的 Java 平台收集垃圾
- 长期存在的问题
本章主要介绍 Java11。Java 平台的标准版(SE)可以从 Oracle 的官方下载站点下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
垃圾收集是 Java 中用来释放未使用内存的机制。本质上,当一个对象被创建时,内存空间被分配并专用于该对象,直到它不再有任何指向它的引用为止。此时,系统将释放内存
Java 为我们自动执行这种垃圾收集,这可能会导致对内存使用的关注不足,以及在内存管理和系统性能方面的糟糕编程实践。Java 的垃圾收集被认为是一种自动内存管理模式,因为程序员不必将对象指定为随时可用取消分配。垃圾收集在低优先级线程上运行,并且,正如您将在本章后面阅读的,具有可变的执行周期。
在垃圾收集概述中,我们将介绍以下概念:
- 对象生命周期
- 垃圾收集算法
- 垃圾收集选项
- 与垃圾收集相关的 Java 方法
我们将在接下来的章节中逐一介绍这些概念。
为了完全理解 Java 的垃圾收集,我们需要了解对象的整个生命周期。因为垃圾收集的核心在 Java 中是自动的,所以将术语垃圾收集和内存管理视为对象生命周期的假定组件并不少见
我们将从对象创建开始回顾对象生命周期。
对象被声明和创建。当我们编写一个对象声明或声明一个对象时,我们声明的是一个名称或标识符,这样我们就可以引用一个对象。例如,下面的代码行将myObjectName
声明为CapuchinMonkey
类型的对象的名称。此时,没有创建对象,也没有为其分配内存:
CapuchinMonkey myObjectName;
我们使用new
关键字来创建一个对象。下面的示例说明如何调用new
操作来创建对象。此操作导致:
myObjectName = new CapuchinMonkey();
当然,我们可以使用CapuchinMonkey myObjectName = new CapuchinMonkey();
来组合声明和创建语句,而不是使用CapuchinMonkey myObjectName;
和myObjectName = new CapuchinMonkey();
,在前面的示例中,它们是分开的。
当一个对象被创建时,会为存储该对象分配一个特定的内存量,分配的内存量会因架构和 JVM 的不同而不同。
接下来,我们将看一个对象的中期寿命。
对象被创建,Java 为存储该对象分配系统内存。如果对象未被使用,分配给它的内存将被视为浪费。这是我们要避免的。即使对于小型应用,这种类型的内存浪费也会导致性能低下,甚至出现内存不足的问题
我们的目标是释放或释放内存,即我们不再需要的任何先前分配的内存。幸运的是,对于 Java,有一种机制可以处理这个问题。这就是所谓的垃圾收集
当一个对象(比如我们的myObjectName
示例)不再有任何指向它的引用时,系统将重新分配相关的内存。
Java 让垃圾收集器在代码的暗处运行(通常是一个低优先级线程)并释放当前分配给未引用对象的内存的想法很有吸引力。那么,这是怎么回事?垃圾收集系统监视对象,并在可行的情况下统计每个对象的引用数
如果没有对对象的引用,则无法使用当前运行的代码访问该对象,因此释放相关内存是非常有意义的。
术语内存泄漏是指丢失或不正确释放的小内存块。Java 的垃圾收集可以避免这些泄漏。
JVM 可以使用几种垃圾收集算法或类型。在本节中,我们将介绍以下垃圾收集算法:
- 标记和扫描
- 并发标记扫描(CMS)垃圾收集
- 串行垃圾收集
- 并行垃圾收集
- G1 垃圾收集
Java 的初始垃圾收集算法标记清除使用了一个简单的两步过程:
-
第一步,标记,是遍历所有具有可访问引用的对象,将这些对象标记为活动对象
-
第二步,扫描,包括扫描海洋中任何没有标记的对象
正如您可以很容易地确定的那样,标记和扫描算法似乎很有效,但由于这种方法的两步性质,它可能不是很有效。这最终导致了一个 Java 垃圾收集系统,大大提高了效率。
用于垃圾收集的 CMS 算法使用多个线程扫描堆内存。与“标记并扫描”方法类似,它标记要删除的对象,然后进行扫描以实际删除这些对象。这种垃圾收集方法本质上是一种升级的标记和扫描方法。它进行了修改,以利用更快的系统和性能增强。
要为应用手动调用 CMS 垃圾收集算法,请使用以下命令行选项:
-XX:+UseConcMarkSweepGC
如果要使用 CMS 垃圾收集算法并指定要使用的线程数,可以使用以下命令行选项。在下面的示例中,我们告诉 Java 平台使用带有八个线程的 CMS 垃圾收集算法:
-XX:ParallelCMSThreads=8
Java 的串行垃圾收集在一个线程上工作。执行时,它冻结所有其他线程,直到垃圾收集操作结束。由于串行垃圾收集的线程冻结性质,它只适用于非常小的程序
要手动调用应用的串行垃圾收集算法,请使用以下命令行选项:
-XX:+UseSerialGC
在 Java8 和更早版本中,并行垃圾收集算法是默认的垃圾收集器。它使用多个线程,但冻结应用中的所有非垃圾收集线程,直到垃圾收集函数完成,就像串行垃圾收集算法一样。
G1 垃圾收集算法是为处理大内存堆而创建的。这种方法包括将内存堆分割成多个区域。使用 G1 算法的垃圾收集与每个堆区域并行进行
G1 算法的另一部分是当内存被释放时,堆空间被压缩。不幸的是,压实操作是使用停止世界方法进行的
G1 垃圾收集算法还根据要收集的垃圾最多的区域来确定区域的优先级。
G1 名称指垃圾优先。
要为应用手动调用 G1 垃圾收集算法,请使用以下命令行选项:
-XX:+UseG1GC
以下是 JVM 大小调整选项的列表:
大小说明 | JVM 选项标志 |
---|---|
此标志建立初始堆大小(年轻空间和长期空间的组合)。 | XX:InitialHeapSize=3g |
此标志建立最大堆大小(年轻空间和长期空间的组合)。 | -XX:MaxHeapSize=3g |
此标志建立初始和最大堆大小(年轻空间和长期空间的组合)。 | -Xms2048m -Xmx3g |
这个标志建立了年轻空间的初始大小。 | -XX:NewSize=128m |
此标志确定了年轻空间的最大大小。 | -XX:MaxNewSize=128m |
此标志确定空间大小。它使用了年轻人和终身监禁者的比例。在右边的示例标志中,3 表示年轻空间将比终身空间小三倍。 |
-XX:NewRation=3 |
此标志将单个幸存者空间的大小确定为伊甸园空间大小的一部分。 | -XX:SurvivorRatio=15 |
此标志确定永久空间的初始大小。 | -XX:PermSize=512m |
此标志确定永久空间的最大大小。 | -XX:MaxPermSize=512m |
此标志确定每个线程专用的栈区域的大小(以字节为单位)。 | -Xss512k |
此标志确定每个线程专用的栈区域的大小(以 KB 为单位)。 | -XX:ThreadStackSize=512 |
此标志确定 JVM 可用的堆外内存的最大大小。 | -XX:MaxDirectMemorySize=3g |
以下是新生代垃圾收集选项的列表:
新生代垃圾收集调优选项 | 标志 |
---|---|
设置保留阈值(从年轻空间升级到保留空间之前集合的阈值) | -XX:Initial\TenuringThreshold=16 |
设置上限寿命阈值。 | -XX:Max\TenuringThreshold=30 |
设置空间中允许的最大对象大小。如果一个对象大于最大大小,它将被分配到终身空间和绕过年轻空间。 | -XX:Pretenure\SizeThreshold=3m |
用于将年轻集合中幸存的所有年轻对象提升到终身空间。 | -XX:+AlwaysTenure |
使用此标记,只要幸存者空间有足够的空间,年轻空间中的对象就永远不会升级到终身空间。 | -XX:+NeverTenure |
我们可以指出我们想要在年轻空间中使用线程本地分配块。这在默认情况下是启用的。 | -XX:+UseTLAB |
切换此选项以允许 JVM 自适应地调整线程的 TLAB(简称线程本地分配块)。 | -XX:+ResizeTLAB |
设置线程的 TLAB 的初始大小。 | -XX:TLABSize=2m |
设置 TLAB 的最小允许大小。 | -XX:MinTLABSize=128k |
以下是 CMS 调整选项列表:
CMS 调优选项 | 标志 |
---|---|
指示您希望仅使用占用率作为启动 CMS 收集操作的标准。 | -XX:+UseCMSInitiating\OccupancyOnly |
设置 CMS 生成占用率百分比以开始 CMS 收集周期。如果您指示一个负数,那么您就告诉 JVM 您要使用CMSTriggerRatio 。 |
-XX:CMSInitiating\OccupancyFraction=70 |
设置要启动 CMS 集合以进行引导集合统计的 CMS 生成占用百分比。 | -XX:CMSBootstrap\Occupancy=10 |
这是在 CMS 循环开始之前分配的 CMS 生成中MinHeapFreeRatio 的百分比。 |
-XX:CMSTriggerRatio=70 |
设置在开始 CMS 收集循环之前分配的 CMS 永久生成中MinHeapFreeRatio 的百分比。 |
-XX:CMSTriggerPermRatio=90 |
这是触发 CMS 集合后的等待时间。使用参数指定允许 CMS 等待年轻集合的时间。 | -XX:CMSWaitDuration=2000 |
启用平行备注。 | -XX:+CMSParallel\RemarkEnabled |
启用幸存者空间的平行备注。 | -XX:+CMSParallel\SurvivorRemarkEnabled |
您可以使用此命令在备注阶段之前强制年轻的集合。 | -XX:+CMSScavengeBeforeRemark |
如果使用的 Eden 低于阈值,则使用此选项可防止出现计划注释。 | -XX:+CMSScheduleRemark\EdenSizeThreshold |
设置您希望 CMS 尝试和安排备注暂停的 Eden 占用百分比。 | -XX:CMSScheduleRemark\EdenPenetration=20 |
至少在新生代的入住率达到您想要安排备注的 1/4(在我们右边的示例中)之前,您就要在这里开始对 Eden 顶部进行采样。 | -XX:CMSScheduleRemark\SamplingRatio=4 |
备注后可选择variant=1 或variant=2 验证。 |
-XX:CMSRemarkVerifyVariant=1 |
选择使用并行算法进行年轻空间的收集。 | -XX:+UseParNewGC |
允许对并发阶段使用多个线程。 | -XX:+CMSConcurrentMTEnabled |
设置用于并发阶段的并行线程数。 | -XX:ConcGCThreads=2 |
设置要用于停止世界阶段的并行线程数。 | -XX:ParallelGCThreads=2 |
您可以启用增量 CMS(iCMS)模式。 | -XX:+CMSIncrementalMode |
如果未启用,CMS 将不会清理永久空间。 | -XX:+CMSClassUnloadingEnabled |
这允许System.gc() 触发并发收集,而不是整个垃圾收集周期。 |
-XX:+ExplicitGCInvokes\Concurrent |
这允许System.gc() 触发永久空间的并发收集。 |
‑XX:+ExplicitGCInvokes\ConcurrentAndUnloadsClasses |
iCMS 模式适用于 CPU 数量少的服务器。 不应在现代硬件上使用它。
以下是一些杂项垃圾收集选项:
其他垃圾收集选项 | 标志 |
---|---|
这将导致 JVM 忽略应用的任何System.gc() 方法调用。 |
-XX:+DisableExplicitGC |
这是堆中每 MB 可用空间的生存时间(软引用),以毫秒为单位。 | -XX:SoftRefLRU\PolicyMSPerMB=2000 |
这是用于在抛出OutOfMemory 错误之前限制垃圾收集所用时间的使用策略。 |
-XX:+UseGCOverheadLimit |
这限制了抛出OutOfMemory 错误之前在垃圾收集中花费的时间比例。与GCHeapFreeLimit 一起使用。 |
-XX:GCTimeLimit=95 |
这将设置在抛出OutOfMemory 错误之前,完全垃圾收集之后的最小可用空间百分比。与GCTimeLimit 一起使用。 |
-XX:GCHeapFreeLimit=5 |
最后,这里有一些特定于 G1 的选项。请注意,从 jvm6u26 开始,所有这些都受支持:
G1 垃圾收集选项 | 标志 |
---|---|
堆区域的大小。默认值是 2048,可接受的范围是 1 MiB 到 32 MiB。 | -XX:G1HeapRegionSize=16m |
这是置信系数暂停预测启发式算法。 | -XX:G1ConfidencePercent=75 |
这决定了堆中的最小保留空间。 | -XX:G1ReservePercent=5 |
这是每个 MMU 的垃圾收集时间——时间片(毫秒)。 | -XX:MaxGCPauseMillis=100 |
这是每个 MMU 的暂停间隔时间片(毫秒)。 | -XX:GCPauseIntervalMillis=200 |
MiB 代表 Mebibyte,它是数字信息的字节倍数。
让我们看看与垃圾收集相关联的两种特定方法。
虽然垃圾收集在 Java 中是自动的,但是您可以显式调用java.lang.System.gc()
方法来帮助调试过程。此方法不接受任何参数,也不返回任何值。它是一个显式调用,运行 Java 的垃圾收集器。下面是一个示例实现:
System.gc();
System.out.println("Garbage collected and unused memory has been deallocated.");
让我们看一个更深入的例子。在下面的代码中,我们首先创建一个实例Runtime
,使用返回单例的Runtime myRuntime = Runtime.getRuntime();
。这使我们能够访问 JVM。在打印一些头信息和初始内存统计信息之后,我们创建了大小为300000
的ArrayList
。然后,我们创建一个循环来生成100000
数组列表对象。最后,我们在三个过程中提供输出,要求 JVM 调用垃圾收集器,中间有1
秒的暂停。以下是源代码:
package MyGarbageCollectionSuite;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
public class GCVerificationTest {
public static void main(String[] args) throws InterruptedException {
// Obtain a Runtime instance (to communicate with the JVM)
Runtime myRuntime = Runtime.getRuntime();
// Set header information and output initial memory stats
System.out.println("Garbage Collection Verification Test");
System.out.println("-----------------------------------------------
-----------");
System.out.println("Initial JVM Memory: " + myRuntime.totalMemory()
+
"\tFree Memory: " + myRuntime.freeMemory());
// Use a bunch of memory
ArrayList<Integer> AccountNumbers = new ArrayList<>(300000);
for (int i = 0; i < 100000; i++) {
AccountNumbers = new ArrayList<>(3000);
AccountNumbers = null;
}
// Provide update with with three passes
for (int i = 0; i < 3; i++) {
System.out.println("--------------------------------------");
System.out.println("Free Memory before collection number " +
(i+1) + ": " + myRuntime.freeMemory());
System.gc();
System.out.println("Free Memory after collection number " +
(i+1) + ": " + myRuntime.freeMemory());
TimeUnit.SECONDS.sleep(1); // delay thread 5 second
}
}
}
从以下输出中可以看到,垃圾收集器在第一次甚至第二次传递期间没有重新分配所有垃圾:
垃圾收集验证测试
除了使用System.gc()
方法调用垃圾收集器之外,还有一种替代方法。在我们的例子中,我们可以使用myRuntime.gc()
,我们早期的单例例子。
你可以把 Java 的垃圾收集器想象成死亡贩子。当它从记忆中删除某些东西时,它就消失了。这个所谓的死亡贩子并非没有同情心,因为它为每个方法提供了他们最后的遗言。对象通过finalize()
方法给出他们的最后一句话。如果一个对象有一个finalize()
方法,垃圾收集器会在移除该对象和释放相关内存之前调用它。该方法不带参数,返回类型为void
。
finalize()
方法只调用一次,在运行时可能会有变化,当然,方法是在被删除之前调用的,但是垃圾收集器运行时依赖于系统。例如,如果您有一个运行内存丰富系统的相对较小的应用,则垃圾收集器可能根本不会运行。那么,为什么要包含一个finalize()
方法呢?覆盖finalize()
方法被认为是糟糕的编程实践。也就是说,如果需要的话,你可以使用这个方法。实际上,您可以在那里添加代码来添加对对象的引用,以确保垃圾收集器不会删除该对象。同样,这是不可取的。
因为 Java 中的所有对象,甚至是您自己创建的对象,都是java.lang.Object
的子类,所以 Java 中的每个对象都有一个finalize()
方法
垃圾收集器虽然复杂,但可能无法按您希望的方式关闭数据库、文件或网络连接。如果您的应用在收集其对象时需要特定的注意事项,您可以覆盖对象的finalize()
方法
下面是一个示例实现,它演示了当您可能希望覆盖对象的finalize()
方法时的一个用例:
public class Animal {
private static String animalName;
private static String animalBreed;
private static int objectTally = 0;
// constructor
public Animal(String name, String type) {
animalName = name;
animalBreed = type;
// increment count of object
++objectTally;
}
protected void finalize() {
// decrement object count each time this method
// is called by the garbage collector
--objectTally;
//Provide output to user
System.out.println(animalName + " has been removed from memory.");
// condition for 1 animal (use singular form)
if (objectTally == 1) {
System.out.println("You have " + objectTally + " animal
remaining.");
}
// condition for 0 or greater than 1 animals (use plural form)
else {
System.out.println("You have " + objectTally + " animals
remaining.");
}
}
}
正如您在前面的代码中所看到的,objectTally
计数在每次创建类型为Animal
的对象时递增,而在垃圾收集器删除类型为Animal
的对象时递减。
通常不鼓励覆盖对象的finalize()
方法。finalize()
方法通常应声明为protected
。
Java 的垃圾收集对于 Java9 来说并不新鲜,它从 Java 的初始版本就已经存在了,Java 早就有了一个复杂的垃圾收集系统,它是自动的并且在后台运行。通过在后台运行,我们指的是在空闲时间运行的垃圾收集进程。
空闲时间是指输入/输出之间的时间,例如键盘输入、鼠标单击和输出生成之间的时间。
这种自动垃圾收集是开发人员选择 Java 作为编程解决方案的关键因素之一。其他编程语言,如 C#和 Objective-C,在 Java 平台成功之后已经实现了垃圾收集。
在查看当前 Java 平台中对垃圾收集的更改之前,下面让我们先看看下面列出的概念:
- 可视化垃圾收集
- Java8 中的垃圾收集升级
- 用 Java 编写的案例游戏
将垃圾收集的工作原理以及(也许更重要的是)对它的需求形象化是很有帮助的。考虑以下逐步创建字符串Garbage
的代码段:
001 String var = new String("G");
002 var += "a";
003 var += "r";
004 var += "b";
005 var += "a";
006 var += "g";
007 var += "e";
008 System.out.println("Your completed String is: " + var + ".");
显然,前面的代码生成的输出如下所示:
Your completed String is Garbage.
可能不清楚的是,示例代码产生了五个未引用的字符串对象,这在一定程度上是由于字符串是不可变的。如下表所示,对于每一行连续的代码,被引用的对象都会被更新,而另一个对象将变为未被引用:
未引用对象累积
前面列出的未引用对象肯定不会破坏内存库,但它表示大量未引用对象的累积速度有多快。
从 Java8 开始,默认的垃圾收集算法是并行垃圾收集器。这些改进之一是能够使用以下命令行选项通过删除重复的字符串值来优化堆内存:
-XX:+UseStringDeduplication
G1 垃圾收集器在看到字符串时可以查看字符数组。然后,它获取值并将其与新的、弱的字符数组引用一起存储。如果 G1 垃圾收集器发现一个具有相同哈希码的字符串,它将用一个字符一个字符的检查来比较这两个字符串。如果找到匹配项,两个字符串最终都指向同一个字符数组。具体来说,第一个字符串将指向第二个字符串的字符数组。
这种方法可能需要大量的处理开销,只有在认为有益或绝对必要时才应使用。
多人游戏需要广泛的管理技术,无论是服务器还是客户端系统。JVM 在低优先级线程中运行垃圾收集线程,并定期运行。服务器管理员以前使用了一个增量垃圾收集模式,使用现在已废弃的-Xincgc
命令行选项,以避免服务器过载时发生服务器暂停。目标是让垃圾收集运行得更频繁,每次执行周期要短得多。
在考虑内存使用和垃圾收集时,在目标系统上使用尽可能少的内存并在可行的范围内限制垃圾收集的暂停是很重要的。这些技巧对于游戏、模拟和其他需要实时性能的应用尤其重要。
JVM 管理存储 Java 内存的堆。默认情况下,JVM 从一个小堆开始,随着其他对象的创建而增长。堆有两个分区:年轻分区和终身分区。最初创建对象时,它们在年轻分区中创建。持久对象被移动到保留分区。对象的创建通常非常快速,只需增加指针即可。年轻分区的处理速度比长期分区快得多。这是很重要的,因为它适用于整个应用,或者在我们的情况下,一个游戏的效率。
对我们来说,监控游戏的内存使用情况以及垃圾收集发生的时间变得非常重要。为了监控垃圾收集,我们可以在启动游戏时添加verbose
标志(-verbose:gc
),例如下面的例子:
java -verbose:gc MyJavaGameClass
然后 JVM 将为每个垃圾收集提供一行格式化输出。以下是verbose
GC 输出的格式:
[<TYPE> <MEMORY USED BEFORE> -> MEMORY USED AFTER (TOTAL HEAP SIZE), <TIME>]
让我们看两个例子。在第一个例子中,我们看到类型的GC
,它指的是我们之前讨论过的年轻分区:
[GC 31924K -> 29732K(42234K), 0.0019319 secs]
在第二个示例中,Full GC
表示对内存堆的永久分区执行了垃圾收集操作:
[Full GC 29732K -> 10911K(42234K), 0.0319319 secs]
您可以使用-XX:+PrintGCDetails
选项从垃圾收集器获取更详细的信息,如下所示:
java -verbose:gc -XX:+PrintGCDetails MyJavaGameClass
Java 以自动垃圾收集的方式脱颖而出,成为许多程序员的首选开发平台。在其他编程语言中,想要避免手动内存管理是司空见惯的。我们深入研究了垃圾收集系统,包括 JVM 使用的各种方法或算法。Java,从 Release9 开始一直到 Release11,其中包括对垃圾收集系统的一些相关更改。让我们回顾一下最重要的变化:
- 默认垃圾收集
- 废弃的垃圾收集组合
- 统一垃圾收集日志
- 垃圾收集接口
- G1 的并行完全垃圾收集
- Epsilon:一个任意低开销的垃圾收集(GC)
我们将在下面的小节中回顾每一个垃圾收集概念问题。
我们之前详细介绍了 Java9 之前的 JVM 使用的以下垃圾收集方法。这些仍然是合理的垃圾收集算法:
- CMS 垃圾收集
- 串行垃圾收集
- 并行垃圾收集
- G1 垃圾收集
让我们简要回顾一下这些方法:
- CMS 垃圾收集:CMS 垃圾收集算法使用多线程扫描堆内存。使用这种方法,JVM 标记要删除的对象,然后进行扫描以实际删除它们。
- 串行垃圾收集:这种方法在单个线程上使用线程冻结模式。当垃圾收集正在进行时,它会冻结所有其他线程,直到垃圾收集操作结束。由于串行垃圾收集的线程冻结特性,它只适用于非常小的程序。
- 并行垃圾收集:这种方法使用多个线程,但冻结应用中所有非垃圾收集线程,直到垃圾收集函数完成,就像串行垃圾收集算法一样
- G1 垃圾收集:这是垃圾收集算法,具有以下特点:
- 与大内存堆一起使用
- 包括将内存堆分割为多个区域
- 与每个堆区域并行进行
- 释放内存时压缩堆空间
- 使用停止世界方法进行压实操作
- 根据要收集的垃圾最多的区域来确定区域的优先级
在 Java9 之前,并行垃圾收集算法是默认的垃圾收集器,在 Java9 中,G1 垃圾收集器是 Java 内存管理系统的新默认实现。32 位和 64 位服务器配置都是如此
Oracle 评估 G1 垃圾收集器,主要是由于它的低暂停特性,是一种比并行方法性能更好的垃圾收集方法。这一变化基于以下概念:
- 限制延迟是很重要的
- 最大化吞吐量不如限制延迟重要
- G1 垃圾收集算法是稳定的
使 G1 垃圾收集方法成为并行方法的默认方法涉及两个假设:
- 使 G1 成为默认的垃圾收集方法将显著增加其使用量。这种增加的使用可能会暴露出在 Java9 之前没有意识到的性能或稳定性问题
- G1 方法比并行方法更需要处理器。在某些用例中,这可能有点问题。
从表面上看,这一变化对于 Java9 来说似乎是一个伟大的进步,很可能就是这样。但是,当盲目接受这种新的默认收集方法时,应该谨慎使用。如果切换到 G1,建议对系统进行测试,以确保应用不会因使用 G1 而出现性能下降或意外问题。如前所述,G1 并没有从并行方法的广泛测试中获益。
关于缺乏广泛测试的最后一点意义重大。使用 Java9 将 G1 作为默认的自动内存管理(垃圾收集)系统等同于将开发人员变成毫无戒备的测试人员。虽然预计不会出现大的问题,但了解到在使用 G1 和 Java9 时可能会出现性能和稳定性问题,将更加强调测试 Java9 应用。
Oracle 在将特性、API 和库从 Java 平台的新版本中删除之前,一直非常重视这些特性、API 和库。有了这个模式,在 Java8 中被贬低的语言组件就可以在 Java9 中被删除。在 Java8 中,有一些垃圾收集组合被认为很少使用和被贬低
下面列出的这些组合已在 Java9 中删除:
- DefNew + CMS
- ParNew + SerialOld
- 增量 CMS
这些组合除了很少使用之外,还为垃圾收集系统带来了不必要的复杂性。这导致了系统资源的额外消耗,而没有为用户或开发人员提供相应的好处
以下列出的垃圾收集配置受 Java8 平台中上述废弃的影响:
垃圾收集配置 | 标志 |
---|---|
DefNew + CMS | -XX:+UseParNewGC |
-XX:UseConcMarkSweepGC |
|
ParNew + SerialOld | -XX:+UseParNewGC |
ParNew + iCMS | -Xincgc |
ParNew + iCMS | -XX:+CMSIncrementalMode |
-XX:+UseConcMarkSweepGC |
|
Defnew + iCMS | -XX:+CMSIncrementalMode |
-XX:+UseConcMarkSweepGC |
|
-XX:-UseParNewGC |
随着 Java9 的发布,JDK8 中的垃圾收集组合被删除,这些组合与控制这些组合的标志一起列出。此外,启用 CMS 前台集合的标志已被删除,并且在 JDK9 中不存在。这些标志如下所示:
垃圾收集组合 | 标志 |
---|---|
CMS 前景 | -XX:+UseCMSCompactAtFullCollection |
CMS 前景 | -XX+CMSFullGCsBeforeCompaction |
CMS 前景 | -XX+UseCMSCollectionPassing |
删除已废弃的垃圾收集组合的唯一缺点是,使用带有本节中列出的任何标志的 JVM 启动文件的应用将需要修改其 JVM 启动文件以删除或替换旧标志。
统一 GC 日志记录是 JDK9 增强的一部分,旨在使用统一 JVM 日志记录框架重新实现垃圾收集日志记录。因此,让我们首先回顾一下统一 JVM 日志记录计划。
为 JVM 创建统一的日志模式包括以下目标的高级列表:
- 为所有日志操作创建一组 JVM 范围的命令行选项。
- 使用分类标签进行日志记录。
- 提供六个级别的日志记录,如下所示:
- 错误
- 警告
- 信息
- 调试
- 跟踪
- 开发
这不是一个详尽的目标清单。我们将在第 14 章“命令行标志”中更详细地讨论 Java 的统一日志模式。
在日志记录上下文中,对 JVM 的更改可以分为:
- 标签
- 水平
- 装饰
- 输出
- 命令行选项
让我们简单地看一下这些类别。
日志标记在 JVM 中标识,如果需要,可以在源代码中更改。标签应该是自识别的,例如用于垃圾收集的gc
。
每个日志消息都有一个关联的级别。如前所列,级别包括错误、警告、信息、调试、跟踪和开发。下图显示了级别的详细程度如何随着记录的信息量的增加而增加:
冗长程度
在 Java 日志框架的上下文中,装饰是关于日志消息的元数据。以下是按字母顺序排列的可用装饰品列表:
level
pid
tags
tid
time
timemillis
timenanos
uptime
uptimemillis
uptimenanos
有关这些装饰的说明,请参阅第 14 章、“命令行标志”。
Java9 日志框架支持三种类型的输出:
stderr
:向stderr
提供输出stdout
:向stdout
提供输出- 文本文件:将输出写入文本文件
通过命令行控制 JVM 的日志操作。-Xlog
命令行选项有大量的参数和可能性。下面是一个例子:
-Xlog:gc+rt*=debug
在本例中,我们告诉 JVM 执行以下操作:
- 记录至少带有
gc
和rt
标记的所有消息 - 使用
debug
水平 - 向
stdout
提供输出
现在我们已经对 Java 的日志框架的变化有了大致的了解,让我们看看引入了哪些变化。在本节中,我们将了解以下方面:
- 垃圾收集日志记录选项
gc
标签- 宏
- 其他注意事项
下面是我们在引入 Java 日志框架之前可以使用的垃圾收集日志选项和标志的列表:
垃圾收集日志记录选项 | JVM 选项标志 |
---|---|
这将打印基本的垃圾收集信息。 | -verbose:gc 或-XX:+PrintGC |
这将打印更详细的垃圾收集信息。 | -XX:+PrintGCDetails |
您可以打印每个垃圾收集事件的时间戳。秒是连续的,从 JVM 开始时间开始。 | -XX:+PrintGCTimeStamps |
您可以为每个垃圾收集事件打印日期戳。样本格式:2017-07-26T03:19:00.319+400:[GC . . . ] |
-XX:+PrintGCDateStamps |
您可以使用此标志打印单个垃圾收集工作线程任务的时间戳。 | -XX:+PrintGC\TaskTimeStamps |
使用此选项,可以将垃圾收集输出重定向到文件而不是控制台。 | -Xloggc: |
您可以在每个收集周期之后打印有关年轻空间的详细信息。 | -XX:+Print\TenuringDistribution |
可以使用此标志打印 TLAB 分配统计信息。 | -XX:+PrintTLAB |
使用此标志,您可以打印Stop the World 暂停期间的参考处理时间(即弱、软等)。 |
-XX:+PrintReferenceGC |
此报告垃圾收集是否正在等待本机代码取消固定内存中的对象。 | -XX:+PrintJNIGCStalls |
每次停止暂停后,打印暂停摘要。 | -XX:+PrintGC\ApplicationStoppedTime |
此标志将打印垃圾收集的每个并发阶段的时间。 | -XX:+PrintGC\ApplicationConcurrentTime |
使用此标志将在完全垃圾收集后打印类直方图。 | -XX:+Print\ClassHistogramAfterFullGC |
使用此标志将在完全垃圾收集之前打印类直方图。 | -XX:+Print\ClassHistogramBeforeFullGC |
这将在完全垃圾收集之后创建一个堆转储文件。 | -XX:+HeapDump\AfterFullGC |
这将在完全垃圾收集之前创建一个堆转储文件。 | -XX:+HeapDump\BeforeFullGC |
这将在内存不足的情况下创建堆转储文件。 | -XX:+HeapDump\OnOutOfMemoryError |
您可以使用此标志指定要在系统上保存堆转储的路径。 | -XX:HeapDumpPath=<path> |
如果n >= 1 ,您可以使用它来打印 CMS 统计信息。仅适用于 CMS。 |
-XX:PrintCMSStatistics=2 |
这将打印 CMS 初始化详细信息。仅适用于 CMS。 | -XX:+Print\CMSInitiationStatistics |
您可以使用此标志打印有关可用列表的其他信息。仅适用于 CMS。 | -XX:PrintFLSStatistics=2 |
您可以使用此标志打印有关可用列表的其他信息。仅适用于 CMS。 | -XX:PrintFLSCensus=2 |
您可以使用此标志在升级(从年轻到终身)失败后打印详细的诊断信息。仅适用于 CMS。 | -XX:+PrintPromotionFailure |
当升级(从年轻到终身)失败时,此标志允许您转储有关 CMS 旧代状态的有用信息。仅适用于 CMS。 | -XX:+CMSDumpAt\PromotionFailure |
当使用-XX:+CMSDumpAt\PromotionFailure 标志时,您可以使用-XX:+CMSPrint\ChunksInDump 来包含关于空闲块的附加细节。仅适用于 CMS。 |
-XX:+CMSPrint\ChunksInDump |
当使用-XX:+CMSPrint\ChunksInDump 标志时,您可以使用-XX:+CMSPrint\ObjectsInDump 标志包含有关已分配对象的附加信息。仅适用于 CMS。 |
-XX:+CMSPrint\ObjectsInDump |
我们可以使用带有-Xlog
选项的gc
标记来通知 JVM 在info
级别只记录gc
标记的项。您还记得,这类似于使用-XX:+PrintGC
。使用这两个选项,JVM 将为每个垃圾收集操作记录一行。
值得注意的是,gc
标签并非单独使用,而是建议与其他标签一起使用。
我们可以创建宏,以向垃圾收集日志记录添加逻辑。以下是log
宏的一般语法:
log_<level>(Tag1[,...])(fmtstr, ...)
以下是一个log
宏的例子:
log_debug(gc, classloading)("Number of objects loaded: %d.", object_count)
下面的示例框架log
宏显示了如何使用新的 Java 日志框架来创建脚本,以提高日志记录的逼真度:
LogHandle(gc, rt, classunloading) log;
if (log.is_error()) {
// do something specific regarding the 'error' level
}
if (log.is_warning()) {
// do something specific regarding the 'warning' level
}
if (log.is_info()) {
// do something specific regarding the 'info' level
}
if (log.is_debug()) {
// do something specific regarding the 'debug' level
}
if (log.is_trace()) {
// do something specific regarding the 'trace' level
}
以下是关于垃圾收集日志记录需要考虑的一些附加项目:
- 使用新的
-Xlog:gc
应该会产生与-XX:+PrintGCDetails
命令行选项和标志配对类似的结果 - 新的
trace
级别提供了以前使用verbose
标志提供的详细级别
对 Java 垃圾收集的改进并没有随着 Java8 和 Java9 中的主要变化而停止。在 Java10 中,引入了一个干净的垃圾收集器接口。新接口的目标是增加特定于 HotSpot JVM 的内部垃圾收集代码的模块化。增加的模块化将使新接口更容易更新,而不会对核心代码库产生负面影响。另一个好处是相对容易地从 JDK 构建中排除垃圾收集。
在 Java10 之前,垃圾收集实现在 JVM 的整个文件结构中都是源代码。清理这些代码以使代码模块化是优化 Java 代码库和使垃圾收集现代化的一个自然步骤,这样可以更容易地更新和使用。
在 Java 中,垃圾收集器实现了CollectedHeap
类,该类管理 JVM 和垃圾收集操作之间的交互
新的垃圾收集接口值得注意,但最适用于垃圾收集和 JVM 开发人员
正如本章前面提到的,G1 垃圾收集器自 Java9 以来一直是默认的垃圾收集器。G1 垃圾收集器的效率之一是它使用并发垃圾收集而不是完全收集。有时会实现完全垃圾收集,通常是并发垃圾收集速度不够快。注意,在 Java9 之前,并行收集器是默认的垃圾收集器,是一个并行的完全垃圾收集器。
对于 Java10,G1Full 垃圾收集器被转换为并行,以减轻对使用完全垃圾收集的开发人员的任何负面影响。将用于 G1 完全垃圾收集的 mark-week 压缩算法并行化。
Java 的最新版本 11 附带了一个负责内存分配的被动 GC。这个 GC 的被动性质(称为 EpsilonGC)表明它不执行垃圾收集;相反,它继续分配内存,直到堆上没有剩余空间为止。这时,JVM 关闭。
为了启用 Epsilon GC,我们使用以下任一方法:
-XX:+UseEpsilonGC
-XX:+UseNoGC
EpsilonGC 的使用主要出现在测试中,由于缺乏垃圾收集,它的开销很低,提高了测试效率
即使有了 Java9、10 和 11 的现代版本,Java 的垃圾收集系统也有缺点,因为它是一个自动过程,所以我们不能完全控制收集器的运行时间。作为开发人员,我们不能控制垃圾收集,JVM 是。JVM 决定何时运行垃圾收集。正如您在本章前面所看到的,我们可以要求 JVM 使用System.gc()
方法运行垃圾收集。尽管我们使用了这种方法,但我们不能保证我们的请求会得到满足,也不能保证我们的请求会及时得到满足
在本章前面,我们回顾了垃圾收集的几种方法和算法。我们讨论了作为开发人员如何控制流程。这假设我们有能力控制垃圾收集。即使我们指定了一种特定的垃圾收集技术(例如,将-XX:+UseConcMarkSweepGC
用于 CMS 垃圾收集),我们也不能保证 JVM 将使用该实现。因此,我们可以尽最大努力控制垃圾收集器的工作方式,但是应该记住,JVM 对于如何、何时以及是否发生垃圾收集具有最终的权限
我们缺乏对垃圾收集的完全控制,这突出了在编写高效代码时考虑内存管理的重要性。在下一节中,我们将研究如何编写代码来显式地使对象符合 JVM 垃圾收集的条件。
使对象可用于垃圾收集的一种简单方法是将null
赋给引用该对象的引用变量。让我们回顾一下这个例子:
package MyGarbageCollectionSuite;
public class GarbageCollectionExperimentOne {
public static void main(String[] args) {
// Declare and create new object.
String junk = new String("Pile of Junk");
// Output to demonstrate that the object has an active
// reference and is not eligible for garbage collection.
System.out.println(junk);
// Set the reference variable to null.
junk = null;
// The String object junk is now eligible for garbage collection.
}
}
如在代码注释中所示,一旦字符串对象引用变量设置为null
,在本例中使用junk = null;
语句,对象就可以进行垃圾收集。
在我们的下一个示例中,我们将通过将对象的引用变量设置为指向另一个对象来放弃该对象。正如您在以下代码中看到的,这导致第一个对象可用于垃圾收集:
package MyGarbageCollectionSuite;
public class GarbageCollectionExperimentTwo {
public static void main(String[] args) {
// Declare and create the first object.
String junk1 = new String("The first pile of Junk");
// Declare and create the second object.
String junk2 = new String("The second pile of Junk");
// Output to demonstrate that both objects have active references
// and are not eligible for garbage collection.
System.out.println(junk1);
System.out.println(junk2);
// Set the first object's reference to the second object.
junk1 = junk2;
// The String "The first pile of Junk" is now eligible for garbage
//collection.
}
}
让我们回顾一下使对象可用于垃圾收集的最后一种方法。在本例中,我们有一个实例变量(objectNbr
,它是GarbageCollectionExperimentThree
类实例的引用变量。这个类除了为GarbageCollectionExperimentThree
类的实例创建额外的引用变量之外,没有做任何有趣的事情。在我们的示例中,我们将objectNbr2
、objectNbr3
、objectNbr4
和objectNbr5
引用设置为null
。尽管这些对象有实例变量并且可以相互引用,但是通过将它们的引用设置为null
,它们在类之外的可访问性已经终止。这使得它们(objectNbr2
、objectNbr3
、objectNbr4
和objectNbr5
有资格进行垃圾收集:
package MyGarbageCollectionSuite;
public class GarbageCollectionExperimentThree
{
// instance variable
GarbageCollectionExperimentThree objectNbr;
public static void main(String[] args) {
GarbageCollectionExperimentThree objectNbr2 = new
GarbageCollectionExperimentThree();
GarbageCollectionExperimentThree objectNbr3 = new
GarbageCollectionExperimentThree();
GarbageCollectionExperimentThree objectNbr4 = new
GarbageCollectionExperimentThree();
GarbageCollectionExperimentThree objectNbr5 = new
GarbageCollectionExperimentThree();
GarbageCollectionExperimentThree objectNbr6 = new
GarbageCollectionExperimentThree();
GarbageCollectionExperimentThree objectNbr7 = new
GarbageCollectionExperimentThree();
// set objectNbr2 to refer to objectNbr3
objectNbr2.objectNbr = objectNbr3;
// set objectNbr3 to refer to objectNbr4
objectNbr3.objectNbr = objectNbr4;
// set objectNbr4 to refer to objectNbr5
objectNbr4.objectNbr = objectNbr5;
// set objectNbr5 to refer to objectNbr2
objectNbr5.objectNbr = objectNbr2;
// set selected references to null
objectNbr2 = null;
objectNbr3 = null;
objectNbr4 = null;
objectNbr5 = null;
}
}
在本章中,我们深入回顾了垃圾收集作为一个关键的 Java 平台组件。我们的综述包括对象生命周期、垃圾收集算法、垃圾收集选项以及与垃圾收集相关的方法。我们研究了 Java8、9、10 和 11 中对垃圾收集的升级,并研究了一个案例来帮助我们理解现代垃圾收集。
然后,我们将重点转向新的 Java9 平台对垃圾收集的更改。我们在 Java 中对垃圾收集的探索包括默认垃圾收集、废弃的垃圾收集组合和统一的垃圾收集日志记录。我们通过查看一些即使在最新版本的 Java 中仍然存在的垃圾收集问题来结束对垃圾收集的探索。
在下一章中,我们将研究如何使用 Java 微基准线束(JMH)编写性能测试,这是一个用于编写 JVM 基准测试的 Java 线束库。
- 列举五种垃圾收集算法。
- 什么是 G1?
- iCMS 的用途是什么?
- 什么是 MiB?
- 如何显式调用垃圾收集?
- 如何将
finalize()
方法添加到自定义对象? - 以下垃圾收集组合有什么共同点?
- DefNew + CMS
- ParNew + Serial
- 旧的增量 CMS
- 在 Java 中,由垃圾收集器实现的哪个类管理 JVM 和垃圾收集操作之间的交互?
- Java10 中对 g1fullgc 做了哪些更改?
- Java11 中引入的被动 GC 的名称是什么?
以下参考资料将帮助您深入了解本章中介绍的概念:
- 《Java EE 8 高性能》【视频】在这个页面提供。