Java 内存管理

26 Oct 2024 | 13 分钟阅读

在 Java 中,内存管理是对象分配和释放的过程,称为内存管理。Java 会自动进行内存管理。Java 使用一种称为垃圾回收器的自动内存管理系统。因此,我们不需要在应用程序中实现内存管理逻辑。Java 内存管理分为两个主要部分

  • JVM 内存结构
  • 垃圾回收器的工作原理

JVM 内存结构

JVM 在堆中创建各种运行时数据区域。这些区域在程序执行期间使用。内存区域在 JVM 退出时被销毁,而数据区域在线程退出时被销毁。

Memory Management in Java

方法区

方法区是堆内存的一部分,它在所有线程之间共享。它在 JVM 启动时创建。它用于存储类结构、超类名称、接口名称和构造函数。JVM 在方法区存储以下类型的信息

  • 类型的完整限定名(例如:String)
  • 类型的修饰符
  • 类型的直接超类名称
  • 超接口的完整限定名结构化列表。

堆区域

堆存储实际对象。它在 JVM 启动时创建。用户可以根据需要控制堆。它可以是固定大小或动态大小。当我们使用 new 关键字时,JVM 会在堆中创建一个对象实例。而该对象的引用存储在栈中。每个正在运行的 JVM 进程只有一个堆。当堆满时,就会进行垃圾回收。例如

上面的语句创建了一个 StringBuilder 类的对象。对象分配到堆中,引用 sb 分配到栈中。堆分为以下部分

  • 年轻代
  • 幸存者空间
  • 老年代
  • 永久代
  • 代码缓存

年轻代

年轻代是 Java 堆内存中新对象最初分配的区域。当此区域填满时,会触发Minor Garbage Collection(GC)。年轻代分为以下三个部分

  • 伊甸园内存和
  • 两个幸存者内存空间

让我们详细讨论上面两个。

  • 伊甸园内存空间:这是分配最新对象的主要位置。
  • 幸存者空间:填满后,伊甸园空间会触发 Minor GC,在此过程中,仍然使用的对象会被移到其中一个幸存者空间。因此,一个幸存者空间始终保持为空,以便在未来的收集过程中容纳这些存活的对象。
  • 对象晋升:在幸存者空间中经历了多次垃圾回收周期的对象最终会被晋升到老年代。晋升通常发生在对象达到一定年龄后,该年龄由预设阈值确定。

老年代

老年代,也称为Tenured Generation,是 Java 堆内存中的一个关键区域,用于存储生命周期长的对象。当对象在年轻代中经历多次垃圾回收周期后仍然存活时,就会用到老年代。与年轻代不同,老年代由一个连续的内存空间组成。

老年代中的垃圾回收(GC)

Major GC/ Full GC: 当老年代填满时,会发生更全面、耗时更长的事件。它们会清理年轻代和老年代,并可能涉及内存压缩以优化空间使用,通常会导致更长的暂停时间。

永久代

永久代,通常称为Perm Gen。它是 Java 虚拟机(JVM)中一个专用的区域,用于存储应用程序类和方法的元数据。需要注意的是,Perm Gen 与存储实例数据的 Java 堆是分开的。

此区域由 JVM 在运行时动态填充,具体取决于应用程序主动使用的类,并且它还包含来自 Java SE 库的类和方法。存储在 Perm Gen 中的对象会在完整垃圾回收周期期间进行垃圾回收,从而可以清理不再需要的类和方法元数据。

Java 堆内存开关

1. -xms

描述:它设置 JVM 启动时的初始堆大小。这是 JVM 在启动时尝试为其堆分配的内存量。

示例:-xms512m 以 512 兆字节的初始堆大小启动 JVM。

2. -xmx

描述:设置最大堆大小。它限制了 JVM 可以为其堆分配的最大内存量,从而影响 Java 应用程序可以用于所有对象和进程的内存量。

示例:-xmx2g 将堆大小限制为 2 吉字节。

3. -xmn

描述:它设置年轻代的尺寸。堆的其余部分(年轻代之外)专用于老年代。

示例:-xmn256m 为年轻代分配 256 兆字节。

4. -XX:PermGen

描述:设置永久代的初始大小,用于存储 JVM 的元数据,例如类和方法数据。请注意,此设置仅适用于 Java 8 之前的 JVM,之后 PermGen 被 Metaspace 取代。

示例:-XX:PermGen=128m 将 PermGen 的初始大小设置为 128 兆字节。

5. -XX:MaxPermGen

描述:设置永久区的最大大小。控制此限制对于防止 JVM 在 PermGen 中空间不足时发生的OutOfMemoryError至关重要。

示例:-XX:MaxPermGen=256m 将 PermGen 的大小限制为 256 兆字节。

6. -XX:SurvivorRatio

描述:它指定年轻代中伊甸园和幸存者空间之间的比例。如果年轻代大小为 10m,则 Survivor Ratio 设置为 2,则 5m 将分配给伊甸园空间,而每个幸存者空间将分配 2.5m。

示例:-XX:SurvivorRatio=6 意味着伊甸园空间的大小是每个幸存者空间的六倍。

7. -XX:NewRatio

描述:它提供了老年代与年轻代(Young Generation)大小的比例。比例为 2 意味着老年代是年轻代的两倍大。

示例:-XX:NewRatio=3 将老年代设置为年轻代大小的三倍。

引用类型

有四种引用类型:强引用、弱引用、软引用虚引用。这些引用类型之间的区别在于,它们指向的堆上的对象在不同的标准下才符合垃圾回收的条件。

强引用:它非常简单,就像我们在日常编程中使用它一样。任何附加了强引用的对象都不符合垃圾回收的条件。我们可以通过以下语句创建强引用

弱引用:它在下一次垃圾回收过程之后就不再存在了。如果我们不确定何时会再次请求数据,在这种情况下,我们可以为它创建一个弱引用。如果垃圾回收器处理,它会销毁该对象。当我们再次尝试检索该对象时,我们会得到一个 null 值。它定义在 java.lang.ref.WeakReference 类中。我们可以通过以下语句创建弱引用

软引用:当应用程序内存不足时,它会被收集。垃圾回收器不会收集软可达的对象。所有软引用的对象都会在抛出 OutOfMemoryError 之前被收集。我们可以通过以下语句创建软引用

虚引用:它在 java.lang.ref 包中可用。它定义在 java.lang.ref.PhantomReference 类中。只有虚引用指向的对象可以在垃圾回收器想要收集时被收集。我们可以通过以下语句创建虚引用

栈区域

栈区域在线程创建时生成。它可以是固定大小或动态大小。栈内存是按线程分配的。它用于存储数据和中间结果。它包含对堆对象的引用。它还持有值本身,而不是对堆中对象的引用。存储在栈中的变量具有一定的可见性,称为作用域。

栈帧:栈帧是一个数据结构,包含线程的数据。线程数据代表当前方法中线程的状态。

  • 它用于存储中间结果和数据。它还执行动态链接、方法返回值和分派异常。
  • 当一个方法被调用时,会创建一个新的栈帧。当方法调用完成时,该栈帧会被销毁。
  • 每个栈帧包含自己的局部变量数组(LVA)、操作数栈(OS)和栈帧数据(FD)。
  • LVA、OS 和 FD 的大小在编译时确定。
  • 在给定线程的控制中,任何时候只有一个栈帧(当前正在执行方法的栈帧)是活动的。该栈帧称为当前栈帧,其方法称为当前方法。方法的类称为当前类。
  • 如果当前方法调用了另一个方法,或者方法完成,栈帧将停止当前方法。
  • 线程创建的栈帧仅限于该线程,不能被任何其他线程引用。

本地方法栈

它也被称为 C 栈。它是用 Java 以外的语言编写的本地代码的栈。Java Native Interface (JNI) 调用本地栈。本地栈的性能取决于操作系统。

PC 寄存器

每个线程都有一个与之关联的程序计数器(PC)寄存器。PC 寄存器存储返回地址或本地指针。它还包含当前正在执行的 JVM 指令的地址。

垃圾回收器的工作原理

垃圾回收器概述

当 Java 程序执行时,它以不同的方式使用内存。堆是对象驻留的内存部分。它是唯一参与垃圾回收过程的内存部分。它也被称为可垃圾回收堆。所有垃圾回收都确保堆有尽可能多的可用空间。垃圾回收器的功能是查找并删除无法访问的对象。

对象分配

当分配一个对象时,JRockit JVM 会检查对象的大小。它区分小对象和大对象。大小取决于 JVM 版本、堆大小、垃圾回收策略和使用的平台。对象的大小通常在 2 到 128 KB 之间。

小对象存储在线程本地区域(TLA)中,TLA 是堆中的一块空闲块。TLA 不与其他线程同步。当 TLA 填满时,它会请求新的 TLA。

另一方面,不适合 TLA 的大对象直接分配到堆中。如果一个线程正在使用年轻代空间,它会直接存储在老年代空间。大对象需要更多的线程同步。

Java 垃圾回收器做什么?

JVM 控制垃圾回收器。JVM 决定何时执行垃圾回收。我们也可以请求 JVM 运行垃圾回收器。但无论如何,都不能保证 JVM 会遵守。如果 JVM 检测到内存不足,它会运行垃圾回收器。当 Java 程序请求垃圾回收器时,JVM 通常会很快地处理该请求。它不保证请求被接受。

需要理解的关键点是“对象何时才有资格进行垃圾回收?

每个 Java 程序都有多个线程。每个线程都有自己的执行栈。有一个线程在 Java 程序中运行,那就是 main() 方法。现在我们可以说,当没有活动线程可以访问一个对象时,该对象就有资格进行垃圾回收。垃圾回收器将该对象视为可删除对象。如果程序有一个引用变量指向一个对象,那么该引用变量对活动线程是可访问的,这个对象称为可达对象

这里出现一个问题:“Java 应用程序会耗尽内存吗?

答案是肯定的。垃圾回收系统会尝试在对象不再使用时将其从内存中移除。尽管如此,如果我们维护了许多活动对象,垃圾回收并不能保证有足够的内存。只有可用内存会被有效管理。

Java 垃圾回收监控

监控 Java 应用程序的垃圾回收对于性能调优和资源管理至关重要。作为实际示例,我们将演示如何使用 Java SE 下载中提供的演示应用程序来监控 GC 活动。我使用的应用程序是 Java2Demo.jar,位于下载 JDK 7 和 JavaFX Demos and Samples 后的 jdk1.7.0_55/demo/jfc/Java2D 目录中。但是,我们可以将这些监控技术应用于任何 Java 应用程序。

启动演示应用程序

要使用特定的内存设置运行 Java2Demo 应用程序,请使用以下命令

jstat

jstat 命令行工具是监控 JVM 内存和垃圾回收活动的宝贵资源。jstat 包含在标准 JDK 安装中,无需额外软件即可提供详细的见解。要使用 jstat,您首先需要找到要监控的 Java 应用程序的进程 ID。您可以通过在终端中执行 ps -eaf | grep java 命令轻松获取此信息。此步骤将帮助您识别要使用 jstat 监控的特定 Java 进程。

既然我们已经确定了 Java 应用程序的进程 ID 为 9582,我们可以继续使用 jstat 命令对其进行监控。以下是如何运行该命令

jstat 命令的最后一个参数指定输出的频率,因此将其设置为 1000 毫秒意味着它将每秒报告一次内存和垃圾回收数据。让我们探讨输出中的每一列代表什么

  • S0C 和 S1C:这些列以千字节为单位指示幸存者 0 和幸存者 1 空间当前的尺寸。
  • S0U 和 S1U:这些列以千字节为单位显示幸存者 0 和幸存者 1 空间当前的用法。重要的是要注意,通常其中一个幸存者空间总是空的。
  • EC 和 EU:这些列表示伊甸园空间的当前大小和使用情况(以千字节为单位)。请注意,EU(伊甸园使用量)的大小会增加,当它超过 EC(伊甸园容量)时,会触发 Minor GC,然后将 EU 大小减小。
  • OC 和 OU:这些列以千字节为单位显示老年代的当前大小和使用情况。
  • PC 和 PU:这些列以千字节为单位反映 Perm Gen 的当前大小和使用情况。
  • YGC 和 YGCT:YGC 列计算了年轻代中发生的垃圾回收事件的数量,而 YGCT 列衡量了这些操作花费的总时间。这两个指标通常一起增加,特别是当 EU 下降表明发生了 Minor GC 事件时。
  • FGC 和 FGCT:FGC 列计算了 Full GC 事件的数量,FGCT 列跟踪这些操作花费的累计时间。请注意,Full GC 事件的时间通常比年轻代 GC 的时间长得多。
  • GCT:此列合计了所有垃圾回收操作花费的总累计时间,包括 YGCT 和 FGCT 时间。

垃圾回收的类型

有五种类型的垃圾回收如下

  • Serial GC(串行 GC):它使用标记-清除方法来处理年轻代和老年代,即 Minor GC 和 Major GC。
  • Parallel GC(并行 GC):它与串行 GC 类似,除了它会生成 N(系统 CPU 核心数)个线程来进行年轻代垃圾回收。
  • Parallel Old GC(并行老年代 GC):它与并行 GC 类似,不同之处在于它使用多个线程来处理两个代。
  • Concurrent Mark Sweep (CMS) Collector(并发标记清除收集器):它对老年代进行垃圾回收。您可以使用 XX:ParalleCMSThreads=JVM 选项限制 CMS 收集器中的线程数。它也称为 Concurrent Low Pause Collector。
  • G1 Garbage Collector(G1 垃圾收集器):它在 Java 7 中引入。其目标是取代 CMS 收集器。它是一个并行、并发且是 CMS 收集器。没有年轻代和老年代空间。它将堆划分为多个大小相等的区域。它首先收集具有较少活动数据的区域。

标记和清除算法

JRockit JVM 使用标记和清除算法来执行垃圾回收。它包含两个阶段:标记阶段和清除阶段。

标记阶段:可从线程、本地句柄和其他 GC 根源访问的对象被标记为活动。每个对象树都有多个根对象。GC 根始终是可访问的。因此,任何以垃圾回收根为根的对象。它识别并标记所有正在使用的对象,其余的可以视为垃圾。

Memory Management in Java

清除阶段:在此阶段,遍历堆以查找活动对象之间的间隙。这些间隙记录在空闲列表中,可用于新对象的分配。

标记和清除有两种改进版本

  • 并发标记清除
  • 并行标记清除

并发标记清除

它允许线程在垃圾回收的大部分时间内继续运行。有以下类型的标记

  • 初始标记:它识别活动对象的根集。这是在线程暂停时完成的。
  • 并发标记:在此标记中,会跟踪根集的引用。它在堆中查找并标记其余的活动对象。这是在线程运行时完成的。
  • 预清除标记:它识别并发标记所做的更改。其他活动对象被标记和找到。这是在线程运行时完成的。
  • 最终标记:它识别预清除标记所做的更改。其他活动对象被标记和找到。这是在线程暂停时完成的。

并行标记清除

它利用系统中所有可用的 CPU 来尽快执行垃圾回收。它也称为并行垃圾收集器。并行垃圾回收执行时,线程不执行。

标记和清除的优点

  • 这是一个周期性过程。
  • 这是一个无限循环。
  • 算法执行期间不允许额外的开销。

标记和清除的缺点

  • 垃圾回收算法运行时会停止正常程序执行。
  • 它会对程序运行多次。

下一个主题Java 教程