Java垃圾收集器及内存分配
一、垃圾收集算法
1. 标记-清除算法
分为标记、清除两个阶段:首先标记出要回收的对象,在标记完成后统一回收掉被标记的对象,标记过程就是上文讲的二次标记。缺点:标记和清理的效率都不高;标记清理后会产生大量不连续的内存碎片,将导致程序运行过程中要分配大对象是没有足够的连续空间而不得不提前触发垃圾收集。
2. 复制算法
它将可用内存分为2块大小相等的区域,每次只使用其中一块,当这一块的内存用完时,将活着的对象全部复制到另外一块,然后将已使用过的那一块内存全部清理。实现简单,运行高效,但是内存需要缩小为原来的一般,代价太高。
在现在的商业虚拟机中用这种算法回收新生代,因为新生代的对象大都是朝生夕死的,并不需要按照1:1划分空间,而是将大小分为一块较大的Eden空间和两块叫小的Survivor空间,每次使用时只使用Eden和一块Survivor,回收时将它们中存活的对象都拷贝到另一块Survivor中,然后清理掉Eden和使用过的Survivor空间,不过在极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。
3. 标记-整理算法
而在老年代中复制算法显然不适用,于是有了标记-整理算法,标记过程用标记-清理,之后让所有的存活对象都向一端移动,然后清理掉端边界以外的内存。
4. 分代收集算法
一般将Java堆分为新生代和老年代,然后根据不同代的特点使用不同算法进行收集。
二、垃圾收集器
1. Serial收集器
单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
2. ParNow收集器
Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
3. Parallel Scavenge收集器
也是一个新生代收集器,也是使用复制算法,并且是并行的,parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。它提供两个参数用于控制吞吐量:-XX:MaxGCPauseMills设置最大垃圾收集停顿时间,-XX:GCTimeRatio设置吞吐量大小。
除此以外,还有一个-XX:UseAdaptiveSizePolicy开关参数。打开时,就不需要指定-Xmn(新生代大小);-XX:SurvivorRato(Eden与Survivor的比例)以及-XX:PretenureSizeshold(晋升老年代对象年龄)等细节参数,虚拟机将动态调节这一些。
4. Serial Old算法
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
5. Parallel Old算法
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
6.CMS算法
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
* 初始标记
* 并发标记
* 重新标记
* 并发清除
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集、低停顿
缺点:1、CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。 2、无法处理浮动垃圾(在CMS并发清理阶段产生的新垃圾),也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。 3、,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
7、G1收集器
G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。
三、参数总结
-XX:+< option > 启用选项
-XX:-< option > 不启用选项
-XX:< option >=< number>
-XX:< ption>=< string >
参数 | 描述 |
---|---|
-XX:+UseSerialGC | Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
-XX:+UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。 |
-XX:+UseParallelGC | vm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC | 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 |
-XX:SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold | 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold | 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy | 动态调整java堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure | 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX:ParallelGCThreads | 设置并行GC进行内存回收的线程数 |
-XX:GCTimeRatio | C时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX:MaxGCPauseMillis | 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection | 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效 |
-XX:+CMSFullGCBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用 |
-XX:+UseFastAccessorMethods | 原始类型优化 |
-XX:+DisableExplicitGC | 是否关闭手动System.gc |
-XX:+CMSParallelRemarkEnabled | 降低标记停顿 |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m |
四、内存分配与回收
1、对象优先在Eden上分配
大多数情况下,对象在新生代Eden中分配,当Eden中没有足够的空间分配时会引起一次Minor GC。
注意:
新生代GC(Minor GC) 发生在新生代的垃圾收集动作,发生非常频繁,一般回收速度比较快。
老年代GC(Major GC/Full GC) 发生在老年代,出现major gc,经常会伴随着至少一次Minor GC(非绝对),速度一般比Minor GC慢10倍以上。
2、大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象。虚拟机可通过-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。
3、长期存活的对象将进入老年代
虚拟机会给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍存活,并且能被Survivor接纳的话,将移到Survivor空间中,并将对象年龄设为1,对象在Survivor空间没熬过一次Minor GC,年龄就加一,当到一定数值(默认15)时,就会晋升到老年代中。可通过-XX:MacTenuringThreshold设置。
4、动态对象年龄判定
为了更好适应不同程度的内存状况,虚拟机并不总是要求对象的年龄到MaxTenurintThreshold才晋升老年代,如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于等于这个年龄的对象就会直接进入到老年代。
5、空间分配担保
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代剩余大小,如果大于,则改为直接进行一次Full GC,如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行Minor GC,如果不允许,则改成一次Full GC。