JVM内存管理
原文:Java Memory Management for Java Virtual Machine (JVM) by Justin Gesso. June 2, 2017
翻译:陈同学 欢迎访问译者博客原文,阅读体验更佳
Java内存管理是一项持续的挑战,同时也是锻造出可拓展应用的必备技能。本质上,Java内存管理就是一个为新对象分配内存和释放无用对象内存的过程。
准备好深入Java内存管理之旅吧!
在本文中,我们将讨论JVM、理解内存管理、学习内存监控工具、监控内存使用和垃圾回收活动。
接下来你会发现,有许多模型、方法、工具和建议可用于内存优化。
JVM
JVM是一种可以让计算机运行Java程序的抽象计算机器。JVM有三个概念:
- 规范(specification):为JVM的运作提供规范,由Sun公司或其他公司提供实现
- 实现(implementation):也叫JRE,即Java运行时环境
- 实例(instance):在编写Java代码之后,运行一个Java类,将会创建一个JVM实例
JVM加载代码,验证代码,执行代码,管理内存(包含从操作系统分配内存,管理Java内存分配即堆内存压缩和垃圾对象清除) 和提供运行时环境。
JVM内存结构
JVM内存被划分为多个部分:堆内存(Heap)、非堆内存(Non-Heap)和其他内存。
Fig 1.1 source https://www.yourkit.com/docs/kb/sizes.jsp
- 堆内存(Heap Memory)
堆内存是为所有Java类实例和数组分配内存的运行时数据区。堆内存在JVM启动时被创建,随着应用程序的运行,堆内存可能会变大或减小。可以使用JVM参数 -Xms 来指定堆的size,堆size是固定的,也可以是可变的,这取决于垃圾回收策略。最大堆size可以使用 -Xmx 来配置,堆的默认大小是62M。
- 非堆内存(Non-Heap Memory)
JVM堆以外的内存称之为非堆内存,非堆内存在JVM启动时创建,它存储了每个类的结构,例如:运行时常量池,字段和方法的数据,方法和构造函数的代码,以及被加入到字符串常量池的字符串。非堆内存默认最大size是64M,可以通过 –XX:MaxPermSize 重新设置。
- 其他内存(Other Memory)
JVM使用这块内存来存储JVM自身的代码和内部结构,以及已加载的Profiler agent的代码和数据等。
JVM堆内存结构
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空间的几个重点:
- 大多数新创建的对象都位于 Eden区
- Eden区 满了之后会触发 Minor GC,Eden中所有存活的对象会被移动到 Survivor区 中当前未使用的一个区.
- Minor GC 也会检查另一个正在使用 Survivor区 中存活的对象并把它们移动到未使用的 Survivor区. 所以任何时刻,2个 Survivor区其中有一个一定是空的
- 在多次GC之后依然存活的对象将被移动到 老年代空间,这通常是在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中的数据,直到下一次年轻代垃圾收集才会被处理。这种方式可以阻止在垃圾收集启动不久前创建的对象被移动。
Java内存模型
永久代(Permanent Generation)
该区在Java8中已被Metaspace替换
永久代(Perm Gen)包含了JVM用于描述应用中的类和方法的元数据,Perm Gen由JVM在运行时根据应用程序所需要类进行构建,同时它也包含了Java SE Library中的类和方法。Perm Gen中的对象只在Full GC时进行垃圾收集。
元空间(Metaspace)
在Java8中已经没有Perm Gen,也就是说不会再有 java.lang.OutOfMemoryError:Perm Gen
问题。不像Perm Gen是属于堆内存中的一部分,Metaspace不属于堆内存,类的元数据现在大多分配在机器的内存之外。和Perm Gen会使用固定的最大内存大小相比,Metaspace默认会自增长(大小取决于操作系统),可以使用 -XX:MetaspaceSize and -XX:MaxMetaspaceSize 来进行配置。采用Metaspace之后,类和类的元数据的生存时间和和它们的ClassLoaders保持一致,只要它们的ClassLoaders存活,这些元数据也会一直存活而且不能被释放。
代码缓存(Code Cache)
当Java程序运行时,它会以分层的方式执行代码。在第一层,它使用客户端编译器(C1 编译器)编译代码,相关统计分析数据将用于第二层的服务端编译(C2编译器)以便优化代码。分层编译在Java7中默认未启用,在Java8中已启用。
即时编译器(JIT)将编译后的代码存储到一个叫 Code Cache 的区域中,这是一个保存编译后代码的特殊堆。如果这个区域的大小超出阀值,将会刷新这个区域,而且这个区域的对象不能被GC重新定位。
Java8解决了一些性能问题和编译器未重新启用的问题,Java7中为了避免这些问题的解决方案是将 Code Cache 区域的大小增加到永远不可能达到的程度。
方法区(Method Area)
方法区是Perm Gen的一部分内存,用来保存类的结构(运行时常量和静态变量)和方法以及构造函数的代码。
内存池(Memory Pool)
内存池由JVM管理器创建,它用来创建不可变对象池(immutable object)。内存池可以属于堆或者Perm Gen,到底属于哪部分取决于JVM内存管理器的实现。
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,包含类的运行时常量和静态方法。
栈内存(Java Stack Memory)
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中最大的功能之一就是自动垃圾收集。垃圾收集器是一个在后台运行的程序,它会查看内存中的所有对象,找出那些不被其他程序引用的对象。所有这些未被引用的对象将被清除,空间被回收以分配给其他对象。一种垃圾回收的基本方式包含三个步骤:
- Marking(标记):这是垃圾收集器识别哪些对象正被使用哪些对象不再被使用的第一步
- Normal Deletion(正常清除):垃圾收集器清除不再被使用的对象,回收内存以分配给其他对象
- Deletion with compacting(压缩清除):为了更好的性能,在清除不再使用的对象后,所有存活的对象将被移动到一起,这会提高新对象内存分配的性能。
标记/清除垃圾收集模式
JVM使用 标记/清除垃圾收集模式 来进行整个堆区的垃圾收集工作,这种模式包含两个阶段:标记阶段和清除阶段。
在标记阶段,所有Java线程、本地处理程序以及其他ROOT资源可达的对象都会被标记为 存活,同时也包含这些存活对象可达的对象。这个过程识别和标记了所有正在使用的对象,剩下的所有对象将被视为垃圾。
在清除阶段,将遍历堆以找出存活对象之间的内存碎片并记录到一个list中,这些空间将用于新对象的内存分配。
Java垃圾收集类型
在应用程序中有5种垃圾收集方式可以使用,可以通过配置来调整JVM的垃圾收集策略。
- Serial GC (-XX:+UseSerialGC):Serial GC 使用简单的 标记-清除-压缩 方式来进行年轻代和老年代的垃圾收集,Minor GC和Major GC就是这种方式
- Parallel GC (-XX:+UseParallelGC):Parallel GC 和 Serial GC 相同,只不过它使用N个线程来进行 年轻代垃圾收集,N是系统CPU的内核数。可以使用 –XX:ParallelGCThreads=n 来控制线程数量
- Parallel Old GC (-XX:+UseParallelOldGC):和Parallel GC一样,不过它 年轻代 和 老年代 都使用多个线程来进行垃圾收集
- Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC):CMS也称为 并发的低停顿(concurrent low-pause)收集器,它进行老年代垃圾回收。CMS收集器尝试使用线程并行收集大多数垃圾,以最大限度的减少因垃圾回收造成的应用暂停。CMS年轻代垃圾回收器使用了和并行收集器一样的算法,这种收集器适合于那些不能承担长时间停顿的响应式应用。可以使用 -XX:ParallelCMSThreads=n 来设置CMS收集器的线程数量。
译者注:暂停/停顿指执行垃圾回收时会stop-the-world,即暂停其他线程的工作,只做垃圾收集
- G1 Garbage Collector (-XX:+UseG1GC):Java7中已经有G1(The Garbage First)收集器,它的最终目标是替换CMS收集器。G1是一个 parallel, concurrent and incrementally compact low-pause 的垃圾收集器,G1和其他收集器不一样,它没有年轻代和老年代的概念,而是将堆划分成多个大小相等的区域,当G1执行时,它首先收集那些存活数据(live data)较少的区域,因此叫 垃圾优先。
内存使用和GC活动监控
内存不足常常是导致Java应用不稳定和无响应的原因,因此,为了确保稳定性和性能,我们需要监控垃圾收集对响应时间和内存使用的影响。然后,监控内存使用和垃圾收集还不够,因为这两个因素并不能告诉我们响应时间是否受到它们的影响。只有垃圾GC造成的程序 暂停 会直接影响响应时间,而且GC可以和应用程序同时运行。因此,需要将垃圾回收造成的暂停和应用响应时间关联起来,基于此,我们需要监控以下内容:
- 各个内存池(Eden、Survivor和老年代)的使用情况,因为内存不足是造成GC活动的首要原因
- 如果进行垃圾回收,整体内存的使用率也在不断攀升,说明发生了内存泄漏,这不可避免的会导致内存溢出。在这种情况下,进行一次堆内存分析十分必要
- 年轻代垃圾收集的次数反映了对象分配率信息,次数越多,分配的对象就越多。年轻代垃圾频繁回收可能是影响响应时间的原因,同时也是导致老年代不断增长的原因。
- 如果在GC之后老年代的利用率波动很大,但是其内存大小却没有上升,说明很多不必要的对象从年轻代拷贝到了老年代,这可能有三个原因:年轻代太小、流失率高或太多事务使用了内存。
- 高频GC活动对CPU的使用会造成负面影响,然而,只有 暂停(stop-the-world-event) 对响应时间有直接影响。与主流观点相反,暂停不一定只作用于Major GC,因此,监控暂停和应用的响应时间非常重要。
jstat
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
jmap 工具用于打印运行中的VM和核心文件的内存相关的统计数据。JDK8 引入了 Java Mission Control、Java Flight Recorder 和 jcmd 工具 用于诊断JVM和Java应用程序的问题。推荐使用最新的 jcmd 工具代替 jmap 工具以增强诊断能力、减少性能开销。
可以使用 -heap 选型来获取下列Java 堆信息:
- GC算法的特殊信息,包含了GC算法的名字(如:Parallel GC)和特定算法的详细数据(如: Parallel GC的线程数)
- 查看 通过命令行配置的JVM参数或JVM根据机器配置自动选择的配置信息
- 堆内存使用概要:对于堆内存的每一个区,这个工具可以打印出堆总容量、正在使用的内存、可用的空闲内存。如果一个内存区正在作为垃圾收集区(如新生代),命令的输出结果中将会包含一个特定内存大小的概要信息。
<JAVA_HOME>/bin/jmap –heap <JAVA_PID>
jcmd
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中应用
Java垃圾收集优化
当我们看到因长时间GC导致应用超时而引起的性能下降时,Java垃圾收集优化可能是我们提高应用吞吐量的最后手段。
如果出现 java.lang.OutOfMemoryError:PermGen Space
错误,可以使用 –XX:PermGen 和 –XX:MaxPermGen 参数增加 Perm Gen内存的大小,同时也需要监控内存。不过在Java8中将我们将看不到这个错误。 如果我们发现频繁的 Full GC 活动,可以尝试增加老年代的内存大小。总的来说,垃圾收集优化需要付出很大的精力,而且没有捷径可走,我们只能不断尝试不同的JVM配置并进行比较,最终找到适合我们应用的最佳配置。
这儿有一些性能方案:
- 应用采样和分析
- 服务器和JVM调优
- 使用合适的硬件和操作系统
- 根据应用行为和采样结果改进代码(说起来容易说起来难!)
- 正确的使用JVM(使用最佳的JVM参数配置)
- 在多核机器中使用 -XX:+UseParallelGC
了解下这些有用的点:
- 不要限制JVM的内存,除非我们遇到 暂停 问题
- 将 -Xms 和 -Xmx 配置成一样的值
- 当处理器的数量增加时,一定要加大内存,因为可以并行分配内存
- 不要忘记优化Perm Gen
- 尽可能少的使用同步
- 如果有用的话,尽可能使用多线程,但是也要注意线程的性能开销,同时要确保在不同环境下多线程能够正常运行
- 避免过早的创建对象,最好在真正要使用它时才创建。这是我们常常忽略的一个基本概念
- JSP往往比Servlet更慢
- 使用StringBuilder代替字符串拼接
- 使用基础数据类型而不是包装的对象(例如使用long而不是Long)
- 尽可能的重用对象,同时避免创建不必要的对象
- 在测试empty字符串时,equals是非常昂贵的,使用length属性代替
- “==”比equals更快
- n += 5比 n = n +5 更快,第一种情况产生的字节更少
- 周期性的flush & clear Hibernate Session
- 批量更新和删除