Java JVM内存模型 - Java内存管理

如果想了解JVM内存模型,Java内存管理,了解Java垃圾收集的工作非常重要,内存模型依赖于垃圾收集器。今天将继教探讨java中的内存管理,JVM内存的不同部分以及如何监视和执行垃圾收集调优。

Java JVM 内存模型

这里所说的内存模型是以垃圾收集不视角下的内存模型,而非JMM规范所说的内存模型,JMM规范所描述的Java内存模型,是关于多线程下内存一致性问题,以及对线程交互、编译优化和指令重新排序引发的一系统内存一致性的问题的处理。

Java-Memory-Model

如上图所示,JVM内存分为多个不同的部分,从广义上讲,JVM Heap内存实际上分为两部分 - Young Generation和Old Generation。

Java内存管理 - 年轻代

年轻代是所有新对象出生的地方,当年轻代被填满时,进行垃圾收集。此垃圾收集称为Minor GC。Young Generation分为三个部分--Eden Memory和两个Survivor Memory空间(幸存者空间)。

关于年轻代空间的重点

大多数新创建的对象都位于Eden内存空间中。当Eden空间被对象填满时,执行Minor GC并将所有幸存者对象移动到其中一个幸存者空间(Survivor Memory)。

Minor GC还会检查幸存者对象(针对Survivor Memory空间)并将其移动到另一个幸存者空间。所以,同一时间,两个幸存者空间总有一个是空的。

在循环多次GC后幸存的对象被移动到年老代的内存空间,一般通过设置年轻代对象的年龄阈值,达到这个阈值的对象会被转移到年老代。

Java内存管理 - 年老代

年老代内存中的对象是那些存活时间较长的和从年轻代晋升过来的对象,通常年老代垃圾收集器会在年老代内存快被填满时执行,对年老代进行垃圾收集被称为Major GC,通常需要花费较长的时间。

冻结应用程序(Stop the World Event)

所有垃圾收集都是“Stop the World Event”,因为所有应用程序线程都会被停止,直到GC操作完成。由于年轻代的对象存活时间较短,所以Minor GC会非常快,应用程序一般不受影响。

但Minor GC需要的时间相对较长,因为它需要检查所有存活对象。Minor GC尽可能短暂,因为会在垃圾收集的这段时间里应用程序是没有响应的。因此,如果对响应时间有要求的应用程序的,频繁发生Minor GC,需要注意是否有超时错误。

垃圾收集器持续的时间取决于所选用的垃圾收集策略,这就是为什么要监控并调整垃圾收集器,以避免在响应有要求的应用程序中出现超时现象。

JVM内存模型 - 永久代( Permanent Generation)

Permanent Generation或“Perm Gen”包含应用程序中使用的类和方法所需的元数据。注意,Perm Gen不是Java堆内存的一部分。Perm Gen大小由JVM根据应用程序的使用情况而定,Perm Gen还包含Java SE库中的类和方法,Perm Gen对象会在Full GC时被进行垃圾收集,Full GC会收集年轻代(Young Gen),年老代(Old Gen)和Perm Gen。 

JVM内存模型 - 方法区域(Method Area)

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

JVM内存模型 - 内存池(Memory Pool)

内存池由JVM内存管理器创建,用于创建不可变对象池(如果实现支持它)。String Pool是这种内存池的一个很好的例子。内存池可以属于Heap或Perm Gen,具体取决于JVM内存管理器的实现。 

JVM内存模型 - 运行时常量池(Runtime Constant Pool)

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

JVM内存模型 - Java栈内存(Java Stack Memory)

Java栈内存用于执行线程。它们包含特定于方法的特定值,以及当前方法所使用堆中的对象的引用。

更多详情参见Java内存分配。 

Java内存管理 - Java堆内存调整

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


VM SWITCH    VM SWITCH描述

-Xms    用于在JVM启动时设置堆的初始大小
-Xmx    用于设置堆最大值。
-Xmn    设置年轻代的大小,剩下的空间用于年老代。
-XX:PermGen    设置永久代初始大小(java8已弃用)
-XX:MaxPermGen    设置Perm Gen的最大空间(java8已弃用)
-XX:SurvivorRatio    Eden空间和Survivor Space的比率,例如,如果Young Generation大小为10m,VM参数为-XX:SurvivorRatio=2,则Eden Space为5m,两个Survivor空间各自为2.5m。默认值为8。
-XX:NewRatio    年老代/年轻代空间的比例,默认值为2。

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

JVM的内存管理 - Java垃圾收集

Java垃圾收集是指从内存中识别并删除不在被使用的对象,为将来可能出生的对象留出可用空间。Java编程语言的最大特点之一是自动垃圾收集,与其他编程语言(如C)手动分配和释放内存不同。垃圾收集器是在后台运行的程序,它查看内存中的所有对象,并找出程序任何部分未引用的对象。删除所有这些未引用的对象,并回收空间以分配给其他对象。

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

1、标记(Marking):这是垃圾收集器的第一步,识别哪些对象正在使用以及哪些对象未被使用。

2、正常删除(Normal Deletion):垃圾收集器删除未使用的对象并回收可用空间。

3、使用压缩删除(Deletion with Compacting):为了获得更好的性能,在删除不在被使用的对象后,将所有幸存的对象移到一块。这将提升为新对象分配内存时的性能。 

简单标记和删除的方法有两个问题

首先它的效率不高,因为大多数新创建的对象将被废弃,其次,经历了多个垃圾收集周期的对象很有可能在未来被使用。

使用上述简单方法的缺点是Java垃圾收集是分代的,并且在堆内存中有Young Generation和Old Generation空间。已经在上面解释过如何根据Minor GC和Major GC扫描对象并将其从一代空间移动到另一代空间。 

JVM内存管理 - Java垃圾收集类型

可以在应用程序中使用五种类型的垃圾收集方式。只需要使用JVM开关来为应用程序启用垃圾收集策略。 

串行GC(-XX:+ UseSerialGC)

串行GC使用简单的mark-sweep-compact,串行GC在客户端机器中很有用,例如简单独立应用程序和CPU较小的机器。它适用于内存占用少的小型应用程序。

并行GC(-XX:+ UseParallelGC)

并行GC与串行GC相同,不同之处在于为年轻代收集垃圾时生成N个线程,其中N是系统中的CPU核心数。可以使用-XX:ParallelGCThreads=nJVM选项控制线程数。并行垃圾收集器也称为吞吐量收集器,因为它使用多个CPU来加速GC性能。不过种GC对年老代垃圾进行回收时使用单线程。

并行Old GC(-XX:+ UseParallelOldGC)

与Parallel GC相同,只是它为Young Generation和Old Generation进行垃圾收集时使用多个线程。

并发标记扫描(CMS)收集器(-XX:+ UseConcMarkSweepGC)

CMS收集器也称为并发低暂停收集器,它为年老代进行垃圾收集,CMS收集器尝试在应用程序运行时执行大部分垃圾收集工作,来最小化因垃圾收集而导致的暂停。

年轻代的CMS收集器使用与并行收集器相同的算法。此垃圾收集器适用于无法接受较长暂停时间,导致响应延迟的应用程序。可以使用-XX:ParallelCMSThreads=nJVM选项限制CMS收集器中的线程数。

G1垃圾收集器(-XX:+ UseG1GC)

G1垃圾收集器不像其他收集器那样工作,并且没有年轻代和年老代的概念,它将堆空间划分为若干个大小相等的堆区域。当进行垃圾收集时,首先收集具有较少实时数据(live data)的区域,因此叫“垃圾优先”(Garbage First)。可以在Garbage-First Collector Oracle Documentation中找到有关它的更多详细信息。

JVM内存管理 - Java垃圾收集监控

我们可以使用Java命令行以及UI工具来监视应用程序的垃圾收集活动。对于示例,使用Java SE下载提供的演示应用程序之一。

如果要使用相同的应用程序,请转到Java SE下载页面并下载JDK 7和JavaFX演示和示例。使用的示例应用程序是Java2Demo.jar,它存在于jdk1.7.0_55/demo/jfc/Java2D目录中。但是,这是一个可选步骤,可以为任何Java应用程序运行GC监控命令。

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

admin@admin: ~/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内存和垃圾收集活动。

执行jstat命令需要知道应用程序的进程ID,可以使用ps -eaf | grep java命令或者使用jps轻松获取java进程ID列表。

admin@admin:~$ ps -eaf | grep Java2Demo.jar

运行jstat命令,如下所示:

 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

jstat的最后一个参数是每次输出之间的时间间隔,可以每1秒打印一次内存和垃圾收集数据。

S0C和S1C

此列显示Survivor0和Survivor1区域的当前大小(KB为单元)。

S0U和S1U

此列显示Survivor0和Survivor1区域的当前使用情况,以KB为单位。请注意,其中一个幸存区域始终为空。

EC和EU

这些列以KB为单位显示Eden空间的当前大小和使用情况。请注意,EU大小正在增加,一旦超过EC,就会调用minor GC将EU值降低。

OC和OU

这些列以KB为单位显示年老代的当前大小和当前使用情况。

PC和PU

这些列显示Perm Gen的当前大小和当前使用情况,以KB为单位。

YGC和YGCT

YGC列显示年轻代发生的GC的次数。YGCT列显示年轻代的GC操作的累计时间。请注意,这两个值都是在同一个行中增加的,EU值会因为minor GC而下降。

FGC和FGCT

FGC列显示发生的Full GC的数量。FGCT列显示完整GC操作的累计时间。请注意,与年轻代GC时序相比,Full GC时间更长。

GCT

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

jstat的优点是可以在没有GUI的服务器中执行。请注意,通过-Xmn10m JVM选项指定的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垃圾收集调整

Java JVM 性能调优是用来提高应用程序吞吐量的最后一个选项,并且只有当看到由于较长的GC时间,导致应用程序超时而造成的性能下降时。

如果java.lang.OutOfMemoryError: PermGen space在日志中看到错误,尝试使用-XX:PermGen和-XX:MaxPermGen JVM选项监视和增加Perm Gen内存空间。

也可以尝试使用-XX:+CMSClassUnloadingEnabled 并检查它与CMS垃圾收集器的性能。如果看到很多Full GC操作,那么应该尝试增加年老代内存空间。

整体垃圾收集调整需要花费很多精力和时间,并且没有严格的规则。需要尝试不同的选项并进行比较,以找出适合应用的最佳选项。

Java中的内存管理和垃圾收集简单介绍到这,希望有助于理解JVM内存和垃圾收集过程。