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

时间:2020-02-23 14:34:30  来源:igfitidea点击:

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

Java(JVM)内存模型

如上图所示,JVM内存分为多个单独的部分。
从广义上讲,JVM堆内存在物理上分为两个部分-青年一代和老一代。

Java中的内存管理–年轻的一代

年轻一代是创建所有新对象的地方。
当年轻一代被填满时,将执行垃圾收集。
此垃圾回收称为次要GC。
年轻一代分为三部分-伊甸园记忆和两个幸存者记忆空间。

关于年轻一代空间的要点:

  • 大多数新创建的对象位于Eden存储空间中。

  • 当Eden空间中充满对象时,将执行次要GC,并将所有幸存者对象移至其中一个幸存者空间。

  • 次要GC还检查幸存者对象并将其移至其他幸存者空间。
    因此,一次,幸存者空间始终是空的。

  • 在许多次GC循环后仍然存在的对象将移至旧代存储空间。
    通常,可以通过设置年轻一代对象的年龄阈值,然后才有资格晋升为老一代对象。

Java中的内存管理–老一代

上一代内存包含经过多次次要次要GC寿命长且可以幸存的对象。
通常,垃圾收集会在旧版内存已满时执行。
旧世代垃圾回收称为专业垃圾回收,通常需要更长的时间。

停止世界事件

所有垃圾回收都是"世界停止"事件,因为所有应用程序线程都将停止,直到操作完成。

由于年轻世代保留了短暂的对象,因此Minor GC的运行速度非常快,因此应用程序不会受到此影响。

但是,专业GC需要很长时间,因为它会检查所有活动对象。
应该最小化主要GC,因为它会使您的应用程序在垃圾回收期间无响应。
因此,如果您有响应式应用程序,并且发生了很多"大型垃圾回收",您将注意到超时错误。

垃圾收集器花费的时间取决于用于垃圾收集的策略。
因此,有必要监视和优化垃圾收集器,以避免高响应应用程序中的超时。

Java内存模型–永久生成

永久生成或者"永久生成"包含JVM所需的应用程序元数据,用于描述应用程序中使用的类和方法。
请注意,Perm Gen并非Java Heap内存的一部分。

JVM在运行时根据应用程序使用的类填充PermGen。
Perm Gen还包含Java SE库类和方法。
Perm Gen对象是在完整垃圾收集中收集的垃圾。

Java内存模型–方法区域

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

Java内存模型–内存池

内存池由JVM内存管理器创建,以在实现支持的情况下创建不可变对象池。
字符串池就是这种内存池的一个很好的例子。
内存池可以属于Heap或者Perm Gen,具体取决于JVM内存管理器的实现。

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

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

Java内存模型– Java堆栈内存

Java Stack内存用于执行线程。
它们包含短期的方法特定值,以及对从该方法引用的堆中其他对象的引用。
您应该阅读堆栈与堆内存之间的区别。

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

Java提供了很多内存开关,我们可以用来设置内存大小及其比率。
一些常用的内存开关是:

VM SwitchVM Switch Description
-XmsFor setting the initial heap size when JVM starts
-XmxFor setting the maximum heap size.
-XmnFor setting the size of the Young Generation, rest of the space goes for Old Generation.
-XX:PermGenFor setting the initial size of the Permanent Generation memory
-XX:MaxPermGenFor setting the maximum size of Perm Gen
-XX:SurvivorRatioFor 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:NewRatioFor providing ratio of old/new generation sizes. The default value is 2.

在大多数情况下,以上选项就足够了,但是如果您也想查看其他选项,请查看JVM Options Official Page。

Java中的内存管理– Java垃圾回收

Java Garbage Collection是从内存中识别并删除未使用的对象的过程,并分配自由空间以分配给将来处理中创建的对象。
Java编程语言的最佳功能之一是自动垃圾收集,这与其他编程语言(例如C)不同,后者的内存分配和释放是手动过程。

垃圾收集器是在后台运行的程序,它可以查看内存中的所有对象,并找出该程序任何部分未引用的对象。
删除所有这些未引用的对象,并回收空间以分配给其他对象。

垃圾收集的基本方法之一涉及三个步骤:

  • 标记:这是第一步,垃圾收集器将识别正在使用的对象和未使用的对象。

  • 正常删除:垃圾收集器删除未使用的对象,并回收要分配给其他对象的可用空间。

  • 压缩压缩:为了获得更好的性能,在删除未使用的对象之后,可以将所有剩余的对象移动到一起。
    这将提高为较新对象分配内存的性能。

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

  • 第一个是效率不高,因为大多数新创建的对象将变为未使用状态
  • 其次,在多个垃圾回收周期中使用的对象也很有可能在将来的周期中使用。

使用简单方法的上述缺点是Java垃圾回收是分代的,并且堆内存中有Young Generation和Old Generation空间的原因。
上面我已经解释了如何根据次要GC和主要GC将对象扫描并从一个世代空间移动到另一个世代空间。

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

我们可以在应用程序中使用五种类型的垃圾收集类型。
我们只需要使用JVM开关来为应用程序启用垃圾回收策略即可。
让我们一一看一下。

  • 串行GC(-XX:+ UseSerialGC):串行GC使用简单的mark-sweep-compact方法进行青年和老年人垃圾收集,即次要和主要垃圾回收。
    串行GC在客户端计算机中非常有用,例如我们的简单独立应用程序和CPU较小的计算机。
    这对于内存占用量少的小型应用程序非常有用。

  • 并行GC(-XX:+ UseParallelGC):并行GC与串行GC相同,只不过它为年轻一代垃圾回收生成N个线程,其中N是系统中CPU内核的数量。
    我们可以使用JVM选项-XX:ParallelGCThreads = n来控制线程数。
    并行垃圾收集器也称为吞吐量收集器,因为它使用多个CPU来提高GC性能。
    并行GC使用单个线程进行旧式垃圾回收。

  • 并行旧GC(-XX:+ UseParallelOldGC):与并行GC相同,不同之处在于它同时对Young Generation和Old Generation垃圾回收使用多个线程。

  • 并发标记扫描(CMS)收集器(-XX:+ UseConcMarkSweepGC):CMS收集器也称为并发低暂停收集器。
    它为旧一代进行垃圾收集。
    CMS收集器通过与应用程序线程同时执行大多数垃圾收集工作来尝试最大程度地减少由于垃圾收集而造成的暂停。
    年轻一代的CMS收集器使用与并行收集器相同的算法。
    此垃圾收集器适用于响应性应用程序,在这些应用程序中我们无法承受更长的暂停时间。
    我们可以使用-XX:ParallelCMSThreads = nJVM选项来限制CMS收集器中的线程数。

  • G1垃圾收集器(-XX:+ UseG1GC):Java 7提供了Garbage First或者G1垃圾收集器,其长期目标是替换CMS收集器。
    G1收集器是并行的,并发的,渐进压缩的低中断垃圾收集器。
    垃圾优先收集器的工作原理与其他收集器不同,并且没有年轻一代和老一代空间的概念。
    它将堆空间分成多个相等大小的堆区域。
    调用垃圾收集时,它首先收集活动数据较少的区域,因此称为"垃圾优先"。
    您可以在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监视命令。

我用来启动演示应用程序的命令是:

hyman@hyman:~/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命令。

hyman@hyman:~$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命令,如下所示。

hyman@hyman:~$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:此列以KB为单位显示Survivor0和Survivor1区域的当前大小。

  • S0U和S1U:此列以KB为单位显示Survivor0和Survivor1区域的当前使用情况。
    请注意,幸存者区域之一始终是空的。

  • EC和EU:这些列显示以字节为单位的Eden空间的当前大小和使用情况。
    请注意,EU大小正在增加,一旦超过EC,就会调用次要GC,EU大小会减小。

  • OC和OU:这些列以KB为单位显示了Old generation的当前大小和当前使用情况。

  • PC和PU:这些列显示Perm Gen的当前大小和当前用法(以KB为单位)。

  • YGC和YGCT:YGC列显示年轻一代中发生的GC事件的数量。
    YGCT列显示Young代GC操作的累积时间。
    请注意,由于GC较小,它们都在同一行中增加了,而EU值下降了。

  • FGC和FGCT:FGC列显示发生Full GC事件的次数。
    FGCT列显示Full GC操作的累积时间。
    请注意,与年轻一代GC计时相比,"完全GC"时间太高。

  • GCT:此列显示GC操作的总累积时间。
    请注意,它是YGCT和FGCT列值的总和。

jstat的优点是它也可以在没有GUI的远程服务器中执行。
注意,通过-Xmn10mJVM选项指定,S0C,S1C和EC的总和为10m。

带有Visual GC的Java VisualVM

如果您想在GUI中查看内存和GC操作,则可以使用jvisualvm工具。
Java VisualVM也是JDK的一部分,因此您无需单独下载它。

只需在终端中运行" jvisualvm"命令以启动Java VisualVM应用程序。
启动后,您需要从Tools-<Plugins选项安装Visual GC插件,如下图所示。

安装Visual GC之后,只需从左侧列打开应用程序,然后转到Visual GC部分。
您将获得JVM内存和垃圾回收详细信息的图像,如下图所示。

Java垃圾收集优化

只有在由于较长的GC定时导致应用程序超时而导致性能下降时,才应该使用Java垃圾收集优化来提高应用程序的吞吐量。

如果您在日志中看到" java.lang.OutOfMemoryError:PermGen space"错误,请尝试使用-XX:PermGen和-XX:MaxPermGen JVM选项监视并增加Perm Gen的内存空间。
您也可以尝试使用-XX:+ CMSClassUnloadingEnabled并检查其在CMS Garbage collector中的运行情况。

如果看到很多Full GC操作,则应尝试增加Old generation的内存空间。

总体上,垃圾回收调整需要大量的精力和时间,对此没有硬性规定。
您将需要尝试不同的选项并进行比较,以找出最适合您的应用程序的选项。