JVM内存区域划分 EDEN SPACE、SURVIVOR SPACE、TENURED GEN

时间:2024-05-22 08:23:45

JVM区域总体分两类,heap区和非heap区。heap区又分:Eden Space(伊甸园)、Survivor Space(幸存者区)、Tenured Gen(老年代-养老区)。 非heap区又分:Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。

JVM内存区域划分 EDEN SPACE、SURVIVOR SPACE、TENURED GEN

HotSpot虚拟机GC算法采用分代收集算法:

1、一个人(对象)出来(new 出来)后会在Eden Space(伊甸园)无忧无虑的生活,直到GC到来打破了他们平静的生活。GC会逐一问清楚每个对象的情况,有没有钱(此对象的引用)啊,因为GC想赚钱呀,有钱的才可以敲诈嘛。然后富人就会进入Survivor Space(幸存者区),穷人的就直接kill掉。

2、并不是进入Survivor Space(幸存者区)后就保证人身是安全的,但至少可以活段时间。GC会定期(可以自定义)会对这些人进行敲诈,亿万富翁每次都给钱,GC很满意,就让其进入了Genured Gen(养老区)。万元户经不住几次敲诈就没钱了,GC看没有啥价值啦,就直接kill掉了。

3、进入到养老区的人基本就可以保证人身安全啦,但是亿万富豪有的也会挥霍成穷光蛋,只要钱没了,GC还是kill掉。

分区的目的:新生区由于对象产生的比较多并且大都是朝生夕灭的,所以直接采用标记-清理算法。而养老区生命力很强,则采用复制算法,针对不同情况使用不同算法。

非heap区域中Perm Gen中放着类、方法的定义,jvm Stack区域放着方法参数、局域变量等的引用,方法执行顺序按照栈的先入后出方式。

以上转自:http://lhc1986.iteye.com/blog/1421832

以下转自:http://www.cnblogs.com/xhr8334/archive/2011/12/01/2270994.html


GC工作机制

SUN的jvm内存池被划分为以下几个部分:

EdenSpace (heap)

内存最初从这个线程池分配给大部分对象。

Survivor Space (heap)

用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。

Tenured Generation (heap)

用于保持已经在survivor space内存池中存在了一段时间的对象。

Permanent Generation (non-heap)

保存虚拟机自己的静态(reflective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的。

Code Cache (non-heap)

HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)。

简单来讲,jvm的内存回收过程是这样的:

对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。

若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时jvm gc停止所有在堆中运行的线程并执行清除动作。


现代JVM内存管理方法及GC的实现和主要思路

谨以此文纪念已经辞世的C语言之父,Dennis Ritchie。无论世事如何变迁,无论日月如何更替,您的光辉成就都照耀着现代计算机技术发展之路。

提到现代JVM内存管理,就不能不提到一个意义深远的东西,C语言。C语言最为人诟病,但是也是C语言最让人神往的,就是它的内存管理机制。在C语言中,程序员可以*的控制内存,自己决定内存里写0还是写1.所谓的数据类型转换,在C语言看来,不过就是内存里的几次复制以及排列位置的不同,仅此而已。

然而随着应用规模的不断增大,无论是盘根错节的对象耦合关系,还是巨大的内存使用量,都让开发人员麻爪。动辄几个GB的内存总量,动辄成千上万的内存对象数量,都不再是一个人乃至十个人可以控制的范围了。况且,百密一疏,只要有一点点内存泄露,随着时间的推移,都有可能变成无比的灾难。OOM之类的问题,在程序员眼里,早已经是家常便饭,谁还没溢出过内存呢,是吧。

的确是有高手可以控制好内存,但是不是所有人。那么,大规模团队化开发的时候,如何保证内存使用不出现问题呢?代码走查?人工校验?反复测试?这些能不能行的通先不谈,就算可行,巨大的工作量也可以让所有合同超期到下个世纪。于是有人提出了一个想法。可以不可以让一部分高手写出完善的内存管理模块,再加上一堆各式各样的类库和标准,最后构成一个庞大的运行时?

这一想法被无数语言团队采用。第一个实现的,就是James Gosling领导的Java团队。Java的目标是Write Once,Run Anywhere.估计他们在咖啡馆喝咖啡的时候一时写错了,应该是Debug Anywhere,这才符合现在的实际,呵呵。扯远了,我们回头看内存管理。

JVM提供了很多类库,封装了很多数据类型和常用工具类,作为自己的基本库来使用,比如java.lang包。举一个最简单的例子,来一句最简单的代码。int i = 5;

在C语言里,这句话申请了几个字节的内存,然后放了个5进去,Java也是这么搞的。只不过,C语言里申请了以后要自己管理,而Java你不用自己烦恼这个事情,虚拟机会帮你处理。它会判断何时需要,何时不需要。由此推开去,更加复杂的业务,比如连接数据库,读取文件,我们要做的只是调用类库而已,内存申请和释放都由虚拟机全盘接管,我们不用动一根手指头。

我们是爽了,虚拟机就头疼了。这么多对象,什么时候该销毁,什么时候该保持,什么时候要检查这些关系呢?在JVM里,这个事情有一个模块来做,也就是我们这片文章的主角,GC,Garbage Collection,垃圾回收。

假设我们是实现GC的程序员,那么我们要做什么呢?首先,负责分配内存,负责控制对象的持有计数,负责销毁内存对象,还得负责内存整理什么的。在Sun制定的JVM规范里,详细描述了GC部分要做的事情,这里就不赘述了,想看的话,请自行Google。

现有的JVM,主流的,分别是HotSpot和JRockit,主要研究对象也是这两个。这篇文章里,我们只研究HotSpot,也就是所谓的Sun JVM。目前阶段,Sun的GC方式主要有CMS和G1两种。考虑到效果和实际应用,这里只介绍CMS。

CMS,全称Concurrent Low Pause Collector,是JDK1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求较高的应用,并且预期这部分应用能够承受垃圾回收线程和应用线程共享处理器资源,且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。

JVM在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象,小部分是长周期的对象,对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频率垃圾回收以确保无谓地垃圾扫描检测。为解决这种矛盾,Sun JVM的内存管理采用分代的策略。

1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和BSuvivor Space。这么做主要是为了减少内存碎片的产生。

我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
3)持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。

相关应用

JVM GC调优一则--增大Eden Space提高性能

缘起

线上有Tomcat升级到7.0.52版,然后有应用的JVM FullGC变频繁,在高峰期socket连接数,Cpu使用率都暴增。

思路

思路是Tomcat本身的代码应该是没有问题的,有问题的可能是应用代码升级,或者环境改变了,总之Tomcat的优先级排在最后。

先把应用的heap dump下来分析下:

jmap -dump:format=b,file=path pid

用IBM的Heap Analyser分析,发现dubbo rpc调用的RpcInvocation对象和taglibs的SimpleForEachIterator对象占用了很大部分内存。

JVM内存区域划分 EDEN SPACE、SURVIVOR SPACE、TENURED GEN

正常来说,这两种类型的对象都应该可以很快被回收掉,怎么会占用了那么大的内存空间?是不是有别的对象引用了它们,导致不能释放?

再仔细分析,发现RpcInvocation对象都是root refer的,也就是根对象,正常来说根对象应该可以很快就被回收掉的,为什么在内存中会有那么多对象?

再查看应用的JVM参数:

-Xms2g -Xmx2g -Xmn256m -XX:SurvivorRatio=8 -XX:ParallelGCThreads=8 -XX:PermSize=512m -XX:MaxPermSize=512m -Xss256k -XX:-DisableExplicitGC -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
首先发现应用的新生代,即-Xmn256m 设置得太小了。对照上面RpcInvocation对象占用了226M,SimpleForEachIterator占用了267M内存。

显然在新生代里,没办法放下那么多的对象,这些对象必然是被放到老生代(old space)里去了。

既然RpcInvocation对象和SimpleForEachIterator对象应该都是可以很快被回收了,那么思路变成,触发一下线上的FullGC,看下对象有没有被回收。

在触发之前,先用jmap -histo pid统计下对象的数量:
34: 136762 4376384 com.alibaba.dubbo.rpc.RpcInvocation
129: 16345 392280 org.apache.taglibs.standard.tag.common.core.ForEachSupport$SimpleForEachIterator
用 jmap -histo:live <pid>触发Full GC之后:
294: 625 20000 com.alibaba.dubbo.rpc.RpcInvocation
495: 292 7008 org.apache.taglibs.standard.tag.common.core.ForEachSupport$SimpleForEachIterator
果然数量大大的减少了。

所以结论比较明显了,新生代(Young generation)的空间太小,导致有一些本应该可以很快就被回收的对象被放到了老生代(Old generation)里,导致老生代上涨很快,频繁Full GC。

于是想办法增加新生代的大小,把JVM参数改为:

-Xms2g -Xmx2g -XX:ParallelGCThreads=8 -XX:PermSize=256m -XX:MaxPermSize=512m -Xss256k -XX:-DisableExplicitGC -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
因为观察到PermSize实际上只用了不到200M,没有必要设置为512M,浪费内存,所以改为-XX:PermSize=256m -XX:MaxPermSize=512m 。

另外,把新生代最大限制-Xmn256m 去掉。因为默认的NewRatio = 2,即除了PermSize,新生代大约占内存的1/3,即约(2048 - 256) /3 = 597M。和原来相比增大了一倍不止。

修改上线之后,观察发现Old Space增长缓慢,FullGC次数大大减少,时间在50ms下,Yong GC都在10ms下,达到了想要的效果。

简单的GC过程分析

首先来看一张GC的模型图,很形象:

JVM内存区域划分 EDEN SPACE、SURVIVOR SPACE、TENURED GEN

简单来说,对于GC,我们了解到这些信息就足够了。

大部分新对象在Eden Space上分配,当Eden Space满了,则要用到Survivor Space来回收。YGC的算法是很快的。

多次YGC之后,还存活的对象就会被移到Old Generation(old space)上,当Old Generation满了的时候,就会FGC,FGC有通常比较慢。

Permanent Space只要你在开始时分配了足够大的空间,那它可以不用管。

我们可以得出一些结论:

  • 合理减少对象进入老生代;
  • Old Space可能会一直增长,有时没有办法避免不让对象进入Old Space,当然也有一些程序是从来都不执行FGC的;
  • 是不是尽全力防止对象进入老生代?显然不是,有些对象如果长久存在在新生代里,显然加重了YGC的负担,多次YGC之后仍然存活的对象显然应该放到Old Space里。

理想的GC/内存使用情况

总结下来,可以发现,理想的GC情况应该是这样的:

Old Space增长缓慢,FullGC次数少,FullGC的时间短(大部情况应该要在1秒内)。

总结:

尽量少加上一些默认参数。这点我很赞同RednaxelaFX的看法,配置了默认参数除了让后面调优的人蛋疼之外,没有太多的帮助。

GC调优就是一个取舍权衡的过程,有得必有失,是好可以在多个不同的实例里,配置不同的参数,然后进行比较。

有很多命令行工具或者图形工具可以使用,好的工具事半功倍。

参考:

http://www.alphaworks.ibm.com/tech/heapanalyzer‎ IBM Heap Analyser

http://hllvm.group.iteye.com/group/topic/27945 JVM调优的"标准参数"的各种陷阱,RednaxelaFX 出品,强列推荐

http://www.taobaotesting.com/blogs/2392 JAVA性能剖析1——JVM内存管理与垃圾回收

http://www.oschina.net/translate/using-headless-mode-in-java-se 在 Java SE 平台上使用 Headless 模式