原文:Java Memory Management for Java Virtual Machine (JVM) by Justin Gesso. June 2, 2017 翻译:陈同学,欢迎访问陈同学博客查看更多文章
Java内存管理是一项持续的挑战,同时也是锻造出可拓展应用的必备技能。本质上,Java内存管理就是一个为新对象分配内存和释放无用对象内存的过程。
准备好深入Java内存管理之旅吧!
在本文中,我们将讨论JVM、理解内存管理、学习内存监控工具、监控内存使用和垃圾回收活动。
接下来你会发现,有许多模型、方法、工具和建议可用于内存优化。
JVM是一种可以让计算机运行Java程序的抽象计算机器。JVM有三个概念:
JVM加载代码,验证代码,执行代码,管理内存(包含从操作系统分配内存,管理Java内存分配即堆内存压缩和垃圾对象清除) 和提供运行时环境。
JVM内存被划分为多个部分:堆内存(Heap)、非堆内存(Non-Heap)和其他内存。
Fig 1.1 source https://www.yourkit.com/docs/kb/sizes.jsp
堆内存是为所有Java类实例和数组分配内存的运行时数据区。堆内存在JVM启动时被创建,随着应用程序的运行,堆内存可能会变大或减小。可以使用JVM参数 -Xms 来指定堆的size,堆size是固定的,也可以是可变的,这取决于垃圾回收策略。最大堆size可以使用 -Xmx 来配置,堆的默认大小是62M。
JVM堆以外的内存称之为非堆内存,非堆内存在JVM启动时创建,它存储了每个类的结构,例如:运行时常量池,字段和方法的数据,方法和构造函数的代码,以及被加入到字符串常量池的字符串。非堆内存默认最大size是64M,可以通过 –XX:MaxPermSize 重新设置。
JVM使用这块内存来存储JVM自身的代码和内部结构,以及已加载的Profiler agent的代码和数据等。
Fig 1.2 source http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
JVM堆在物理上被划分为2个部分(或称为2代):nursery(或称为年轻空间/年轻代) 或 old space(或称为老年代)。
译者注:nursery和old space不做翻译。几个单词直译说明:nursery:幼儿园, Minor:年轻的, Major:成年的/老年的。
nursery是堆内存中分配新对象的地方。在nursery满了之后,会运行一个特殊的 yong collection 来进行垃圾收集,当nursery中对象生存的时间足够长时,这些对象会被移动到 old space,以腾出nursery的空间来为新对象分配内存,这种垃圾收集叫做 Minor GC。nursery分成3部分:Eden(伊甸园) 和 2个 Survivor Space(幸存者区).
关于nursery空间的几个重点:
在老年代满后,会进行垃圾收集,这个过程称为 old collectio。老年代空间包含那些存活已经且在多次 Minor GC之后依然幸存的对象,通常只有在老年代满后才会触发老年代垃圾收集,老年代垃圾收集也叫 Major GC,Major GC通常会消耗很长的时间。
Nursery中大多是临时对象而且生存时间很短,yong collection 被设计成能够快速找到依然存活的对象并将他们移出nursery. 通常情况下,和 old collection 以及 单代堆(single-generational heap, 一个没有nursery的堆) 相比,一次 yong collection 会快速释放大量内存。
最近的发布版本中,nursery中有一个叫做 keep area(保留区) 的地方,keep area中包含大多数新创建的对象,而且在垃圾回收时不会处理 keep area中的数据,直到下一次年轻代垃圾收集才会被处理。这种方式可以阻止在垃圾收集启动不久前创建的对象被移动。
该区在Java8中已被Metaspace替换
永久代(Perm Gen)包含了JVM用于描述应用中的类和方法的元数据,Perm Gen由JVM在运行时根据应用程序所需要类进行构建,同时它也包含了Java SE Library中的类和方法。Perm Gen中的对象只在Full GC时进行垃圾收集。
在Java8中已经没有Perm Gen,也就是说不会再有 java.lang.OutOfMemoryError:Perm Gen
问题。不像Perm Gen是属于堆内存中的一部分,Metaspace不属于堆内存,类的元数据现在大多分配在机器的内存之外。和Perm Gen会使用固定的最大内存大小相比,Metaspace默认会自增长(大小取决于操作系统),可以使用 -XX:MetaspaceSize and -XX:MaxMetaspaceSize 来进行配置。采用Metaspace之后,类和类的元数据的生存时间和和它们的ClassLoaders保持一致,只要它们的ClassLoaders存活,这些元数据也会一直存活而且不能被释放。
当Java程序运行时,它会以分层的方式执行代码。在第一层,它使用客户端编译器(C1 编译器)编译代码,相关统计分析数据将用于第二层的服务端编译(C2编译器)以便优化代码。分层编译在Java7中默认未启用,在Java8中已启用。
即时编译器(JIT)将编译后的代码存储到一个叫 Code Cache 的区域中,这是一个保存编译后代码的特殊堆。如果这个区域的大小超出阀值,将会刷新这个区域,而且这个区域的对象不能被GC重新定位。
Java8解决了一些性能问题和编译器未重新启用的问题,Java7中为了避免这些问题的解决方案是将 Code Cache 区域的大小增加到永远不可能达到的程度。
方法区是Perm Gen的一部分内存,用来保存类的结构(运行时常量和静态变量)和方法以及构造函数的代码。
内存池由JVM管理器创建,它用来创建不可变对象池(immutable object)。内存池可以属于堆或者Perm Gen,到底属于哪部分取决于JVM内存管理器的实现。
运行时常量池是方法区的一部分,包含类的运行时常量和静态方法。
Java栈内存用于线程执行,包含方法执行所需要的一些生存时间很短的特殊数据和一些指向堆内存对象的引用。
堆内存配置
配置项 | 说明 |
---|---|
–Xms | 设置JVM启动时的初始堆内存 |
-Xmx | 设置最大堆内存 |
-Xmn | 设置年轻代的大小,剩下的就是老年代的空间 |
-XX:PermGen | 设置永久代的初始内存大小 |
-XX:MaxPermGen | 设置永久代最大内存 |
-XX:SurvivorRatio | 设置Eden空间的比率,例如:如果年轻代空间是10M,-XX:SurvivoRatio=2,那么5M将用于Eden区,剩余的5M平均分给2个Survivor区(每个2.5M)。默认值为8 |
-XX:NewRatio | 设置老年代/新生代比率,默认值为2 |
垃圾收集是一个释放堆空间以分配新对象的过程,Java中最大的功能之一就是自动垃圾收集。垃圾收集器是一个在后台运行的程序,它会查看内存中的所有对象,找出那些不被其他程序引用的对象。所有这些未被引用的对象将被清除,空间被回收以分配给其他对象。一种垃圾回收的基本方式包含三个步骤:
JVM使用 标记/清除垃圾收集模式 来进行整个堆区的垃圾收集工作,这种模式包含两个阶段:标记阶段和清除阶段。
在标记阶段,所有Java线程、本地处理程序以及其他ROOT资源可达的对象都会被标记为 存活,同时也包含这些存活对象可达的对象。这个过程识别和标记了所有正在使用的对象,剩下的所有对象将被视为垃圾。
在清除阶段,将遍历堆以找出存活对象之间的内存碎片并记录到一个list中,这些空间将用于新对象的内存分配。
Java垃圾收集类型
在应用程序中有5种垃圾收集方式可以使用,可以通过配置来调整JVM的垃圾收集策略。
译者注:暂停/停顿指执行垃圾回收时会stop-the-world,即暂停其他线程的工作,只做垃圾收集
内存不足常常是导致Java应用不稳定和无响应的原因,因此,为了确保稳定性和性能,我们需要监控垃圾收集对响应时间和内存使用的影响。然后,监控内存使用和垃圾收集还不够,因为这两个因素并不能告诉我们响应时间是否受到它们的影响。只有垃圾GC造成的程序 暂停 会直接影响响应时间,而且GC可以和应用程序同时运行。因此,需要将垃圾回收造成的暂停和应用响应时间关联起来,基于此,我们需要监控以下内容:
jstat 是Java HotSpot VM的内置工具,用于获取运行中应用的性能和资源消耗信息。该工具可用于诊断性能问题,特别是与堆大小和垃圾收集相关的问题。jstat 不需要设置任何JVM启动参数,Java HotSpot VM中的内置指令已默认启用。任何可下载JDK版本中都包含了这个工具,jstat 使用虚拟机标识符(VMID)来识别目标进程。
使用 带有 gc选项的jstat 命令来查看JVM堆内存使用情况
<JAVA_HOME>/bin/jstat –gc <JAVA_PID>
译者注:下述表格非常直白,不做任何翻译
项 | 说明 |
---|---|
S0C | Current survivor space 0 capacity (KB) |
S1C | Current survivor space 1 capacity (KB) |
S0U | Survivor space 0 utilization (KB) |
S1U | Survivor space 1 utilization (KB) |
EC | Current eden space capacity (KB) |
EU | Eden space utilization (KB) |
OC | Current old space capacity (KB) |
OU | Old space utilization (KB) |
MC | Metasapce capacity (KB) |
MU | Metaspace utilization (KB) |
CCSC | Compressed class space capacity (KB) |
CCSU | Compressed class space used (KB) |
YGC | Number of young generation garbage collection events |
YGCT | Young generation garbage collection time |
FGC | Number of full GC events |
FGCT | Full garbage collection time |
GCT | Total garbage collection time |
jmap 工具用于打印运行中的VM和核心文件的内存相关的统计数据。JDK8 引入了 Java Mission Control、Java Flight Recorder 和 jcmd 工具 用于诊断JVM和Java应用程序的问题。推荐使用最新的 jcmd 工具代替 jmap 工具以增强诊断能力、减少性能开销。
可以使用 -heap 选型来获取下列Java 堆信息:
<JAVA_HOME>/bin/jmap –heap <JAVA_PID>
jcmd 命令用于向JVM发送诊断请求,这些请求对于控制Java Flight Recording、故障排除、JVM和应用诊断非常有用。该命令必须在JVM运行的机器上使用,而且具有和启动JVM的用户/用户组一样的权限。
使用以下命令创建 heap dump.
jcmd <JAVA_PID> GC.heap_dump filename=<FILE>
上述命令的效果和以下命令一样:
jmap –dump:file=<FILE> <JAVA_PID>
译者注:原文还有jhat/hprof/javac等命令以及VirtualVM的使用,可自行查看。本文量太大,译者要累崩了。对于VirtualVM的使用,可以查看译者以前的一篇文章 VisualVM远程监控Tomcat中应用
当我们看到因长时间GC导致应用超时而引起的性能下降时,Java垃圾收集优化可能是我们提高应用吞吐量的最后手段。
如果出现 java.lang.OutOfMemoryError:PermGen Space
错误,可以使用 –XX:PermGen 和 –XX:MaxPermGen 参数增加 Perm Gen内存的大小,同时也需要监控内存。不过在Java8中将我们将看不到这个错误。 如果我们发现频繁的 Full GC 活动,可以尝试增加老年代的内存大小。总的来说,垃圾收集优化需要付出很大的精力,而且没有捷径可走,我们只能不断尝试不同的JVM配置并进行比较,最终找到适合我们应用的最佳配置。
这儿有一些性能方案:
了解下这些有用的点:
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。