弄清JVM(Java Virtual Machine)的内存管理模型对了解java GC工作原理是很有必要的。最近正好看到一篇文档写的不错,介绍了Java内存管理的处理方式,包括JVM内存分配各个区域的含义,以及如何监测协调GC工作。翻译后在此记录。
原文传送门
内存分配区域
如上图所示,JVM内存被分成了几个区域,粗略的看JVM的堆内存分成了两块区域----新生域,年老域。
新生域内存管理
刚创建的新对象会被分配到新生域。当新生域使用殆尽,GC就开始工作了。这种状况下促发的GC被称作次级GC(Minor GC)。而新生域又被分成三个部分
- Eden Memory.就像是上帝(JVM)划分出来的伊甸园区域,可以想象刚生成的对象在这里快快乐乐的生活着。
- 两个Survivor Memory(幸存域),也就是上图中的S0和S1区域。
新生域的主要特性:
- 大多数新创建的对象都被分配到了Eden Memory
- 当Eden Memory被新分配的对象填充满时,次级GC开始工作,幸存下来的对象被移到其中一个Survivor Memory(幸存域)。
- 次级GC会同时检查Survivor Memory(幸存域),将其中一个Survivor Memory(幸存域)中的对象移到另一个Survivor Memory(幸存域),保证一个Survivor Memory(幸存域)为空。
- 当一个对象经过次级GC多次扫描后,依然幸存,那么它就会被移到年老域(Old generation).JVM会设置一个阀值,当一个对象被次级GC扫描过的次数达到这个阀值,而这个对象依然幸存,则该对象就被移到年老域(Old generation)。
年老域内存管理
年老域的内存存放的是一些长生命周期和被次级GC多次扫描依然幸存下来的对象。GC在年老域内存吃紧的情况下开始工作,这种GC称为主级GC(Major GC),通常主级GC消耗的时间更长。
世界暂停事件(Stop the World Event)
次级GC和主级GC开始工作时,应用线程就会停下来,因此整个Java程序也就会处于停止状态,这种情况就是"Stop the World Event"。
新生域存放的是短生命周期的对象,因而次级GC的这个过程会很快结束,在程序看来几乎不受影响。
主级GC需要扫描所有的幸存对象,因而它花费的时间会较长。一旦主级GC工作,应用程序就会暂停下来,直观感受就是程序不够流畅,无法快速响应业务事件处理,主级GC运行次数过多,甚至会引发程序的超时错误。要想应用程序跑的流畅,就得少去促发主级GC工作,要少促发主级GC工作,就得尽可能的保证年老域的内存空间不被填充满。所以这提醒我们一定要珍惜内存空间,尤其在Android移动设备上。
不同的GC策略会影响GC工作消耗的时长。为了让应用程序运行流畅度最优,就有必要根据应用程序的运行场景运用不同的GC策略。
持久域(Permanent Generation)
JVM通过元数据(metadata)来记录应用程序的类和方法,这些元数据就被放在持久域就,另外Java公共的库文件和方法也被放在这里,持久域不属于堆内存。在内存满时该块内存中的对象也可能被回收。Java8中已经删除该区域。
方法域(Method Area)
方法域属于持久域中的一块,它被用来存放定义方法的代码和类文件结构。
内存池(Memory Pool)
内存池由JVM创建出来存放不可变对象,比如String,它可能在堆内存分配也可能在持久域分配,这取决于JVM的内存管理机制。
运行时常量池( Runtime Constant Pool)
运行时常量池用于存放编译期生成的各种字面量和符号引用以及静态方法,这部分内容将在类加载后存放,它属于方法域的一部分。
栈内存(Stack Memory)
栈内存被执行线程所用。它用来存放方法内的变量,这些变量通常是一些指向堆内存对象的引用,它们生命周期短。
堆内存调节器(Heap Memory Switches)
VM SWITCH | VM SWITCH DESCRIPTION |
---|---|
-Xms | 初始堆内存大小 |
-Xmx | 最大堆内存 |
-Xmn | 新生域大小,余下空间就是年老域 |
-XX:PermGen | 设置持久域(Permanent Generation)大小 |
-XX:MaxPermGen | 最大持久域 |
-XX:SurvivorRatio | 伊甸园(Eden space)和幸存者(Survivor Space)比值, 比如新生域共分配了10M,-XX:SurvivorRatio=2,那么Eden Space就占5M,余下的5M被两个Survivor spaces均分。默认值为8。 |
-XX:NewRatio | 年老域和新生域比值,默认为2,也就是说年老域大小是新生域大小的2倍 |
Java提供了许多设置内存大小以及各个内存域占比大小的调节器。常用的调节器如下:
VM SWITCH | VM SWITCH DESCRIPTION |
---|---|
-Xms | 初始堆内存大小 |
-Xmx | 最大堆内存 |
-Xmn | 新生域大小,余下空间就是年老域 |
-XX:PermGen | 设置持久域(Permanent Generation)大小 |
-XX:MaxPermGen | 最大持久域 |
-XX:SurvivorRatio | 伊甸园(Eden space)和幸存者(Survivor Space)比值, 比如新生域共分配了10M,-XX:SurvivorRatio=2,那么Eden Space就占5M,余下的5M被两个Survivor spaces均分。默认值为8。 |
-XX:NewRatio | 年老域和新生域比值,默认为2,也就是说年老域大小是新生域大小的2倍 |
更详细的调节器配置信息请查看JVM Options Official Page。
垃圾回收器
GC(Garbage Collection)是一个进程,它专注于标示和清理没有引用的对象,释放内存空间给新分配的对象腾地方住。其他某些语言这个过程都是程序猿自己实现的,而Java自动完成了这个过程。
GC作为一个后台进程,一直默默监察着应用程序的运行过程,寻找--->标记-->释放那些没有引用到的对象,为新对象腾空间。
典型的GC处理涉及到的过程如下:
- 标记:GC标记哪些对象在使用,哪些对象没有地方使用
- 正常删除:GC删除无用的对象所占有的空间,这些空间可以被其他存活的对象所使用
- 删除后汇集:为了提升性能,删除无用对象后,所有幸存对象被汇集在一起,如此新对象分配内存时效率会更高。
上述过程可能存在如下问题:
- 大多数新创建的对象都是很快就变为无用的,而GC的运行频率又不宜过高,针对这种caseGC显得不够高效。
- 长生命周期的对象也会被GC多次扫描标记。
正是为了规避上述问题,Java将堆内存划分成了不同的区域,也就是之前提到的新生域和年老域。
GC类型
垃圾回收策略共有5种类型,根据应用程序业务场景的不同,可以设置差异化的内存调节器。
- Serial GC (-XX:+UseSerialGC): Serial GC 采用简单的标记-清楚-整理策略。比如副线GC和主线GC的模式,Serial GC 针对小型CPU上运行的简单应用很实用,特别是那些低内存设备上运行的小型应用。
- Parallel GC (-XX:+UseParallelGC): Parallel GC 和Serial GC 很类似,但它可以有N个线程去处理新生域上的GC回收工作,这里的N值对应CPU的核数。可以用
-XX:ParallelGCThreads=n 选项去设定。
Parallel GC 也是采用单线程模式在年老域上进行GC工作的。 - Parallel Old GC (-XX:+UseParallelOldGC): 和Parallel GC类似,但该种配置可以采用多线程GC来处理年老域上的GC工作。
- Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): 因为CMS不会整理、压缩堆空间,带来的好处就是GC工作时暂停的时间很短暂,.它作用在年老域上,和工作线程并发执行。针对新生域上的内存处理,它采用的是Parallel GC的处理方式。该GC策略适用需要实时快速响应的应用程序上。可以通过
-XX:ParallelCMSThreads=n
来设置CMS的线程数。 - G1 Garbage Collector (-XX:+UseG1GC): G1 Garbage Collector是Java 7新加入的,其目的是用了代替CMS回收策略. 它同样是并发执行,但会逐步压缩堆空间。
Garbage First Collector不用于其他其中GC策略,它没有新生域年老域的概念。对它而言,堆内存会被分成多个相同大小的区域,GC运行,首先回收无用对象最多的小区域。Garbage-First Collector Oracle Documentation有更详细的说明。
GC监测
可以用命令行或者可视化工具监测应用程序背后的GC运行情况。这里我用自己写的一段简单测试代码,用命令行来进行GC监控.
先看测试代码,在一个10次的循环结构中,每次去申请一个10M大小的空间,这里忽略掉string申请的空间。
package com.azhengye.test;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
String string = "i="+i;
byte[] bt = new byte[1024*1024*10];
System.out.print(string);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们在命令行里编译运行.
可以看到程序运行正常。
接下来参考之前介绍的内存调节器,我们在运行时加上一些参数。
问题出来了。
前两次我们配置Xms为2m,运行时爆出错误提示了初始堆内存过小。在最后一次将Xms设置为10m该错误消失,但却爆出了我们常见的OOM问题。
这个简单的例子就说明Java提供的这种内存调节器作用,在某些应用程序运行时,我们要根据不同的场景,配置不同的运行参数。
jstat命令行监测内存
JDK提供了jstat命令用了监测JVM内存使用情况以及GC运行状态。
其使用规则如下:
jstat -gc
进程id可以通过ps命令查看到
192:~/Documents/eclipse_workspace/DemoTestJava $ ps -eaf | grep java
501 2538 1956 0 11:41下午 ttys000 0:02.50 /usr/bin/java -Xmx20m -Xms10m -Xmn3m -XX:PermSize=2m -XX:MaxPermSize=4m -XX:+UseSerialGC -cp bin/ com.azhengye.test.Test
501 2540 1613 0 11:41下午 ttys001 0:00.00 grep java
有了进程号,在用jstat命令查看详细的内存使用情况,每隔1s打印一次内存情况。
jstat -gc 2538 1s
对照上图介绍下每一栏的含义。
- S0C and S1C: 两个幸存域的大小,之前介绍过它们总是相等的。这里也可以印证。它们的大小均为256KB.
- S0U and S1U: 两个幸存域已经使用的大小。
- EC and EU: 伊甸园区域的内存大小和已经占用的内存大小,副线GC运行后EU大小就会减少。
- OC and OU: 年老域内存大小和已经被使用的内存大小。
- PC and PU: Perm Gen内存大小和已经被使用的内存大小。
- YGC and YGCT: YGC 表示副线GC运行的次数,YGCT表示副线GC所消耗的时间。
- FGC and FGCT: FGC 表示主线GC运行次数. 相应的FGCT打印的是主线GC消耗的时间.
- GCT: 副线GC和主线GC消耗时间总和。
jvisualvm可视化监测
JDK同样提供了jvisualvm可视化的监测工具,首次打开需要先安装Visual GC 插件。下图是我截取的界面,功能比较多,这里不做过多介绍了。
内存和GC调整
这一步轻易不要走,程序运行的优化更多的注意了应该放在软件实现上。除非很明显的定位到程序运行受到了GC的拖累,或者确实需要调整内存的分配情况才能让程序运行起来最佳。
下面是几点调整建议:
- 主线GC运行太过频繁,可以尝试增大年老域内存。
- OOM问题,可以适当增加堆内存大小。
- 采用不同的GC策略,监测程序运行情况,选取最合适的。
参考链接
了解CMS(Concurrent Mark-Sweep)垃圾回收器
Java 8新特性探究(9):跟OOM:Permgen说再见吧
Java8 Demos and Samples