JVM堆模型

JVM堆(Heap)= 新生代(Young) + 旧生代(Tenured)

新生代(Young)= Eden区 + Survivor区



 


 

http://blog.csdn.net/jollyant/article/details/5647141

http://blog.csdn.net/zhangren07/article/details/6270895

http://blog.csdn.net/cutesource/article/details/5906705

http://www.linuxidc.com/Linux/2011-05/36506.htm

JVM学习

JVM学习2

JVM垃圾回收与性能调优总结

JVM问题

 

NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2

SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为32

堆/Heap

JVM管理的内存叫堆;在32Bit操作系统上有4G的限制,一般来说Windows下为2G,而Linux下为3G;64Bit的就没有这个限 制。

TLAB:

JVM所占用的主要内存都是从堆空间分配的,堆是所有线程共享的因此在堆上分配内存需要加锁Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。其大小由JVM根据运行情况计算得到,也可通过参数-XX:TLABWasteTargetPercent来设置TLAB可占用的Eden空间的百分比,默认值为1%。在TLAB上分配内存不需要加锁因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64但小于1G。

JVM最大分配的内存由-Xmx指定,默认是物理内存的 1/4但小于1G。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指 定。 

默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。

服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小,所以上面的两个参数没啥用。

分代/堆模型

分代是Java垃圾收集的一大亮点,根据对象的生命周期长短,把堆分为3个代:Young,Old和Permanent,根据不同代的特点采用不同 的收集算法,可以扬长避短。

分区作用: 

新创建的对象通常先将其分配在新生代中,在新生代中经过若干次GC之后仍未释放的对象,再将它移动到旧生代。为了让内存回收更高效(GC会暂停JVM中的应用),Sun JDK在1.2开始对堆采用了分代管理的方式。在分配对象遇到内存不足时,先对新生代进行GC(Young GC);当新生代GC之后仍无法满足内存空间分配需求时, 才会对整个堆空间以及方法区进行GC (Full GC)

Young(Nursery):年轻代

研究表明大部分对象都是朝生暮死,随生随灭的。所以对于年轻代在GC时都采取复制收集算法,具体算法参考下面的描述;

Young的默认值为 4M,随堆内存增大,约为1/15,JVM会根据情况动态管理其大小变化。

Young里面又分为3个区域

一个Eden,所有新建对象都会存在于 该区

两个Survivor区,用来实施复制算法

Eden区为对象通常最初分配到的地方,Survivor区分为S0和S1两块大小相等的区域。

JVM进行Minor GC时,将Eden中还存活的对象拷贝到Survivor区中,还会将Survivor区中还存活的对象拷贝到Old区中。在这种GC模式下,JVM为了提升GC效率, 将Survivor区分为S0和S1,这样就可以将对象回收和对象晋升分离开来。

-XX:NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2,但实际上young启动时远低于这个比率?如果信不过JVM,也可以用 -Xmn硬性规定其大小,有文档推荐设为Heap总大小的1/4。

-XX:SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为32。Survivio大了会浪费,小了的话,会使一些年轻对象潜逃到老人区,引起老人区的不安, 但这个参数对性能并不太重要。

Old(Tenured):年老代

年轻代的对象如果能够挺过数次收集,就会进入老人区

老人区使用标记整理算法。因为老人区的对象都没那么容易死的,采用复制算法就要反复的复制对 象,很不合算,只好采用标记清理算法,但标记清理算法其实也不轻松,每次都要遍历区域内所有对象,所以还是没有免费的午餐啊。

-XX:MaxTenuringThreshold= 设置熬过年轻代多少次收集后移入老人区,CMS中默认为0,熬过第一次GC就转入,可以用-XX:+PrintTenuringDistribution 查看。

Permanent(Perm):持久代

装载Class信息等基础数据,默认64M,如果是类很多很多的服务程序,需要加大其设置-XX:MaxPermSize=,否则它满了之后会引起 fullgc()或Out of Memory。 注意Spring,Hibernate这类喜欢AOP动态生成类的框架需要更多的持久代内存。一般情况下,持久代是不会进行GC的,除非通过 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进行强制设置。

持久代也被成为方法区,方法区是全局共享的,在一定条件下也会被GC。

持久代存放JVM加载时的类型信息

        类型基本信息

        常量池

        类变量

        方法信息

        ClassLoader引用

        Class类引用

GC的类型

当每个代满了之后都会自动促发collection,各收集器触发的条件不一样,当然也可以通过一些参数进行强制设定。主要分为两种类型:

Minor Collection:GC用较高的频率对young进行扫描和回收,采用复制算法

Major Collection同时对Young和Old进行内存收集,也叫Full 

GC;因为成本关系对Old的检查回收频率要比Young低很多,采用标记清除/标记整理算法。可以通过调用代码System.gc()引发major collection,使用-XX:+DisableExplicitGC禁止它,或设为CMS并发 -XX:+ExplicitGCInvokesConcurrent。

更为具体的阐述如下:

由于年轻代进进出出的人多而频繁,所以年轻代的GC也就频繁一点,但涉及范围也就年轻代这点弹丸之地内的对象,其特点 就是少量,多次,但快速,称之为Minor Collection。当年轻代的内存使用达到一定的阀值时,Minor Collection就被触发,Eden及某一Survior space(from space)之内存活的的对象被移到另一个空的Survior space(to space)中,然后from space和to space角色对调。当一个对象在两个survivor space之间移动过一定次数(达到预设的阀值)时,它就足够old了,够资格呆在年老代了。当然,如果survivor space比较小不足以容下所有live objects时,部分live objects也会直接晋升到年老代。

Survior spaces可以看作是Eden和年老代之间的缓冲,通过该缓冲可以检验一个对象生命周期是否足够的长,因为某些对象虽然逃过了一次Minor Collection,并不能说明其生命周期足够长,说不定在下一次Minor Collection之前就挂了。这样一定程度上确保了进入年老代的对象是货真价实的,减少了年老代空间使用的增长速度,也就降低年老代GC的频率

当 年老代或者永久代的内存使用达到一定阀值时,一次基于所有代的GC就触发了,其特定是涉及范围广(量大),耗费的时间相对较长(较慢),但是频率比较低 (次数少),称之为Major Collection(Full Collection)。通常,首先使用针对年轻代的GC算法进行年轻代的GC,然后使用针对年老代的GC算法对年老代和永久代进行GC。

总结:

最小收集

较高频率对年轻代进行扫描、回收

年轻代内存使用达到阀值  --->【触发Min GC】 Eden及from space内的存活对象移入to space                                               |

                                                       |

                                                       |【不足以容纳所有对象时,部分移入老人代】

                                                       V 

---> from/to 角色对调 --->【一个对象移动到一定次数】  移入老人代

最大收集

同时对年轻代、年老代、永久代进行内存收集

Full GC

1、年老代、永久代内存使用达到阀值

2、Yong GC后内存仍然不够分配时

 

 

GC收集算法

  • 复制 (copying):将堆内分成两个相同空间,从根(ThreadLocal的对象,静态对象)开始访问 每一个关联的活跃对象,空间A的活跃对象全部复制到空间B,然后一次性回收整个空间A
    因为只访问活跃对象,将所有活动对象复制走之后就清空整 个空间,不用去访问死对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。可参考如下的示例图:
  • 标记清除 (mark-sweep):收集器先从根开始访问所有活跃对象,标记为活跃对象。然后再遍历一次整个 内存区域,把所有没有标记活跃的对象进行回收处理。该算法遍历整个空间的成本较大暂停时间随空间大小线性增大,而且整理后堆里的碎片很多。可参考如下的示 例图:
  • 标记整理 (mark-sweep-compact):综合了上述两者的做法和优点,先标记活跃对象,然后将其 合并成较大的内存块。可参考如下的示例图:

 

总结:

GC收集算法

1、复制 (copying)

  将堆内分成两个相同空间,将空间A的活跃对象全部复制到空间B,然后一次性回收空间A

  

  需要拆分

  只访问活跃对象,所以遍历空间成本小,复制成本大

  

2、标记清除 (mark-sweep)

 遍历第一次访问所有活跃对象并标记

 遍历第二次回收所有未标记对象

  遍历成本大,碎片多

  空间越大暂停时间越多

3、标记整理 (mark-sweep-compact)

compact : 压紧、使紧凑

  综合了上述两者的做法和优点,标记清理后合并活跃对象成较大的内存块

  成本高,但不产生碎片

 

并行、并发的区别

并行(Parallel)与并发(Concurrent)仅一字之差,但体现的意思却完全不同,这可能也是很多同学非常困惑的地方,要想深刻体会这 其中的差别,可以多揣摩下上面关于GC收集器的示例图;

并行指多条垃圾收集线程并行,此时用户线程是没有运行的;

并发指用户线程与垃圾收集线程并发执行,程序在继续运行,而垃圾收集程序运行于另一个个CPU上。

并发收集一开始会很短暂的停止一次所有线程来开始初始标记根对象,然后标记线程与应用线程一起并发运行,最后又很短的暂停一次,多线程并行的重新标 记之前可能因为并发而漏掉的对象,然后就开始与应用程序并发的清除过程。可见,最长的两个遍历过程都是与应用程序并发执行的,比以前的串行算法改进太多太 多了!!!

串行标记清除是等年老代满了再开始收集的,而并发收集因为要与应用程序一起运行,如果满了才收集,应用程序就无内存可用,所以系统默认 68%满的时候就开始收集。内存已设得较大,吃内存又没有这么快的时候,可以用-XX:CMSInitiatingOccupancyFraction= 恰当增大该比率。

年轻代的痛

由于对年轻代的复制收集,依然必须停止所有应用程序线程,原理如此,只能靠多CPU,多收集线程并发来提高收集速度,但除非你的Server独占整 台服务器,否则如果服务器上本身还有很多其他线程时,切换起来速度就..... 所以,搞到最后,暂停时间的瓶颈就落在了年轻代的复制算法上。

因 此Young的大小设置挺重要的 ,大点就不用频繁GC,而且增大GC的间隔后,可以让多点对象自己死掉而不用复制了。 但Young增大时,GC造成的停顿时间攀升得非常恐怖,据某人的测试结果显示:默认8M的Young,只需要几毫秒的时间,64M就升到90毫秒,而升 到256M时,就要到300毫秒了,峰值还会攀到恐怖的800ms。谁叫复制算法,要等Young满了才开始收集,开始收集就要停止所有线程呢。

====================================================

使用JDK自带工具jstat发现内存泄露问题

幸存者0区(Survivor 0 space)和幸存者1区(Survivor1 space):

伊甸园的空间用完时,程序又需要创建对象;此时JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁工作。同时将伊甸园中的还有其他对象引用的对象移动到幸存者0区。幸存者0区就是用于存放伊甸园垃圾回收时所幸存下来的JAVA对象。

当将伊甸园中的还有其他对象引用的对象移动到幸存者0区时如果幸存者0区也没有空间来存放这些对象时,JVM的垃圾回收器将对幸存者0区进行垃圾回收处理,将幸存者0区中不在有其他对象引用的JAVA对象进行销毁,将幸存者0区中还有其他对象引用的对象移动到幸存者1区。幸存者1区的作用就是用于存放幸存者0区垃圾回收处理所幸存下来的JAVA对象。

 

养老区(Tenure (Old) generation space):用于保存从新生区筛选出来的JAVA对象。

 

垃圾回收描述:

垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

 

当一个URL被访问时,内存申请过程如下:

A. JVM会试图为相关Java对象在Eden中初始化一块内存区域

B. 当Eden空间足够时,内存申请结束。否则到下一步

C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收);释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区/OLD区

D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区

E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)

F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”

对象衰老过程:

1. young generation的内存,由一块Eden和两块Survivor Space构成。新创建的对象的内存都分配自eden。两块Survivor Space总有会一块是空闲的,用作copying collection的目标空间。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。所谓survivor,也就是大部分对象在Eden出生后,根本活不过一次GC。对象在young generation里经历了一定次数的minor collection后,年纪大了,就会被移到old generation中,称为tenuring。

 

2. 剩余内存空间不足会触发GC,如eden空间不够了就要进行minor collection,old generation空间不够要进行major collection,permanent generation空间不足会引发Full GC。

 

那么如何从jstat的输出来诊断应用程序是否有内存泄露的问题呢?下面列出几个经验总结(其实这才是本文的重点)

 

如何判断应用程序是否有内存的问题:

 

1. Full GC的频率,时长和效果: 如果Full GC频率较高,比如数秒一次,那么此时程序可能就已经出问题了,因为jvm在Full GC的时候是不响应外部请求的。

如果Full GC时间较长,比如持续数秒,那么此时程序可能就已经出问题了,因为jvm在Full GC的时候是不响应外部请求的。

如果Full GC之后old 区内存没有显著增加,那么程序很可能有内存泄露问题,并且不久将来可能出现outofmemory异常。

如果young gc和full gc能够正常发生,且都能有效回收内存,常驻内存区变化不明显,则说明java内存释放情况正常,垃圾回收及时,java内存泄露的几率就会大大降低。但也不能说明一定没有内存泄露。

 

2. GC的频率,时长和效果: 如果JVM进行内存回收的频率非常高,比如几乎每数秒中就有一次,每次回收的时间为数秒钟;并且,通过输出还发现每次回收释放的内存非常有限,大多数对象都无法回收。这种现象很大程度上暗示着内存泄漏。(此时可以用“jmap”来获得当前的一个内存映象,看看哪些对象导致这个问题来找出原因)

如果每次GC时间特别长,比如说数十秒,那这种现象很大程度上暗示着内存泄漏。(内存中对象太多,导致遍历时间太长,有时候不好的缓存机制会造成这样的问题)

 

3. 常驻内存区(P)的使用率:  常驻内存如果在应用程序稳定运行较长一段时间后还在持续增长,或者在某段某几段时刻有突变,则有可能有内存问题。(当然很大可能是jvm/app server配置问题) 如果P始终停留在某个值,说明常驻内存没有突变,比较正常。

调优总结

  1. 年轻代大小选择
    • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  2. 年老代大小选择
    • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例
      减少年轻代和年老代花费的时间,一般会提高应用的效率
    • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
  3. 较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
    • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

猜你喜欢

转载自uule.iteye.com/blog/1894724