JVM垃圾回收算法与垃圾回收器

时间:2022-12-27 23:34:14

JVM内存模型总体架构图

JVM垃圾回收算法与垃圾回收器

程序计数器
多线程时,当线程数超过CPU数量或CPU内核数量,线程之间就要根据时间片轮询抢夺CPU时间资源。因此每个线程有要有一个独立的程序计数器,记录下一条要运行的指令。线程私有的内存区域。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。
虚拟机栈
线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出*Error;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。
使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

JVM垃圾回收算法与垃圾回收器

本地方法区
和虚拟机栈功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C实现的。

JAVA堆
线程共享的,存放所有对象实例和数组。垃圾回收的主要区域。可以分为新生代和老年代(tenured)。
新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。
新生代又可进一步细分为eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。刚创建的对象都放入eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代(tenured)。

JVM垃圾回收算法与垃圾回收器
方法区
线程共享的,用于存放被虚拟机加载的类的元数据信息:如常量、静态变量、即时编译器编译后的代码。也成为永久代。如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收

   java虚拟机JVM垃圾收集算法有四种:标记-清除算法、复制算法、标记-整理算法以及分代收集算法。

1、标记-清除算法

      这是JVM最基础的垃圾收集算法。如下图:

          JVM垃圾回收算法与垃圾回收器

      该算法分为两个阶段:“标记”和“清除”。首先标记处所有需要回收的对象,然后统一清除被标记的对象。

      该算法,标记和清除两个阶段的效率不高。此外,回收后会产生大量的不连续的内存碎片,进一步会导致垃圾回收次数的增加。

2、复制算法

      为了解决标记-清除算法的效率问题,出现了复制算法,如下图:

          JVM垃圾回收算法与垃圾回收器

      该算法的思想是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,则将还存活的对象复制到另外一块内存上去,然后再把刚使用过的内存空间一次清理掉。从而达到了每次垃圾回收时只是针对其中一块,避免了产生内存碎片等情况。

      该算法的代价是只是使用了其中一本的内存,代价有点高。

      随着研究的不断发现,商业虚拟机在该算法中不断进行优化尝试。比如在HotSpot虚拟机中,新生代中Eden和Survivor的大小比例为8::1,因为新生代的对象需要回收的概率大(对象的生命周期短,存活率低),所以内存的可用率达到了90%(新生代分为:Eden和两块Survivor)。每次都是把Eden和Survivor中存活的对象拷贝到另一块Survivor中,然后清理掉Eden和Survivor空间。

3、标记-整理算法

      当复制收集算法面对的回收对象为存活率较高的情况时,要执行较多的复制操作,效率会变低。为了提高这些对象垃圾回收效率,充分利用可用内存,标记-整理算法出现了。如下图:

        JVM垃圾回收算法与垃圾回收器

      该算法集成了标记-清除和复制收集算法的优点。第一个阶段仍是进行标记,第二个阶段是把所有存活的对象都向一端移动,按顺序排放,然后直接清理掉端边界意外的内存。该算法避免了标记-清除的内存碎片问题以及复制算法的空间问题。该算法适合于老年代对象的回收。

4、分代收集

      这是目前大多数虚拟机采用的垃圾回收算法。基于对象的生命周期划分为新生代、老年代以及持久代。比如新生代就采用复制收集算法,而老年代就采用标记-清除或者标记-整理算法。如下图:

          JVM垃圾回收算法与垃圾回收器

      对于分代收集,虚拟机需要区分对象的分配年代,是放在新生代还是否放在老年代?。解决的办法是:jvm为每个对象定义了一个对象年龄计数器。如果对象在Eden出生并且经过第一次新生代GC(Minor GC)后仍然存活并且能被Survivor容纳,则该对象将被移动到另一块Survivor空间,并将对象年龄计数器加1。对象在Survivor区中每经历过一次Minor GC,年龄计数器就加1,当它的年龄达到设定的阈值(默认是15)时,则被移动到老年代中。阈值的设置通过参数-XX:MaxTenuringThreshold设置。

      当然,并不是一定达到阈值才被移动到老年代,为了适应复杂的情况,动态的判定对象年龄,虚拟机规定:如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,对象的年龄大于或者等于该年龄的对象就可以直接进入老年代,不必等到达到设定的阈值。


垃圾回收器:

Serial(串行GC)收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

ParNew(并行GC)收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
①.初始标记(CMS initial mark)
②.并发标记(CMS concurrenr mark)
③.重新标记(CMS remark)
④.并发清除(CMS concurrent sweep)
     其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。      由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点: CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。 CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full  GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,
即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full  GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程。

G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。