Java内存模型与内存管理

如果您想弄清楚Java垃圾回收的的工作原理,那么理解JVM内存模型以及Java内存管理非常重要。 今天我们将探讨一下Java中的内存管理、JVM内存的组成以及如何监控和进行垃圾回收调优。

Java(JVM)内存模型

Java Memory Model, JVM Memory Model, Memory Management in Java, Java Memory Management

正如上图所示,JVM内存划分为多个不同部分。 从广义上讲,JVM堆内存内存在物理上分为两部分——新生代(Young Generation)和老年代(Old Generation)。

Java 内存管理——年轻代

年轻代是创建新对象的地方。 当年轻代被填满时,将会进行垃圾回收,称为Minor GCYoung Generation分为三个部分——Eden内存空间和两个Survivor内存空间。

关于新生代(Young Generation)的要点:

  • 大多数新创建的对象都位于Eden内存空间中。
  • Eden空间填满对象时,执行Minor GC并将所有幸存者对象移动到其中一个Survivor内存空间。
  • Minor GC还会检查幸存者对象并将其移动到其他Survivor内存空间。 所以,在同一时间内,总有一个Survivor内存空间是空的。
  • 在多次GC循环后幸存的对象将被移动到老年代(Old Generation)。 通常,新生代(Young Generation)对象达到设定的年龄阈值后才有资格晋升到老年代(Old Generation)。

Java 内存管理——老年代

老年代(Old Generation)包含在多次Minor GC循环之后长期存在并存活下来的对象。通常,当老年代(Old Generation)被填满后,也会执行垃圾回收,称为Major GC,通常花费时间较长。

停顿(Stop the World Event)

所有垃圾回收都是“Stop the World”事件,因为所有应用程序线程都会停止,直到操作完成。由于新生代(Young Generation)主要保存短暂存活的对象,因此Minor GC非常快,应用程序不会受此影响。

然而,Major GC需要花费很长时间,因为它会检查所有活动对象。 Major GC应该最小化,因为它会使您的应用程序在垃圾回收期间没有响应。 因此,如果您有响应式应用程序而且进行了大量Major GC,您会注意到超时错误。

垃圾收集器进行垃圾回收时持续的时间取决于采用的垃圾回收策略。为了避免高响应应用程序中的超时,很有必要去监控、调整垃圾收集器。

Java 内存模型——永久代

永久代(Permanent GenerationPerm Gen)包含JVM描述应用程序中类和方法所需的应用程序元数据。 请注意,Perm Gen不是Java堆内存的一部分。

Perm Gen在运行时,由JVM根据应用程序使用的类填充。 Perm Gen还包含Java SE库中的类和方法。 Perm Gen中的对象在完全垃圾回收(full garbage collection)中将会被回收。

Java 内存模型——方法区

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

Java 内存模型——内存池

内存池(Memory Pool)由JVM内存管理器创建,支持创建不可变对象( immutable object)池。比如 String池。 内存池可以属于Java堆内存或Perm Gen,具体取决于JVM内存管理器实现。

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

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

Java 内存模型——栈内存

Java栈内存(Stack Memory)用于执行线程。 它们包含方法特定的值,这些值是短暂存活的,并且引用了堆内存中被方法引用的其他对象。 堆内存和栈内存

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

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

VM SWITCHVM SWITCH DESCRIPTION
-Xms在 JVM 启动时设置初始堆内存大小。
-Xmx设置堆内存空间大小的最大值。
-Xmn设置新生代内存空间大小,剩下的空间大小就是老年代的内存空间大小。
-XX:PermGen设置永久代初始内存空间大小
-XX:MaxPermGen设置永久代内存空间大小的最大值。
-XX:SurvivorRatio设置Eden内存空间和Survivor内存空间的比例,例如,如果新生代的空间大小是10M,而-XX:SurvivorRatio=2,那么Eden内存空间大小为5M,两个Survivor内存空间大小将分别为2.5M。-XX:SurvivorRatio 默认值为8。
-XX:NewRatio设置老年代和新生代空间大小的比例。 默认值为 2.

大多数情况下,上面的选项足够使用了,但是如果您想使用其他选项,可以参考JVM 选项

Java 内存管理——垃圾回收

Java垃圾收集是从内存中标识、删除未使用对象以及释放空间的过程。 Java编程语言的特色之一是自动垃圾回收,与其他编程语言(如C)不同,它们的内存需要手动分配和释放。

垃圾收集器(Garbage Collector)是在后台运行的程序(守护线程),它检查内存中的所有对象,并找出程序所有未被引用的对象。 然后删除所有未被引用的对象,并释放空间以分配给其他对象。

垃圾回收的一个简单实现可以分为三步:

  1. 标记——这是垃圾回收的第一步,将识别出哪些对象正在使用,哪些对象没有在使用。
  2. 普通删除——垃圾收集器将删除未被使用的对象,然后回收内存空间分配给其他对象使用。
  3. 压缩删除——为了获得更好的性能,在删除未被使用的对象后,可以将所有幸存的对象移动到一起。 这会提高给新对象分配内存时的性能。

标记-删除方法的缺点

  1. 它效率不高,因为大多数新创建的对象都将被闲置
  2. 在多个垃圾收集周期中使用的对象很有可能在未来的垃圾收集周期中继续使用。

这是因为堆内存分为新生代(Young Generation)和老年代(Old Generation),Java垃圾回收是分代的。 上文中已经解释过如何根据Minor GCMajor GC扫描对象并将其从一个空间移动到另一个空间。

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

在应用程序中我们可以使用5种垃圾回收类型,我只需要调整JVM开关即可为我们的应用程序选择垃圾回收策略。

  1. Serial GC (-XX:+UseSerialGC): Serial GC使用简单的标记 - 清除 - 整理(mark-sweep-compact)方法用于新生代和老年代的垃圾回收,即Minor GCMajor GCSerial GC在客户端机中非常有用,例如我们的应用程序比较独立,而且CPU比较小。 它适用于内存占用少的小型应用程序。
  2. Parallel GC (-XX:+UseParallelGC): Parallel GCSerial GC相同,不同的是Parallel GC使用N(N是系统中的CPU核心数)个线程进行新生代的垃圾回收。 我们可以使用-XX:ParallelGCThreads = nJVM 选项来控制线程数。并行垃圾收集器也称为吞吐量收集器,因为它使用多个 CPU 来提升 GC 性能。 Parallel GC使用单个线程进行老年代的垃圾回收。
  3. Parallel Old GC (-XX:+UseParallelOldGC): 和Parallel GC相同,不同的是它采用多线程进行老年代和新生代的垃圾回收。
  4. Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): CMS收集器也称为并发低暂停收集器。 在老年代进行垃圾回收时,CMS收集器尝试通过与应用程序线程同时执行大多数垃圾收集工作来最小化由于垃圾收集而导致的暂停。
    新生代的CMS收集器与并行收集器使用相同的算法。 此垃圾收集器适用于我们无法忍受暂停时间过长的响应式应用程序。 我们可以使用-XX:ParallelCMSThreads=nJVM选项调整CMS收集器中的线程数 。
  5. G1 Garbage Collector (-XX:+UseG1GC): Garbage FirstG1垃圾收集器从 Java 7 开始支持,它的目标是取代CMS收集器G1收集器是并行,并发和增量压缩的低暂停垃圾收集器。G1收集器不像其他收集器那样工作,并且没有新生代和老年代的概念。 它将堆空间划分为多个大小相等的堆区域。 当进行垃圾回收时,它首先收集具有较少实时数据的区域,因此称为“Garbage First”。 您可以在Oracle Garbage-First Collector 文档中找到有关它的更多详细信息。

Java 内存管理——Java 垃圾回收监控

我们可以使用Java命令行以及UI工具来监视应用程序的垃圾收集活动。

jstat

我们可以使用jstat命令行工具来监控JVM 内存和垃圾收集活动。 它适配标准JDK,因此您无需执行任何其他操作即可使用它。

在使用jstat命令之前,你需要知道 Java 应用的进程id。你可以使用ps -ef | grep java命令来获取进程id。假如我的进程id是9582,那么我就可以使用jstat -gc 9582 1000来查看垃圾回收的信息。命令中最后一个参数是每个输出之间的时间间隔,因此它将每1秒打印一次内存和垃圾收集数据。

1
2
3
4
5
6
7
8
9
$ 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

每一列的含义如下:

  • S0C and S1C: Survivor0 和 Survivor1当前的内存大小,以 KB 计。
  • S0U and S1U: Survivor0 和 Survivor1当前已使用的内存大小,以 KB 计。注意其中一个 survivor 一直是空的。
  • EC and EU: Eden 区当前的内存大小和 Eden 区当前已使用的内存大小,以 KB 计。注意 EU 的大小会一直增大直到接近EC的大小,此时会执行Minor GC,然后EU 的大小就降低了。
  • OC and OU: 老年代当前的大小和已使用的大小,以 KB 计。
  • PC and PU: 永久代当前的大小和已使用的大小,以 KB 计。
  • YGC and YGCT: YGC 代表在新生代 GC 发生的次数;YGCT 代表新生代执行GC的累计时间。注意,这两个值都在同一行中增加,因为 Minor GC 会使得 EU 值下降。
  • FGC and FGCT: FGC 代表 Full GC 发生的次数;FGCT Full GC 发生的累计时间。 注意,与新生代 GC 相比, Full GC 花费时间太长。
  • GCT: GC 操作的总累计时间。注意,它是 YGCT 和 FGCT 列值的总和。

jstat的优点是它也可以在没有 GUI 的远程服务器上执行。请注意,根据-xmn10mjvm选项的限制,S0C、S1C和EC的总和为10M。

可视化的 Java VisualVM

如果您想在GUI中看到内存和GC操作,那么可以使用jvisualvm工具。Java VisualVM也是JDK的一部分,不需要单独下载。

只需在终端中运行jvisualvm命令即可启动Java VisualVM应用程序。启动后,您需要从tools -> plugins 选项安装VisualGC插件,如下图所示。

VisualVM-Visual-GC-Plugin

安装完VisualGC之后,只需打开左侧列中的应用程序,然后转到VisualGC部分。您将得到一个JVM内存和垃圾收集细节的映像,如下图所示。

Serial-GC-VisualGC

垃圾回收调优

Java垃圾回收调优应该是提高应用程序吞吐量的最后选项,只有当GC时间较长导致应用程序超时时,才看到性能下降。

假如你在日志中看到java.lang.OutOfMemoryError: PermGen space错误,你可以通过-XX:PermGen和-XX:MaxPermGen来监控和提供永久代内存。你也可以使用-XX:+CMSClassUnloadingEnabled,然后观察在CMS 垃圾收集器的性能。

假如你看到很多 Full GC 操作,你可以提高老年代内存大小。

总的来说,垃圾回收调优需要花费大量的精力和时间,而且没有硬性和快速的规则。您需要尝试不同的选项并进行比较,从中找出最适合您的选项。