Java虚拟机内存模型及垃圾回收监控调优

时间:2021-06-21 14:07:56

Java虚拟机内存模型及垃圾回收监控调优

如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要。今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优。

JVM内存模型        

Java虚拟机内存模型及垃圾回收监控调优

正如你上图所看到的,JVM内存可以划分为不同的部分,广义上,JVM堆内存可以划分为两部分:年轻代和老年代(Young Generation and Old Generation)

年轻代(Young Generation)

年轻代用于存放由new所生成的对象。当年轻代空间满时,垃圾回收就会执行。这个垃圾回收我们称之为 最小化垃圾回收(Minor GC)。年轻代又可以分为三分部: 一个Eden内存区(Eden Memory)和两个Survivor 内存区(Survivor Memory)。

 

关于年轻代的重点:

  • 大多数新创建的对象都存放在Eden内存区。
  • 当Eden区存满对象时,最小化GC将会被执行,所有存活的对象被移到其中一个Survivor区。
  • 最小化GC同时也会检查存活的对象并将它们移到另一个Survivor区,所以在一段时间,其中一个Survivor区总是空的。
  • 存活的对象经过多次GC后,将会被移到老年代(Old Generation)通常年轻代转化多长时间变为老年代都会设置一个阀值。

老年代(Old Generation)

老年代内存包含那些经过多次最小化GC且存活的对象。 通常当老年代内存满时垃圾回收会被执行。我们称之为大GC (Major GC),通常这个过程会花费很长的时间。

Stop the World Event

所有的垃圾回收都是“阻塞”事件(“Stop the World” events) ,因为所有应用程序线程必须停止,直到垃圾回收操作完成之后才能继续。

由于年轻代总是保存着短期存活的对象,最小化GC非常快,应用程序也不会受到影响。然而大GC(Major GC)由于它要检查所有存活的对象,所以需要花费很长的时间。 大GC会在垃圾回收期间使得你的应用程序没有任何响应,应最大化的减少此类GC。如果你有一个即时响应应用程序且总是有很多的大GC在执行。你会发现存在超时错误。

垃圾回器扫行的时间依赖于GC所使用的策略。这就是为什么对于那些高响应用程序来说,GC的监控与调优显得非常必要。

永久代(Permanent Generation)

永久代或持久代包含JVM用来描述应用程序中使用的类和方法的元数据。注意 永久代不是JAVA堆内存的一部分。

JVM在运行期间依据应用程序所使用的类来存入永久代,同时也包含Java SE库类各方法。 永久代中的对象通过全GC(Full GC)来进行垃圾回收。

方法区Method Area

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

运行时常量池Runtime Constant Pool

运行时常量池是类中常量池在编译期的表示,它包含类的运行时常量,静态方法,它是方法区的一部分。

Java栈内存Java Stack Memory

Java 栈内存用于执行线程。包含方法短期存活的特定值及方法中所涉及的指向堆中其它对象的引用。

Java 堆内存Switches(Java Heap Memory Switches)

提供了大量的内存Switch,我们可以用来设置内存大小及它们的比率。一此常用的内存Switches如下。

VM Switch VM Switch 描述
-Xms 设置JVM启动时堆初始值。
-Xmx 设置堆的最大值
-Xmn 设置年轻代的大小
-XX:PermGen 设置永久代内存的初始值
-XX:MaxPermGen 设置永久代内存的最大值
-XX:SurvivorRatio 用于提供Eden区和Survivor区的比例, 例如,如果年轻代的大小为 10M,VM switch is -XX:SurvivorRatio=2 那么 5 M 分配给 Eden 区,每一个Survivor 区为 2.5 M .默认比例大小为  8。
-XX:NewRatio 老年代/新生代的比例. 默认值是 2.

大多数情况下,上述选项是非常有用的。如果你也想试试其它参数可以查看 JVM官方主页

JAVA GC

Java GC是从内存中标记、移除非可用对象并释放所分配的空间用于后续创建对像的过程。Java编程语言最大的功能之一就是自动垃圾回收。不像其它类似C的编程语言,它们的内存分配与回收可以人为操作。

GC是后台执行用于检查内存中的所有对象并找出未被任务程序所引用的对象。所有这些未引用的对象将被删除,同时空间也会被回收用于分配给其它对象。

 关于标记、删除方法有两个问题:

1.由于新创建的对象将会变成不可用,所有效率上不是很高。

2.多次GC后可用的对象很能在后续的GC后仍然可用。

上述方法的不足在于Java的GC是分代的,在堆内存中分年轻代和老年代。 前面我已经解释过基于最小化GC(Minor GC)和大GC(Major GC)对象是如果被扫描并从一个分代区移到别一个分区。

Java GC的类型

在应用程序中我们可以使用如下五种类型的GC。我们可以使用JVM相关设置为应用程序开启GC策略,现在我们就来具体的看看.

  1. 串行GC (Serial GC -XX:+UseSerialGC): 串行GC为年轻代和老年代GC提供简单的标记-清除-压缩方法。串行GC在独立应用程序运行的少量CPU的客户机器上非常有效。对于那些低内存占用的应用程序非常有利。
  2. 并行GC (Parallel GC -XX:+UseParallelGC): 并行GC中大部分实现与串行GC相同,除了年轻代的垃圾回收使用N个线程,N是系统CPU内核的数量。我们可以通过 JVM 的 -XX:ParallelGCThreads=n 选项来控制线程的数量。并行的垃圾回收器因为使用多CPU来加速GC的实现所以也称为并行收集器(throughput collector).并行GC使用单线程来处理老年代的垃圾回收.
  3. 并行Old GC (Parallel Old GC  -XX:+UseParallelOldGC):此类型的GC中年轻代和老年代GC都使用多线程进行。其它与并行GC相同,
  4. 并发标记清除收集器(Concurrent Mark Sweep (CMS) Collector) (-XX:+UseConcMarkSweepGC): CMS收集器是并发短暂停收集器。它为老年代进行垃圾回收。CMS收集器 通过与应用程序线程并发执行垃圾回收 从而缩短因GC而出现的暂停时间。CMS收集器在年轻代使用与并行收集器相同的算法。这类垃圾收集器适合不能容忍长时间暂停的响应程序。我们可以通过JVM中的 -XX:ParallelCMSThreads=n 来设置CMS收集器中的线程数。
  5. G1 垃圾收集器G1 Garbage Collector (-XX:+UseG1GC) Java 7中可以使用G1 垃圾收集器。它的最终目标是替换掉CMS收集器。G1收集器是一个并行、并发、增量压缩、低暂停的垃圾收集器。

G1垃圾收集器不同于其它收集器,它没有年轻代、老年代空间,它将堆空间划分成多个相同大小的堆区域(heap regions),当调用一次垃圾回收,它首先收集实时性不是很高的区域的数据,因此称为”Garbage First”。有关详细的可参考我的另外一篇Blog:Java垃圾收集器之--Garbage-First Collector 。

JAVA 垃圾收集监控

我们可以使用Java命令行和UI工具来监控应用程序的垃圾收集活动。下面的例子中,我使用Java SE Downloads 中一个演示程序。

如果你想使用同样的程序,前往 Java SE Downloads  页面下载JDK 7 and JavaFX Demos and Samples 我使用的和序示例是Java2Demo.jar  可以在jdk1.7.0_55/demo/jfc/Java2D 目录中找到。当然了这步可选。你可以选用任何Java程序执行GC监控命令。

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

1
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内存和GC活动。标准的JDK中含有此命令。因此可以直接使用。

运行 jstat 之前你需要知道程序的进程ID号。你可以运行 ps -eaf | grep java 命令来获取。

1
2
3
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 and S1C: 这列显示当前 Survivor0 and Survivor1 区域的大小(KB)
    • S0U and S1U: 这列显示 Survivor0 and Survivor1 区域的使用情况(KB). 其中一个Survivor区域总是空的。
    • EC and EU: 这列显示Eden区的当前大小及使用情况(KB). 注意 EU 的大小逐渐增加,当达到EC大小, 最小化 GC 被调用 ,EU 的大小减小。
    • OC and OU: 这列显示了老年代的当前大小及使用情况(KB)
    • PC and PU: 这列显示了 持久化代(Perm Gen) 的当前大小及使用情况(KB)
    • YGC and YGCT: YGC 列显示年轻代发生GC事件的数量。 YGCT 列显示年轻代发生GC操作累计时间.  注意这两列值都在增加与EU值减少是在同一行。这主要是最小化GC的原因。
    • FGC and FGCT: FGC 列显示了FUll GC发生的数量. FGCT 列显示了FULL GC操作的累计时间. 注意Full GC 所用的时间相比年轻代GC所用的时间要大的多。
    • GCT:  这列显示了GC 操作总累计时间。 注意它是 YGCT 和 FGCT 值的总和。

jstat的优点在于它可以在无GUI的远程服务器上执行。注意S0C, S1C 和EC之和为 10M ,与我们通过JVM选项 -Xmn10m 设置的值一样。

Java VisualVM with Visual GC

如果你想在GUI下查看内存及GC操作。 那么你可以使用 jvisualvm 工具。Java VisualVM 也同样包含在JDK中,你不需要单独下载。

只需要运行在终端上执行jvisualvm 命令来启动Java VisualVM程序。一旦启动,你需要通过Tools--》Plugins选项安装Visual GC 插件,正如下图所示。

Java虚拟机内存模型及垃圾回收监控调优

   Visual GC 安装完毕后,左边列中打开程序前往Visual GC 部分。你将会得到如下图所示的JVM内存及GC截图。

Java虚拟机内存模型及垃圾回收监控调优

Java GC 调优是提高应用程序吞吐量的最后选择,只有当你发现长时间的GC导致性能下降而产生应用程序超时。

你会在日志中看到java.lang.OutOfMemoryError: PermGen space的错误信息。然后可以尝试监控并通过使用JVM 选项  -XX:PermGen and -XX:MaxPermGen  来增加Perm Gen内存空间。你或许也可以尝试使用-XX:+CMSClassUnloadingEnabled 来检查使用CMS垃圾收集的性能如何?

如果你发现大量的FUll GC操作,你可以试着增加老年代内存空间。

总之GC调优需要花费大量的精力和时间,这里绝没有什么硬性或者快速的规则。你需要尝试不同的选项,比较他们,并找对你应用程序来说最好的那个。

这就是所有有关Java内存模型和垃圾回收。我希望这有助于你理解JVM内存模型及垃圾回收的过程。谢谢。

原文链接: JournalDev 翻译: TonySpark
译文链接: http://www.cnblogs.com/tonyspark/p/3731696.html

转载请保留原文出处、译者和译文链接。]