Java(JVM)内存模型 - Java 中的内存管理
如果您想了解Java 垃圾收集的工作原理,那么了解JVM 内存模型、Java 内存管理非常重要。今天我们将研究 Java 中的内存管理、JVM 内存的不同部分以及如何监视和执行垃圾收集调整。
Java(JVM)内存模型
如上图所示,JVM 内存被划分为不同的部分。从广义上讲,JVM 堆内存在物理上分为两部分 -年轻代和老生代。
Java中的内存管理-年轻代
年轻代是所有新对象被创建的地方。当年轻代被填满时,就会执行垃圾收集。这种垃圾收集称为Minor GC。年轻代分为三部分 - Eden 内存和两个Survivor 内存空间。关于年轻代空间的要点:
- 新创建的对象大部分都位于Eden内存空间。
- 当 Eden 空间被对象填满时,将执行 Minor GC,并将所有幸存对象移动到其中一个幸存空间。
- Minor GC 还会检查幸存对象,并将它们移至另一个幸存空间。因此,同一时间,其中一个幸存空间始终是空的。
- 经过多次 GC 循环后仍存活的对象将被移至老生代内存空间。通常,这是通过设置年轻代对象的年龄阈值来实现的,超过该年龄,这些对象才有资格晋升到老生代。
Java中的内存管理-老一代
老生代内存包含长期存活且在多轮 Minor GC 后仍存活的对象。通常,当老生代内存已满时,会对其进行垃圾收集。老生代垃圾收集称为Major GC,通常需要较长时间。
停止世界事件
所有垃圾收集都是“Stop the World”事件,因为所有应用程序线程都会停止,直到操作完成。由于年轻代保留了短寿命对象,因此 Minor GC 非常快,应用程序不会受到影响。但是,Major GC 需要很长时间,因为它会检查所有活动对象。应尽量减少 Major GC,因为它会使您的应用程序在垃圾收集期间无响应。因此,如果您有一个响应迅速的应用程序,并且发生了很多 Major GC,您将注意到超时错误。垃圾收集器所花费的持续时间取决于用于垃圾收集的策略。这就是为什么有必要监视和调整垃圾收集器以避免高响应应用程序中的超时。
Java内存模型 - 永久代
永久代或“Perm Gen”包含 JVM 所需的应用程序元数据,用于描述应用程序中使用的类和方法。请注意,Perm Gen 不是 Java 堆内存的一部分。JVM 在运行时根据应用程序使用的类填充 Perm Gen。Perm Gen 还包含 Java SE 库类和方法。Perm Gen 对象在完整垃圾回收中被垃圾回收。
Java内存模型-方法区
方法区是永久代 (Perm Gen) 中的一部分空间,用于存储类结构(运行时常量和静态变量)以及方法和构造函数的代码。
Java内存模型 - 内存池
如果实现支持,内存池由 JVM 内存管理器创建,用于创建不可变对象池。字符串池就是这种内存池的一个很好的例子。内存池可以属于堆或永久代,具体取决于 JVM 内存管理器实现。
Java内存模型 - 运行时常量池
运行时常量池是类中常量池的每个类的运行时表示。它包含类运行时常量和静态方法。运行时常量池是方法区的一部分。
Java内存模型 - Java堆栈内存
Java 堆栈内存用于执行线程。它们包含方法特定的短暂值以及对从方法引用的堆中的其他对象的引用。您应该阅读堆栈和堆内存之间的区别。
Java 中的内存管理 - Java 堆内存开关
Java 提供了很多内存开关,我们可以使用这些开关来设置内存大小及其比例。一些常用的内存开关包括:
虚拟机切换 | 虚拟机交换机描述 |
---|---|
-Xms | 用于设置 JVM 启动时的初始堆大小 |
-Xmx | 用于设置最大堆大小。 |
-Xmn | 用于设置年轻代的大小,剩余空间用于老生代。 |
-XX:永久代 | 用于设置永久代内存的初始大小 |
-XX:最大永久代 | 用于设置 Perm Gen 的最大大小 |
-XX:幸存者比例 | 用于提供 Eden 空间和 Survivor 空间的比率,例如,如果年轻代大小为 10m,并且 VM 切换为 -XX:SurvivorRatio=2,则将为 Eden 空间保留 5m,为两个 Survivor 空间各保留 2.5m。默认值为 8。 |
-XX:新比例 | 用于提供旧代/新生代大小的比率。默认值为 2。 |
大多数情况下,上述选项就足够了,但如果您也想查看其他选项,请查看JVM 选项官方页面。
Java中的内存管理-Java垃圾收集
Java 垃圾收集是从内存中识别和删除未使用的对象并释放空间以分配给将来处理中创建的对象的过程。Java 编程语言的最佳特性之一是自动垃圾收集,这与其他编程语言(如 C)不同,在这些编程语言中内存分配和释放是一个手动过程。垃圾收集器是在后台运行的程序,它查看内存中的所有对象并找出未被程序任何部分引用的对象。所有这些未引用的对象都将被删除,并回收空间以分配给其他对象。垃圾收集的基本方法之一涉及三个步骤:
- 标记:这是第一步,垃圾收集器识别哪些对象正在使用以及哪些对象未使用。
- 正常删除:垃圾收集器删除未使用的对象并回收要分配给其他对象的可用空间。
- 压缩删除:为了获得更好的性能,删除未使用的对象后,可以将所有幸存的对象移动到一起。这将提高向较新的对象分配内存的性能。
简单的标记和删除方法存在两个问题。
- 第一个缺点是效率不高,因为大多数新创建的对象将变得未被使用
- 其次,在多个垃圾收集周期中使用过的对象很可能也会在未来的周期中使用。
上述简单方法的缺点在于,Java 垃圾收集是分代的,堆内存中存在年轻代和老生代空间。我已经在上面解释了如何基于 Minor GC 和 Major GC 扫描对象并将其从一个代空间移动到另一个代空间。
Java 中的内存管理 - Java 垃圾收集类型
在我们的应用程序中,可以使用五种类型的垃圾收集类型。我们只需要使用 JVM 开关来为应用程序启用垃圾收集策略。让我们逐一看一下。
- 串行 GC (-XX:+UseSerialGC):串行 GC 使用简单的标记-清除-压缩方法进行年轻代和老代垃圾收集,即 Minor 和 Major GC。串行 GC 在客户端计算机(例如我们的简单独立应用程序和 CPU 较小的计算机)中很有用。它适用于内存占用低的小型应用程序。
- 并行 GC (-XX:+UseParallelGC):并行 GC 与串行 GC 相同,不同之处在于它为年轻代垃圾收集生成 N 个线程,其中 N 是系统中的 CPU 核心数。我们可以使用
-XX:ParallelGCThreads=n
JVM 选项控制线程数。并行垃圾收集器也称为吞吐量收集器,因为它使用多个 CPU 来加快 GC 性能。并行 GC 使用单个线程进行老生代垃圾收集。 - 并行老生代 GC (-XX:+UseParallelOldGC):这与并行 GC 相同,只是它对年轻代和老生代垃圾收集都使用多个线程。
- 并发标记清除 (CMS) 收集器 (-XX:+UseConcMarkSweepGC):CMS 收集器也称为并发低暂停收集器。它为老一代执行垃圾收集。CMS 收集器尝试通过与应用程序线程同时执行大部分垃圾收集工作来最大限度地减少由于垃圾收集而导致的暂停。年轻一代的 CMS 收集器使用与并行收集器相同的算法。此垃圾收集器适用于我们无法承受较长暂停时间的响应式应用程序。我们可以使用
-XX:ParallelCMSThreads=n
JVM 选项限制 CMS 收集器中的线程数。 - G1 垃圾收集器 (-XX:+UseG1GC):Garbage First 或 G1 垃圾收集器从 Java 7 开始提供,其长期目标是取代 CMS 收集器。G1 收集器是一种并行、并发、增量压缩的低暂停垃圾收集器。Garbage First 收集器的工作方式与其他收集器不同,并且没有年轻代和老年代空间的概念。它将堆空间划分为多个大小相等的堆区域。调用垃圾收集时,它首先收集活动数据较少的区域,因此称为“垃圾优先”。您可以在Garbage-First Collector Oracle 文档中找到有关它的更多详细信息。
Java中的内存管理-Java垃圾收集监控
我们可以使用 Java 命令行以及 UI 工具来监视应用程序的垃圾收集活动。例如,我使用的是 Java SE 下载提供的演示应用程序之一。如果您想使用相同的应用程序,请转到 Java SE 下载页面并下载JDK 7 和 JavaFX 演示和示例。我使用的示例应用程序是Java2Demo.jar,它存在于jdk1.7.0_55/demo/jfc/Java2D
目录中。但是这是一个可选步骤,您可以为任何 Java 应用程序运行 GC 监视命令。我用来启动演示应用程序的命令是:
pankaj@Pankaj:~/Downloads/jdk1.7.0_55/demo/jfc/Java2D$ java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2Demo.jar
状态统计
我们可以使用jstat
命令行工具来监控 JVM 内存和垃圾收集活动。它随标准 JDK 一起提供,因此您无需执行任何其他操作即可获取它。执行时jstat
您需要知道应用程序的进程 ID,您可以使用命令轻松获取它ps -eaf | grep java
。
pankaj@Pankaj:~$ ps -eaf | grep Java2Demo.jar
501 9582 11579 0 9:48PM ttys000 0:21.66 /usr/bin/java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseG1GC -jar Java2Demo.jar
501 14073 14045 0 9:48PM ttys002 0:00.00 grep Java2Demo.jar
因此我的 java 应用程序的进程 ID 是 9582。现在我们可以运行jstat命令,如下所示。
pankaj@Pankaj:~$ jstat -gc 9582 1000
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
1024.0 1024.0 0.0 0.0 8192.0 7933.3 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
1024.0 1024.0 0.0 0.0 8192.0 8026.5 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
1024.0 1024.0 0.0 0.0 8192.0 8030.0 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
1024.0 1024.0 0.0 0.0 8192.0 8122.2 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
1024.0 1024.0 0.0 0.0 8192.0 8171.2 42108.0 23401.3 20480.0 19990.9 157 0.274 40 1.381 1.654
1024.0 1024.0 48.7 0.0 8192.0 106.7 42108.0 23401.3 20480.0 19990.9 158 0.275 40 1.381 1.656
1024.0 1024.0 48.7 0.0 8192.0 145.8 42108.0 23401.3 20480.0 19990.9 158 0.275 40 1.381 1.656
jstat 的最后一个参数是每次输出之间的时间间隔,因此它将每 1 秒打印一次内存和垃圾收集数据。让我们逐一查看每一列。
- S0C 和 S1C:此列显示 Survivor0 和 Survivor1 区域的当前大小(以 KB 为单位)。
- S0U 和 S1U:此列显示 Survivor0 和 Survivor1 区域的当前使用情况(以 KB 为单位)。请注意,其中一个 Survivor 区域始终为空。
- EC 和 EU:这些列显示 Eden 空间的当前大小和使用情况(以 KB 为单位)。请注意,EU 大小在增加,一旦超过 EC,就会调用 Minor GC,EU 大小就会减少。
- OC 和 OU:这些列显示老生代的当前大小和当前使用情况(以 KB 为单位)。
- PC 和 PU:这些列显示 Perm Gen 的当前大小和当前使用情况(以 KB 为单位)。
- YGC 和 YGCT:YGC 列显示年轻代发生的 GC 事件数。YGCT 列显示年轻代 GC 操作的累计时间。请注意,由于次要 GC,EU 值下降的同一行中,这两个值均增加。
- FGC 和 FGCT:FGC 列显示发生的完整 GC 事件的次数。FGCT 列显示完整 GC 操作的累计时间。请注意,与年轻代 GC 时间相比,完整 GC 时间太长。
- GCT:此列显示 GC 操作的总累计时间。请注意,它是 YGCT 和 FGCT 列值的总和。
jstat的优点是它也可以在没有 GUI 的远程服务器中执行。请注意,S0C、S1C 和 EC 的总和为 10m,如通过-Xmn10m
JVM 选项指定的。
Java VisualVM 与 Visual GC
如果您想在 GUI 中查看内存和 GC 操作,则可以使用jvisualvm
工具。Java VisualVM 也是 JDK 的一部分,因此您无需单独下载它。只需jvisualvm
在终端中运行命令即可启动 Java VisualVM 应用程序。启动后,您需要从工具 -< 插件选项安装Visual GC插件,如下图所示。安装Visual GC后,只需从左侧栏打开应用程序并转到Visual GC部分。您将获得 JVM 内存和垃圾收集详细信息的图像,如下图所示。
Java 垃圾回收调优
Java 垃圾回收调优应该是您用来提高应用程序吞吐量的最后一个选项,并且仅当您看到由于较长的 GC 时间导致应用程序超时而导致性能下降时才使用。如果您java.lang.OutOfMemoryError: PermGen space
在日志中看到错误,请尝试使用 -XX:PermGen 和 -XX:MaxPermGen JVM 选项监视并增加 Perm Gen 内存空间。您也可以尝试使用-XX:+CMSClassUnloadingEnabled
CMS 垃圾收集器并检查其性能。如果您看到很多完整的 GC 操作,那么您应该尝试增加老生代内存空间。总体而言,垃圾回收调优需要花费大量的精力和时间,并且没有硬性规定。您需要尝试不同的选项并进行比较,以找出最适合您应用程序的选项。这就是 Java 内存模型、Java 中的内存管理和垃圾回收的全部内容,我希望它能帮助您理解 JVM 内存和垃圾回收过程。