Java (JVM) 内存模型 – Java 中的内存管理

了解JVM内存模型Java内存管理对于理解Java垃圾收集的工作非常重要。今天我们将深入研究Java中的内存管理、JVM内存的不同部分以及如何监控和执行垃圾收集调优。

Java(JVM)内存模型

如上图所示,JVM内存被划分为不同的部分。在宏观层面上,JVM堆内存被物理上划分为两部分 – 年轻代老年代

Java中的内存管理 – 年轻代

年轻代是所有新对象创建的地方。当年轻代填满时,会执行垃圾收集。这种垃圾收集称为Minor GC。年轻代被划分为三部分 – 伊甸园空间和两个幸存者空间。有关年轻代空间的重要要点:

  • 大多数新创建的对象都位于Eden内存空间。
  • 当Eden空间被对象填满时,将执行Minor GC,并将所有幸存对象移动到其中一个幸存者空间。
  • Minor GC还会检查幸存对象,并将它们移动到另一个幸存者空间。因此,在任何时候,一个幸存者空间总是空的。
  • 经过多次GC后幸存的对象将被移动到老年代内存空间。通常情况下,这是通过设置年轻代对象的年龄阈值来完成的,使它们有资格晋升到老年代。

Java内存管理 – 老年代

老年代内存包含了长时间存活并在多轮Minor GC后幸存下来的对象。通常情况下,当老年代内存满时会执行垃圾收集。老年代垃圾收集被称为Major GC,通常需要较长时间。

停止世界事件

所有的垃圾回收都是“停止-全球”事件,因为所有应用程序线程都会在操作完成之前停止。由于年轻代保存的是短寿命对象,所以次要垃圾回收非常快,应用程序不会受到影响。然而,主要垃圾回收需要很长时间,因为它会检查所有存活对象。应尽量减少主要垃圾回收,因为它会使应用程序在垃圾回收期间变得不响应。因此,如果您有一个响应迅速的应用程序,并且主要垃圾回收频繁发生,您将注意到超时错误。垃圾回收器所花费的时间取决于用于垃圾回收的策略。因此,有必要监视和调整垃圾回收器,以避免高度响应的应用程序出现超时。

Java 内存模型 – 永久代

永久代或“Perm Gen”包含 JVM 需要描述应用程序中使用的类和方法的应用程序元数据。请注意,Perm Gen 不是 Java 堆内存的一部分。Perm Gen 在运行时由 JVM 根据应用程序使用的类进行填充。Perm Gen 还包含 Java SE 库类和方法。Perm Gen 对象在完整垃圾回收中进行垃圾收集。

Java内存模型 – 方法区

方法区是永久代中的一部分空间,用于存储类结构(运行时常量和静态变量)以及方法和构造函数的代码。

Java内存模型 – 内存池

内存池由JVM内存管理器创建,用于创建不可变对象池(如果实现支持)。字符串池是这种内存池的一个很好的例子。内存池可以属于堆内存或永久代,这取决于JVM内存管理器的实现方式。

Java内存模型 – 运行时常量池

运行时常量池是每个类的运行时常量池的表示,它包含类的运行时常量和静态方法。运行时常量池是方法区的一部分。

Java内存模型 – Java堆栈内存

Java堆栈内存用于执行线程。它们包含方法特定的值,这些值具有短暂的生命周期,并引用堆中从方法引用的其他对象。您应该阅读堆栈内存和堆内存之间的区别

Java内存管理 – Java堆内存开关

Java提供了许多内存开关,我们可以使用它们来设置内存大小及其比例。一些常用的内存开关包括:

VM Switch VM Switch Description
-Xms For setting the initial heap size when JVM starts
-Xmx For setting the maximum heap size.
-Xmn For setting the size of the Young Generation, rest of the space goes for Old Generation.
-XX:PermGen For setting the initial size of the Permanent Generation memory
-XX:MaxPermGen For setting the maximum size of Perm Gen
-XX:SurvivorRatio For providing ratio of Eden space and Survivor Space, for example if Young Generation size is 10m and VM switch is -XX:SurvivorRatio=2 then 5m will be reserved for Eden Space and 2.5m each for both the Survivor spaces. The default value is 8.
-XX:NewRatio For providing ratio of old/new generation sizes. The default value is 2.

大多数情况下,上述选项已足够,但如果您想查看其他选项,请参阅JVM选项官方页面

Java内存管理 – Java垃圾回收

Java垃圾回收是识别并清除内存中未使用的对象,释放空间以供将来处理中创建的对象分配的过程。Java编程语言的最佳特性之一是自动垃圾回收,与其他编程语言(如C)不同,其中内存分配和释放是一个手动过程。垃圾收集器是在后台运行的程序,它查看内存中的所有对象,并找出程序的任何部分都不引用的对象。所有这些未引用的对象都将被删除,并为分配给其他对象而回收空间。垃圾收集的基本方式包括三个步骤:

  1. 标记:这是第一步,垃圾收集器确定哪些对象正在使用,哪些对象未被使用。
  2. 正常删除:垃圾收集器删除未使用的对象,并回收空闲空间以分配给其他对象。
  3. 带压缩删除:为了获得更好的性能,在删除未使用的对象后,所有幸存的对象可以移动到一起。这将提高内存分配给新对象的性能。

简单的标记和删除方法存在两个问题。

  1. 第一个问题是,这种方法效率不高,因为大多数新创建的对象都会变成未使用的。
  2. 其次,被多次垃圾回收周期使用的对象很可能在未来的周期中仍然被使用。

以上这种简单方法的缺点是Java垃圾回收是面向代的的原因,我们在堆内存中有年轻代和老年代空间。Java垃圾回收是面向代的,我们在堆内存中有年轻代老年代空间。我已经在上面解释了对象是如何根据Minor GC和Major GC从一个代空间移动到另一个代空间的。

Java内存管理 – Java垃圾回收类型

我们的应用程序可以使用五种垃圾回收类型。我们只需要使用JVM开关为应用程序启用垃圾回收策略。让我们逐个看看它们。

  1. 串行GC(-XX:+UseSerialGC):串行GC使用简单的标记-清除-整理方法进行年轻代和老年代的垃圾回收,即Minor GC和Major GC。串行GC适用于客户端机器,例如我们的简单独立应用程序和CPU较小的机器。它适用于内存占用较小的小型应用程序。
  2. Parallel GC (-XX:+UseParallelGC):并行GC与串行GC相同,只是它会为年轻代垃圾收集生成N个线程,其中N是系统中的CPU核心数量。我们可以使用-XX:ParallelGCThreads=n JVM选项来控制线程数量。并行垃圾收集器也称为吞吐量收集器,因为它利用多个CPU来加速GC性能。并行GC在老年代垃圾收集中使用单个线程。
  3. Parallel Old GC (-XX:+UseParallelOldGC):这与并行GC相同,只是它同时为年轻代和老年代垃圾收集使用多个线程。
  4. Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC):CMS收集器也称为并发低停顿收集器。它负责老年代的垃圾收集。CMS收集器试图通过与应用程序线程同时进行大部分垃圾收集工作来最小化由于垃圾收集而引起的暂停。CMS收集器在年轻代上使用与并行收集器相同的算法。这种垃圾收集器适用于对长暂停时间无法接受的响应性应用程序。我们可以使用-XX:ParallelCMSThreads=n JVM选项来限制CMS收集器中的线程数。
  5. G1垃圾收集器(-XX:+UseG1GC):Garbage First或G1垃圾收集器从Java 7开始可用,其长期目标是取代CMS收集器。G1收集器是一种并行、并发和增量压缩的低停顿垃圾收集器。Garbage First收集器不像其他收集器那样工作,没有年轻代和老年代空间的概念。它将堆空间分成多个大小相等的堆区域。当调用垃圾收集时,它首先收集具有较少活跃数据的区域,因此称为“Garbage First”。您可以在Garbage-First Collector Oracle Documentation中找到更多详细信息。

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

我们可以使用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的最后一个参数是每个输出之间的时间间隔,因此它将每秒打印一次内存和垃圾收集数据。 让我们逐个浏览每个列。

  • S0C和S1C:此列显示Survivor0和Survivor1区域的当前大小(以KB为单位)。
  • S0U和S1U:此列显示Survivor0和Survivor1区域的当前使用情况(以KB为单位)。 请注意,其中一个幸存者区域始终为空。
  • EC 和 EU: 这些列显示了以 KB 为单位的 Eden 空间的当前大小和使用情况。请注意,EU 的大小正在增加,一旦它超过 EC,将调用 Minor GC,并减少 EU 的大小。
  • OC 和 OU: 这些列显示了以 KB 为单位的老年代的当前大小和当前使用情况。
  • PC 和 PU: 这些列显示了以 KB 为单位的 Perm Gen 的当前大小和当前使用情况。
  • YGC 和 YGCT: YGC 列显示了年轻代发生的 GC 事件数量。YGCT 列显示了年轻代 GC 操作的累积时间。请注意,在由于 Minor GC 导致 EU 值下降的同一行中,它们都会增加。
  • FGC 和 FGCT: FGC 列显示了发生的 Full GC 事件数量。FGCT 列显示了 Full GC 操作的累积时间。请注意,与年轻代 GC 时间相比,Full GC 时间太长。
  • GCT: 此列显示了 GC 操作的总累积时间。请注意,它是 YGCT 和 FGCT 列值的总和。

jstat 的优点是它也可以在没有 GUI 的远程服务器上执行。请注意,S0C、S1C 和 EC 的总和为 10m,正如通过 -Xmn10m JVM 选项指定的那样。

Java VisualVM with 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错误,则尝试监视并增加Perm Gen内存空间,使用-XX:PermGen和-XX:MaxPermGen JVM选项。您还可以尝试使用-XX:+CMSClassUnloadingEnabled,并检查CMS垃圾收集器的性能如何。如果看到很多Full GC操作,那么您应该尝试增加老年代内存空间。总体而言,垃圾回收调优需要大量的工作和时间,没有硬性规定。您需要尝试不同的选项并进行比较,找出最适合您应用的最佳选项。关于Java内存模型、Java内存管理和垃圾回收就介绍到这里,希望能帮助您理解JVM内存和垃圾回收过程。

Source:
https://www.digitalocean.com/community/tutorials/java-jvm-memory-model-memory-management-in-java