上期我们讲到LoadRunner性能测GC回收机制,这期我们讲LoadRunner性能测试垃圾收集算法。
垃圾收集算法
上篇文章介绍了GC回收的机制,那么当触发回收机制时,回收垃圾的方法又有哪些呢?这就是本小节要解决的问题,常见的垃圾收集算法包括:标记-清除算法、复制算法、标记-整理算法、分代收集算法。 1)标记-清除算法(Mark-Sweep) 标记-清除算法分为两个阶段完成:标记阶段和清除阶段。标记阶段是将所有需要被回收的对象标记出来,清除阶段就是回收被标记的对象所占用的空间。具体过程如图所示。
标记-清除算法是使用finalize()方法来进行标记的,从根集合GC Roots开始进行扫描,对还存活的对象进行标记,等标记完成后,再重新扫描一次,将未被标记的对象找到并对其进行回收,标记-清除算法如图所示。
使用标记-清除算法并不会将对象进行移动,只是对不存活的对象进行处理回收即可,如果处理的对象中存活对象比很高,那么这种方法是很高效的,但是由于不存活的对象可能不是连续的,所以会出现一个问题,会造成内存碎片。
2)复制算法(Copying) 为了解决Mark-Sweep标记-清除算法中关于内存碎片的问题,提出了Copying复制算法,原理是将可用内存划分为大小相等的两块,每次只使用其中的一块,当这块内存使用完成后,就会将还存活的对象复制到另外一块上面,然后把已使用的一块内存清理掉,这样就不会出现Mark-Sweep标记-清除算法内存碎片的问题,如图所示。
复制算法很简单,运行也很高效且不容易产生内存碎片,但前提是以内存为代价,使能够使用的内存缩减到原来的一半。当然复制算法的效率快慢就与存活对象的数目有很大关系了,如果存活的对象很多,那么复制算法的效率就大大的降低了。
复制算法为了解决句柄开销和内存碎片的问题,在一开始就会把堆分成一个对象面和多个空闲面,程序在对象面为工作的对象分配空间,当对象填满后,复制算法的垃圾收集器就从根集合GC Roots中扫描活动对象,并将每个活动对象复制到空闲面,这样空闲面变成了对象面,原来的对象面就变成了空闲面,程序会在新的对象面中来分配内存。如图所示。
3)标记-整理算法(Mark-compact) 复制法牺牲了内存空间,为了解决复制算法的问题,提出了标记-整理算法Mark-Compact算法。该算法与Mark-Sweep有一些相似之处,首先也需要标记对象,但标记完成后不会直接清理可回收的对象,而是将存活的对象都向一端移动,然后清理掉端边界之外的内存。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是解决了内存碎片的问题。如图所示。
4)分代收集算法(GenerationalCollection) 分代收集算法应该是目前用的最多的一种JVM垃圾收集器采用的算法,之所以使用分代收集算法,是因为JVM把内存划分为年轻代、年老代和持久代,如果没有持久代就不用考虑,其中年轻代和年老代是存放在堆中的,因为每个代需要收集的对象有很大不同,使用的频率也有很大不同,所以会对不同代进行不同的回收算法。这种就是通常说的分代收集算法。
分代收集算法其实通常也是使用上面说的三种收集算法,只是在不同的代采用不同的方法来收集而已,对于新生代一般采用的都是复制Copying算法,新生代每次垃圾回收时都会回收了大部分对象,这样复制的操作次数就会少一些,新生代由一块较大的Eden空间和两块小一些的Survivor空间组成,一般每次使用Eden空间和其中的一块Survivor空间,当进行垃圾回收时,会将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后将Eden空间和刚才使用过的Survivor空间清理掉。年老代就不可能像年轻代回收的那么频繁,并且每次回收的对象也不会像年轻代那么多,所以年老代一般使用标记-整理Mark-Compact算法。
对于年轻代来说,需要尽快的回收了那些生命周期短的对象,年轻代分为一个eden区和两个survivor区,大部分的对象是在eden区中生成,当GC回收后会将eden区中存活的对象复制到一个survivor0区中,这样eden区就会被清空,当survivor0区都已经存满对象后,就会将对象复制到另外一个survivor1区,再清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空,如此循环。
但当survivor1区不中以存放eden和survivor0的存活对象时,就将存活对象直接放到年老代中,重复这个过程循环后,年老代就可能会存满对象,如果年老代也存满了对象,那么就会触发一次FullGC回收。如果只是年轻代发生GC回收时称之为MinorGC。
如果JVM有持久代的话,持久代也会进行GC回收但持久代回收的效果并不明显,因为持久代主要用于存放静态文件,如Java类、方法等。
GC有两种类型:ScavengeGC和FullGC。 当新对象生成时,在eden区中申请空间失败时,就会触发ScavengeGC进行回收,ScavengeGC的效率比FullGC高太多了,因为eden区要经常进行回收,所以GC回收的效果一定要高,否则无法进经常回收。 一般情况下,当新对象生成,并且在Eden申请空间失败时就会触发ScavengeGC,对Eden区域进行GC清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。
因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden能尽快空闲出来。
由于FullGC回收的内容比较多,所以一般不可能频繁的触发FullGC回收。 一般有以下条件触发时才会触发FullGC回收机制: 1.年老代被写满。 2.持久代被写满。 3.system.gc()被调用。 4.上一次GC后,heap分配的策略发生改变。