分代GC
JVM的GC机制叫做分代GC(generational GC),把内存分为两种:新生代(Young Generation)和老年代(Old Generation)。这种GC机制是基于这么一个现象:内存中的大部分对象的生命周期都很短,可能只存在几十毫秒,其他少部分对象的生命周期则很长,比如几分钟。
当一个对象最开始被分配到堆上的时候,会把它放到young generation上。当这个对象在Young Generation上,经过若干次GC之后,如果依然alive,就会把它放到Old Generation。在Young Generation上的GC称为minor gc,在Old Generation上GC称为full gc。
Young Generation
在Young Genration内部,又分为3个区域,一个eden区,两个survivor区,分别称作survivor0和survivor1。
- 每次新创建的对象都会存在于eden区,然后经过一次gc之后,还存活的对象会copy到survivor0区,eden区清空。
- 这样,等到下次gc开始的时候,survivor1保持空,eden区存放的是从上次GC开始到这次GC这段时间创建的新的对象,survivor0存放的是上次GC中survive的对象。那么这次GC的时候,会在eden和survivor0中,把那些alive的对象都copy到survivor1,eden和survivor0清空。
- 这样循环往复。
- 经过若干次(Hotspout是15次,可以用-XX:MaxTenuringThreshold控制)之后,依然存活的对象copy到Old Generation。
从上面的gc过程可以看出:
- survivor0和survivor1总有一个会保持为空
- 每次GC,并不是把无用的对象清理掉,而是把alive的对象copy到survivor0或者survivor1。之所以要强调这一点,是因为这样的做法保证了Young Generation中的可用空间都是连续的。
- GC的时候是stop-the-world的,但是时间非常短。在生产环境中,一般不推荐超过512M,这样即使在最坏的情况下GC时间也不会超过几百毫秒
Old Generation
对于old generation中,GC算法就不会像young generation那样简单粗暴了。通常使用CMS GC算法,也就是Concurrent Mark-Sweep GC。
CMS GC有以下几个阶段
- initial-mark (stops the world). In this phase, the CMS collector places a mark on the root objects. A root object is something directly referenced from a live Thread – for example, the local variables in use by that thread. This phase is short because the number of roots is very small.
- concurrent-mark (concurrent). The collector now follows every pointer starting from the root objects until it has marked all live objects in the system.
- remark (stops the world). Since objects might have had references changed, and new objects might have been created during concurrent-mark, we need to go back and take those into account in this phase. This is short because a special data structure allows us to only inspect those objects that were modified during the prior phase.
- concurrent-sweep (concurrent). Now, we proceed through all objects in the heap. Any object without a mark is collected and considered free space. New objects allocated during this time are marked as they are created so that they aren’t accidentally collected.
The important things to note here are:
- The stop-the-world phases are made to be very short. The long work of scanning the whole heap and sweeping up the dead objects happens concurrently.
- This collector does not relocate the live objects, so free space can be spread in different chunks throughout the heap. We’ll come back to this later!
Stop The World
不管选择哪种GC算法,Stop-The-World都是不可避免的。Stop-The-World意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-The-World发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善Stop-The-World的时间。
JVM 配置调优
调优目标
GC的时候会导致Stop The World,如果JVM频繁GC,会严重影响系统的性能。通过对JVM的参数进行调优,我们期望达到以下目
- GC的时间足够短
- GC的次数足够少
- 发生Full GC的周期足够长
前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。
对于Full GC,我们要尽可能地避免。一般造成Full GC的原因有以下几点
- 老年代空间不足
- CMS GC的时候出现promotion failure和concurrent mode failure。
promotion failure是在进行Minor GC的时候,survivor space放不下,对象只能放入老年代,而此时老年代也放不下。
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
JVM配置
为了达到上面的目的,你需要做的事情有:
1、减少使用全局变量和大对象
2、调整新生代的大小到最合适
3、设置老年代的大小为最合适
4、选择合适的GC收集器
对应的JVM的配置参数有
参数 | 作用 |
---|---|
-Xms | 设置JVM的初始内存 |
-Xmx | 设置JVM的最大可用内存 |
-Xmn1024m | 设置年轻代的大小 |
-XX:SurvivorRatio | 设置年轻代中Eden区与Survivor区的大小比值。如果设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 |
-XX:NewRatio | 设置年轻代(包括Eden和两个Survivor区)与年老代的比值。如果设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 |
-XX:NewSize 和 -XX:MaxNewSize | 设置年轻代大小 |
-XX:MaxPermSize | 设置持久代大小 |
-XX:MaxTenuringThreshold | 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率 |
有三个参数都可以设置年轻代大小
- -XX:NewSize=1024m和-XX:MaxNewSize=1024m;
- -Xmn1024m;
- -XX:NewRatio=2; (假设Heap总共是3G)
经过实践,这些参数优先级如下
最高优先级: -XX:NewSize=1024m和-XX:MaxNewSize=1024m
次高优先级: -Xmn1024m (默认等效效果是:-XX:NewSize==-XX:MaxNewSize==1024m)
最低优先级:-XX:NewRatio=2
对于GC收集器,JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。
参数 | 作用 |
---|---|
-XX:+UseParallelGC | 配置年轻代的垃圾收集方式为并行收集 |
-XX:+UseParallelOldGC | 配置年老代垃圾收集方式为并行收集 |
-XX:ParallelGCThreads=20 | 配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等 |
-XX:+UseConcMarkSweepGC | 设置年老代为并发收集 |
-XX:+UseParNewGC | 设置年轻代为并发收集 |
-XX:+UseCMSCompactAtFullCollection | 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。打开对年老代的压缩。可能会影响性能,但是可以消除碎片 |
XX:+PrintGCDetails XX:+PrintGCTimeStamps Xloggc:/usr/aaa/dump/heap_trace.txt | 垃圾回收统计信息 |
调优总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息
持久代并发收集次数
传统GC信息
花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩