了解 Java 垃圾收集的工作原理并优化应用程序中的内存使用情况。详细了解 Java 中内存管理的复杂性。
垃圾收集是一个关键过程,可以帮助任何Java 开发公司。编程语言中的这一强大功能可以巧妙地管理内存分配和释放,防止内存泄漏并优化资源利用率。它就像一个坚定的看门人,勤奋地清理未使用的对象,使我们免于被不必要的数字杂乱所淹没。作为一家 Java 开发公司,我们在编码工作中经常遇到这种挑战,而垃圾收集为此提供了一个优雅的解决方案。让我们深入研究一下这个复杂的机制。
GC 是 Java 编程中的无名英雄,它不仅为我们清理垃圾,还使 Java 内存更加高效。它至关重要,因为作为程序员,我们需要通过释放资源(尤其是未引用的对象)来管理好内存,而我们在寻求新想法时经常会忘记它(有时是因为懒惰)。
Java 垃圾回收的工作原理
让我们来研究一下这个安静清洁器在 Java 中的具体功能。
Java 中的垃圾收集是一个通过回收不再使用的未使用对象来自动管理内存的过程。
内存结构
在Java中,内存分为栈内存、堆内存、元空间,我们来详细了解一下。
堆栈内存
堆栈内存是方法执行期间存储局部变量、对象引用、方法参数和其他方法特定数据的内存区域。堆栈内存的大小是固定的。
堆内存
堆内存是用于存储实际对象的内存区域。其大小是固定的,并且可以根据需要动态增大或缩小。
这是一个例子。
java
Integer num = new Integer(12);
这会在堆栈内存中创建一个"num"变量,并在堆内存中创建一个新的 Integer 对象。堆栈内存中的"num"变量存储对原始对象的引用。
元空间
元空间是 Java 虚拟机 (JVM) 使用的本机内存的一部分,用于存储有关类和静态方法的元数据。它取代了永久代堆内存,后者是堆内存的一部分。
在早期版本的 Java 中,PermGen 用于存储有关类和静态方法的元数据。但它有一些限制。其中之一是它们的大小是固定的。另一个问题是 PermGen 空间与堆的其余部分一起被垃圾收集,这导致了性能问题。
Metaspace 可动态调整大小,并单独进行垃圾回收。它允许在多个 JVM 实例之间共享类元数据,从而减少内存使用量并提高性能。
垃圾收集资格
活动可访问对象是被程序的某些部分引用的对象,而死亡或不可访问对象是未被程序的任何部分引用的对象。例如:
java
Integer num = new Integer(12);
java
num = null;
如上所述,第一行在堆内存中创建一个新的 Integer 对象,并在堆栈内存中创建一个变量,该变量存储对原始对象的引用。
然后,在下一行中,我们更改了"num"的引用,这意味着"num"不引用我们之前创建的 Integer 对象。事实上,该 Integer 对象未被我们程序的任何部分引用。因此,它是一个无法访问或死对象。死对象可以被垃圾回收。
在以下情况下对象将变得无法访问:
- 所有引用该对象的变量都不再引用它(它们要么设置为空,要么设置为不同的值)。
- 当该方法从堆栈内存中释放时,在该方法内部创建的对象将变得无法访问。
隔离岛
隔离岛是指一组对象相互引用,但不再被程序中的任何对象引用。在下面给出的示例中,"a"和"b"相互引用,但不再被任何其他对象引用。
java
class Node {
Node next;
Node prev;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.prev = a;
为了打破孤立岛,我们需要改变对象的引用。这里,只有改变"a"和"b"的引用(例如,将 a 和 b 设置为 null)后,它们才能被垃圾回收。
堆内存的各部分
前面提到过,堆内存是负责存储对象的内存部分,分为年轻代空间(Young Generation Space)和老生代空间(Old Generation Space)。
年轻一代
在 Java 中,年轻代堆内存是新对象被创建的地方。这部分内存又分为两个部分:Eden 空间和 Survivor 空间。
伊甸园空间
伊甸园空间是年轻代空间的一部分,新对象被分配于此。
幸存者空间
经过一轮垃圾收集后,Eden 空间中幸存下来的对象将被提升至幸存者空间。
Java 垃圾收集器中的 Survivor Spaces 数量取决于所使用的具体收集器。Survivor Spaces 数量取决于所使用的具体收集器。在并行收集器和 CMS 收集器中,有多个 Survivor Spaces。并行收集器将 Survivor Space 划分为多个区域,而 CMS 收集器使用多个 Survivor Spaces。我们将在下面仔细研究不同的 Java 垃圾收集器。
老一辈
经过一定次数的垃圾收集后仍存活的对象将被提升到老一代。老一代中的对象寿命更长。它们不适合进行次要 GC,只能在主要垃圾收集期间清除。
老生代又称为终身代。
垃圾收集涉及的步骤
Java 垃圾收集通过持续监视 Java 虚拟机的堆内存来识别不再使用的对象。
Java垃圾收集按以下步骤进行:
- 标记: GC 首先识别堆中的所有活动对象并对其进行标记。
- **清除:**一旦识别并标记了所有活动对象,GC 就会清除堆并释放不再使用的内存。然后这些内存便可分配给新对象。
- **压缩:**在某些 Java 垃圾收集算法中,清除之后,剩余的对象会被压缩,这意味着它们会被移动到堆的一端,使得 JVM 更容易分配新对象。
小型垃圾收集器
次要垃圾收集是从年轻一代识别和收集垃圾的过程,保持其无垃圾并减少主要 Java 垃圾收集周期的频率。
小型 Java 垃圾收集在较小的堆大小上进行,因此比大型垃圾收集快得多。
次要垃圾收集的工作原理如下:
**Eden 空间填充:**随着新对象被分配到 Eden 空间,Eden 空间最终会被填满。当 Eden 空间已满时,垃圾收集器将开始一次次要 GC 循环。
**初始标记:**垃圾收集器通过执行初始标记阶段来开始次要垃圾收集周期。在此阶段,垃圾收集器会识别 Eden 空间和幸存者空间中的所有活动对象。垃圾收集器会标记这些活动对象以表明不应收集它们。
**复制收集:**初始标记阶段完成后,垃圾收集器将执行复制收集阶段。在此阶段,垃圾收集器将所有存活对象从 Eden 空间和其中一个幸存者空间复制到另一个幸存者空间。
**清除未使用的空间:**将存活对象复制到幸存者空间后,垃圾收集器将清除 Eden 空间和当前垃圾收集周期中未使用的幸存者空间。任何未复制到幸存者空间的对象都被视为垃圾,并由垃圾收集器回收。
**对象的提升:**经历了一定数量的垃圾收集周期而幸存下来的对象最终将被提升到老一代(也称为终身一代),在那里它们将由针对较长寿命的对象进行优化的不同垃圾收集算法进行管理。
**多重循环:**如果在当前 Java 垃圾收集周期中使用的幸存者空间已满,则收集器将执行额外的次要垃圾收集周期,直到足够多的对象被提升到老一代或幸存者空间再次变空。
大型垃圾收集器
当老年代空间被填满时,主垃圾收集器就会启动。之所以称为"主垃圾收集器",是因为它作用于整个堆,并且调用频率低于次要垃圾收集器。它通常更耗时且占用资源更多。
主要垃圾收集所涉及的步骤与上面描述的非常相似。
Java 中的垃圾收集器类型
Java 提供了几种不同类型的垃圾收集器。以下是所有垃圾收集器的列表以及它们的工作原理和优点。
串行收集器或停止并复制
串行垃圾收集器是 Java 中的一种 GCr,它使用单个线程执行 Java 垃圾收集过程。它主要用于具有相对简单的内存使用模式的基本应用程序。
您可能已经猜到了,串行垃圾收集器按顺序工作,这意味着它在执行 Java 垃圾收集过程时会停止应用程序中的所有线程。应用程序执行中的这种暂停有时被称为"stop the world"事件。
要使用串行收集器,请传入 -XX:UseSerialCollector 作为参数。
例如,java -XX:UseSerialCollector YourProgram
串行收集器的优点:
- **简单:**这是 Java 中最简单、最直接的垃圾收集器。它占用空间小,需要的调整最少。
- **可预测性:**由于它使用单线程,因此其行为可预测且易于理解。这使其对于需要可预测内存使用模式的应用程序非常有用。
- **非常低的开销:**这使其对于性能至关重要的小型应用程序非常有用。
串行收集器的缺点:
- **不设计为可扩展:**它不是设计为随着堆的大小或系统上可用的处理器的数量而扩展。
- **内存使用率低:**可用内存利用率低。
- **长暂停时间:**由于其设计,长暂停时间被融入到流程中。
并行收集器
并行垃圾收集器是Java 中的默认垃圾收集器,它是一种利用多个线程来提高垃圾收集性能的方法。它对于具有复杂内存使用模式的大型应用程序特别有效。
通过将堆细分为更小的段,并行垃圾收集器利用多个线程同时执行垃圾收集过程。与串行收集器类似,并行收集器也会在垃圾收集期间导致应用程序执行暂时暂停。
要使用并行收集器,请传入 -XX:+UseParallelGC 作为参数。
并行收集器的优点:
- **更快:**由于利用了多线程,与串行收集器相比,其性能更佳,可以实现更快的垃圾收集操作。
- **更好的可扩展性:**设计用于通过堆的大小有效地扩展,使其适合具有更大内存需求的应用程序。
并行收集器的缺点:
- **更长的暂停时间:**并行垃圾收集器在垃圾收集过程中停止应用程序,与其他垃圾收集器相比,这会导致更长的暂停时间。
- **更高的 CPU 开销:**并行垃圾收集器使用多个线程,这会导致更高的开销和增加内存使用量。
并发标记清除收集器
CMS (并发标记和清除)收集器是另一种垃圾收集器。它与应用程序并发执行垃圾收集过程,或者换句话说,它使用多个垃圾收集器线程。它旨在最大限度地减少应用程序中的暂停时间并减少 Java 垃圾收集对性能的影响。
要使用 CMSCollector,请传入 -XX:+UseConcMarkSweepGC 作为参数。
CMS的优点:
- 低暂停时间: CMS 最大限度地减少垃圾收集期间的暂停时间,为延迟敏感的应用程序提供更流畅的体验。
- 可预测: CMS 提供更可预测的垃圾收集暂停,这对于实时系统或具有严格性能要求的应用程序至关重要。
- **适用于更大的堆:**即使堆大小增加,CMS 仍能保持其性能,这使其成为具有大量内存需求的应用程序的可行选择。
CMS 的缺点:
- 更高的 CPU 开销: CMS 由于其并发特性而消耗更多的 CPU 资源,这可能会影响应用程序的整体性能。
- 碎片化风险: CMS 不是长期运行应用程序的理想选择,因为堆会随着时间的推移而变得碎片化。这种碎片化会导致内存使用量增加和性能下降。
G1 收集器
Garbage -First (G1) 收集器是 Java 7 中引入的一种垃圾收集算法,旨在解决传统垃圾收集器(例如并行收集器和 CMS 收集器)的局限性。G1 被设计为低暂停、以吞吐量为导向的收集器,可以处理非常大的堆。
要使用 G1 Collector,请传入以下参数:
java
-XX:+UseG1GC
G1收集器的优点:
- 低暂停时间: G1 的设计旨在最大限度地减少暂停时间,这使其适合实时应用。
- 可扩展性: G1 具有可扩展性,这使其适用于具有不同堆大小的大型应用程序。
G1收集器的缺点:
- **开销:**与其他垃圾收集器相比,G1 消耗更多的 CPU 资源,导致 CPU 开销增加。
- 较长的初始标记时间: G1 中的初始标记阶段可能需要更长时间,特别是对于较大的堆大小,这可能会对应用程序性能产生负面影响。
- 不适合小堆:G1 收集器不适合堆大小较小的应用程序,因为它的优势在较大的内存环境中才能得到最好的体现。
ZGC
ZGC 垃圾收集器( ZGC) 是一款 Java 垃圾收集器,专门用于管理超大堆(最大 16TB),同时保持最短的暂停时间。其主要目标是最大限度地缩短垃圾收集过程的持续时间,从而最大限度地提高应用程序的吞吐量。
ZGC的优势:
- **低暂停时间:**旨在最大限度地减少暂停时间,使其适用于实时或延迟敏感的应用程序。
- **可扩展性:**它可以根据堆的大小和可用处理器的数量进行扩展。
- **高性能:**针对高性能进行了优化,实现了可观的吞吐量,同时最大限度地减少了 Java 垃圾收集对应用程序性能的影响。
ZGC的缺点:
- 高内存开销: ZGC 需要大量内存才能有效运行。
- 兼容性有限: ZGC 仅在某些平台上可用,包括 Linux/x64,并且至少需要 JDK 11。
- **更高的 CPU 利用率:**由于其先进的功能,ZGC 可能与其他垃圾收集器相比消耗更多的 CPU 资源,从而可能影响整体应用程序的性能。
雪兰多Shenandoah
Shenandoah是一款 Java 垃圾收集器,旨在实现超低暂停时间,同时保持高吞吐量。作为并发垃圾收集器,它与应用程序并行运行,非常适合对延迟敏感的应用程序。
要使用 Shenandoah Collector,请传入以下参数:
java
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
雪兰多的优势:
- **超低暂停时间:**提供最短的暂停时间,使其成为实时或延迟敏感应用程序的理想选择。
- **低内存开销:**有效管理内存,从而减少内存开销。
- **高吞吐量:**尽管 Shenandoah 专注于降低暂停时间,但它仍然保持高吞吐量,确保最佳的应用程序性能。
Shenandoah 的缺点:
- 兼容性有限: Shenandoah 仅在某些平台上可用,包括 Linux/x64,并且至少需要 JDK 8u40。
- **增加 CPU 利用率:**与其他垃圾收集器相比,这可能会消耗更多的 CPU 资源,从而可能影响整体应用程序的性能。
系统.gc()
Runtime.getRuntime().gc() 或 System.gc() 是要求 JVM 执行垃圾收集以释放一些内存的方法------强调"建议"这个词,因为这正是它所做的。
您无法强制进行垃圾收集。如果您收到" java.lang.OutOfMemoryError ",则调用 System,gc() 将无法解决问题,因为 JVM 通常会在抛出" java.lang.OutOfMemoryError "之前尝试运行垃圾收集器。可能的修复方法是使用不同的 GC 或增加堆大小。
一般来说,建议开发人员避免直接调用System.gc(),而是依赖JVM提供的自动垃圾收集功能。
垃圾收集的常见问题及其解决方法
以下是一些可能面临的问题:
内存不足错误:
当 JVM 内存不足时,就会发生此错误。要解决此问题,开发人员可以增加堆大小或优化应用程序以使用更少的内存。
- **-Xmx:**为您的应用程序设置最大堆大小。
- **-Xms:**为您的应用程序设置初始堆大小。
例如,要将最大堆大小设置为 2 GB,将初始堆大小设置为 512 MB,可以使用以下命令:
java
java -Xmx2g -Xms512m app.jar
长时间暂停:
垃圾收集期间的长时间暂停可能会导致应用程序无响应。要解决此问题,您可以选择专为低暂停时间设计的垃圾收集器或调整垃圾收集器参数。
内存泄漏:
内存泄漏是指对象未正确从内存中释放,导致内存使用量随时间增加。为了解决此问题,开发人员可以使用分析器等工具来识别内存泄漏并进行修复。
Java 内存管理的最佳实践
为了避免垃圾收集的常见问题并有效地管理内存,以下是一些最佳做法:
- **将引用设置为空:**当不再需要某个对象时,始终将引用设置为空。
- **避免创建不必要的对象:**创建不必要的对象会增加内存使用量并导致更频繁的垃圾回收。您应避免创建不必要的对象并尽可能重用现有对象。
- **使用匿名对象:**这是当您不存储对对象的引用时。
例如,createUser(new User())。
- **不再需要时释放资源:**使用外部资源(例如文件句柄或数据库连接)的对象应在不再需要时释放,以避免内存泄漏。
结论
无论您是内部开发还是决定外包 Java 开发,了解 Java 垃圾收集机制都是必须的,特别是如果您想提高 Java 应用程序的性能。我们详细研究了 Java 编程的这一重要部分,从垃圾收集工作原理的基础知识和不同类型的垃圾收集器到内存管理的细节。请记住,即使您雇用了 Java 开发人员,选择正确的 Java 垃圾收集类型并有效地管理内存也会对应用程序的速度产生很大的影响。继续探索,继续编码,并记住每一点效率都可以为更流畅、更快的应用程序做出贡献。祝您编码愉快!