标准 专业
多元 极客

JVM研究院(1)——垃圾回收的基础

对象存活

垃圾收集的前提是判断对象是否存活,JVM采用了可达性分析算法而并非引用计数算法来判断对象的存活情况。

可达性分析算法

目前Java语言主要采用可达性分析算法判断对象是否存活。可达性分析算法的基本思想就是通过一系列的“GC Roots”对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots并没有任何引用链时,也就是说这个对象到GC Roots不可达时,则证明这个对象是不可用的。具体实现如图所示:

可达性分析算法

根据图中显示,Object One、Object Two和Object Three都和GC Roots有引用链,但是Object Four和Object Five与GC Roots之间没有引用链,故根据可达性分析算法,Object Four与Object Five会被判定为不可用。

在Java语言中,以下身份的对象可以被称为GC Roots:

  1. 虚拟机栈中引用的对象(关于虚拟机栈中的内容,详情请见)。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中Native方法引用的对象。

再谈引用

对象是否存活都跟引用有关联,所以我们在这里将会谈一谈Java中的引用。

在JDK1.2以后,Java中的引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference),下面将会对这几种引用进行逐一介绍:

  1. 强引用。强引用是Java引用中最强的引用,类似于String str = new String(“Sunshine”)都是强引用,只要强引用仍然存在,那么Java垃圾收集器就不会对这个对象进行回收。
  2. 软引用。软引用的强度仅次于强引用,用来描述一些还有点作用,但并非必须的对象。软引用关联着的对象,会在系统内存溢出之前,被垃圾收集器进行第二次回收。
  3. 弱引用。弱引用的强度比软引用还要弱。它也是用来描述一些非必须的对象,弱引用关联的对象在下一次垃圾回收时必须被回收。无论当前内存是否够用,垃圾收集器都会回收掉只被弱引用关联 着的对象。
  4. 虚引用。虚引用也成为幽灵引用或者是幻影引用。它是最弱的一种关联关系。一个对象是否有虚引用的存在,完全不会对其生存空间构成影响,也无法通过虚引用来获得一个对象的实例,它存在的意义就是对象再被回收时,会收到一个系统的通知。

垃圾收集算法

标记-清除算法

标记-清除算法是指针可达递归定义的简单实现,它分为两个阶段:在第一阶段被称为追踪标记阶段,垃圾收集器从GC Roots开始遍历可访问的对象,并进行标记。第二阶段被称为对象清除阶段,垃圾收集器检查Java堆中的对象,回收未被标记的对象。下面将对这个算法进行形象化的展示:

标记前:

标记前

标记后:

标记后

清除后:

清除后

标记-清除算法是这种算法时垃圾回收中最基础的算法, 后续的算法基本都是根据这种算法的优缺点进行改进的。它同时也是一种间接回收算法,它没有标记需要回收的对象,而是通过标记存活对象后,将未被标记的对象视为回收对象。所以,每次调用回收算法的时候都需要重新重新标记Java堆中的对象。

那么这算法有什么缺点呢?

  1. 效率。在内存中执行不连续的标记和清除两个动作,效率的确不高。
  2. 空间问题。由于需要回收的对象可能不处于连续的空间上,在执行标记清除回收算法后,可能会出现大量的不连续的内存碎片,内存碎片过多,可能导致Java堆为大对象分配内存时无法找到足够大的内存而触发一次不必要的垃圾回收操作,影响线上服务。

复制算法

复制算法是将Java堆中的可用内存划分为两块大小相同的区域,对象首先存放在其中一块内存区域上,当这块区域无法继续分配内存后,就会将这块区域的所有仍然存活的对象复制到另一块内存区域上,然后将已使用过的内存区域一次性清理掉,下面是对复制算法的一个形象化展示:

执行前:

回收前

执行后:

回收后

这种算法的优点是不需要考虑内存碎片的问题。缺点是可用内存缩小为原来的一半,代价有些高,并且在对象存活率比较高的场景,会经常执行复制算法,同样会降低效率。

虽然说代价有些高昂,但是目前市面上的商业虚拟机基本上都是使用复制算法来回收新生代,因为新生代的对象一半都会很快被回收,所以就如我们所知,新生代没有划分为1:1的两部分,而是划分为一块较大的Eden区域和两块较小的Survivor区域,其中一个Survivor区域比较靠近Eden区域,也可称为From Survivor区,另一块也可称为To Survivor区。主要的使用Eden区和靠近Eden区的那个Survivor区,回收的时候,Eden区和Survivor区中仍然存活的对象会一次性的复制到另一个Survivor区中,然后清理掉已使用过的Eden区和Survivor区。HotSpot默认Eden区和Survivor区的大小比例是8:1,你可能会问,剩下的一个Survivor区仅占新生代区域的10%的空间,及时有些数据已经不存活了,但是仍然放不下从Eden区和Survivor区回收的存活对象,这就涉及到另一个概念:分配担保

分配担保可以将因为Survivor空间不够而无法存入Survivor区的对象直接通过分配担保进入老年代。

标记整理算法

标记-整理算法就是将存活的对象都标记出来,然后将这些对象都向一端移动,然后直接清理掉边界以外的内存。下面是对标记-整理算法的形象化展示:

标记前:

标记前

标记后,整理前:

标记后,整理前

整理后:

整理后

标记-整理算法一般使用在回收老年代区域场景中。

分代收集算法

在Java中,堆被分为新生代、老年代。分代收集算法就是根据不同年代的特点采用最合适的垃圾回收算法。在新生代中,每次垃圾回收都只有少量的对象存活,那么复制算法最适合新生代的垃圾回收。而老年代中一般存储存活率较高的或者是比较大的对象,而且也没有额外的空间对老年代进行分配担保,所以标记清除标记整理 这两种算法比较适合老年代的垃圾回收。

垃圾收集器

垃圾收集器是内存回收的具体实现,下面将对比较流行的垃圾收集器进行介绍。

Serial收集器

Serial收集器是最基本,历史最悠久的是垃圾收集器,在JDK1.3.1之前曾是虚拟机回收新生代内存的唯一选择。这是一个单线程收集器,它在进行垃圾回收时,必须暂停其他所有的工作线程,直到内存回收结束。

Serial收集器在新生代采用的是复制算法,在老年代则采取标记清除算法,GC线程在工作时会在用户不可见的情况下把用户正常工作的线程全部停掉。这可能对程序的运行和问题排查造成一定的影响。

虽然说Serial是一款比较霸道的收集器,但是在目前的生产环境中,也并非没有应用场景。相对于其他的收集器的单线程,Serial收集器比较简单而且高效,所以如果生产环境是单个CPU,Serial收集器没有切换线程的损耗,只专注于垃圾回收,所以它的回收效率是比较高的。

ParNew收集器

ParNew收集器是Serial收集器的改良版,也就是Serial收集器的多线程版本,其中控制参数,收集算法,Stop The World,对象分配规则,回收策略都与Serial收集器完全一样。

ParNew收集器是目前许多运行在Server模式下的虚拟机中首选的新生代垃圾收集器。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的性能,甚至由于线程切换的性能开销,该收集器在通过超线程技术实现的两个CPU环境中都不能百分百的超越Serial收集器。但是当使用CPU的数量增加时,它对于垃圾回收时系统资源的有效利用还是很有好处的。它默认开启和CPU数量相同的收集线程数,可使用–XX:ParallelGCThreads参数来限制垃圾收集的线程数。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个并行收集器,所谓并行指的是多条垃圾收集线程并行工作,但是用户线程仍然处于等待状态。它采用的是复制算法,是一个新生代的垃圾收集器。

Parallel Scavenge收集器从介绍来看很像ParNew收集器,但是它的关注点和其他收集器不一样,其他收集器的目的是缩短用户线程等待垃圾回收的时间,而Parallel Scanvenge收集器的目的是达到一个可控制的吞吐量(Throughput),吞吐量指的是CPU用于运行用户代码的时间与CPU总消耗时间的比值。停顿时间越短就越适合需要与用户交互的程序,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,所以Parallel Scavenge收集器适合在后台运算但不需要太多交互的任务。

Parallel Scavenge收集器通过-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间,通过-XX:GCTimeRatio参数控制吞吐量的大小,通过-XX:+UseAdaptiveSizePolicy参数控制系统是否可以自动调节新生代、老年代等细节参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器尽可能的保证内存回收所花费的时间不超过指定值,但是这个指定的值不是越小越好,垃圾回收停顿时间的缩短是以牺牲吞吐量和新生代空间为代价的。系统把新生代调小,回收300M新生代的空间要比回收500M新生代的空间要快,但是垃圾回收的频率将增多,原来10秒回收一次,每次停顿100毫秒,现在每5s回收一次,每次停顿60毫秒,频繁的GC对于用户交互的应用,尤其是高并发的接口,是性能上的损失。

GCTimeRatio参数一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,如果把此参数设为1,那么允许的最大GC时间就占总时间的50%(1/(1+1))。该参数的默认值是99,也就是说,默认允许最大1%的垃圾回收时间。

+UseAdaptiveSizePolicy是一个开关参数,当这个开关打开之后,就不需要指定新生代大小(-Xmn)、Eden区与Survivor区的比例关系(-XX:SurvivorRatio)等细节参数了,虚拟机会根据系统运行的情况动态调整这些参数,使系统达到最优的吞吐量,这种调节方式称为GC自适应调节策略,这个策略也是Parallel Scavenge收集器与ParNew收集器的一个重要的区别。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它是一个单线程收集器,使用标记-整理算法,它主要用于client模式下的虚拟机。但是在Server模式下的虚拟机,它主要有以下两种用途:

  1. 在JDK1.5及以前的版本与Parallel Scavenge收集器一起搭配使用。
  2. 作为CMS收集器的后备,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,始于JDK1.6版本,和Parallel Scavenge一样是一个多线程收集器,使用标记-整理算法。在JDK1.6以前,如果新生代使用Parallel Scavenge收集器,那么老年代将只能使用Serial Old收集器,由于Serial Old收集器是一个单线程的收集器,回收老年代的时候效率不高,Parallel Scavenge的优势将不会那么明显,即未必能获得系统吞吐量最大化的效果。所以,Parallel Old收集器应运而生,和Parallel Scavenge组合在一起,可以更好的实现吞吐量最大化。在注重吞吐量和CPU资源敏感的场合,可以优先考虑Parallel Scavenge与Parallel Old的组合。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一款划时代收集器,基于标记-清除算法,是一种以获取最短回收停顿时间为目的的垃圾回收器,通过英文命名就可以看出它是一个款并发收集器,所谓并发,指的是用户线程和回收线程同时执行,用户线程在运行时,回收线程运行在其他的CPU上。

CMS收集器的标记-清除算法要比其他使用这种算法的收集器略微复杂一点:

  1. 初始标记(CMS initial mark)。初始标记需要Stop the World操作,它仅仅是标记下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记(CMS concurrent mark)。并发标记是进行GC Roots Tracing的过程。
  3. 重新标记(CMS remark)。重新标记是为了修正并发标记期间程序继续运行,对标记产生变动的部分重新标记记录。这个阶段要比初始标记时间稍长,但是远比并发标记时间短。
  4. 并发清除(CMS concurrent sweep)。并发清除指的是以并发执行的方式,回收Java堆中不需要对象。

虽说CMS收集器实现了并发收集,降低了停顿,但是它仍然有如下缺点:

  1. CMS收集器对CPU资源非常敏感。CMS收集器默认启动时的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃回收线程占有不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU个数不足4个时。CMS收集器对程序的影响可能就会变得很大,如果本来CPU的负载就比较大,还分出一半的运算能力去执行收集线程,就很可能导致程序运行效率突然猛降。
  2. CMS收集器无法处理浮动垃圾。由于CMS并发清理阶段用户线程仍然运行,会继续有垃圾对象产生,当这一部分对象出现在标记过程之后,CMS收集器在当前回收过程中就无法回收它们,这部分未被回收的对象又称为浮动垃圾。因此,CMS收集器不像其他收集器一样,等到老年代几乎完全被填满了再进行收集,而是要预留出一部分空间提供给并发收集时程序运行使用。在JDK1.5的默认设置中,当老年代使用了68%的空间后,CMS收集器就会触发回收操作。在JDK1.6与JDK1.7中,这个值已经被提升至92%,下图是CMS收集器触发老年代收集动作阀值的源码。

如果老年代增长不是特别快,可以适当使用-XX:CMSInitiatingOccupancyFraction参数来提高触发百分比,以降低回收次数,提供程序运行性能。要是在CMS收集器运行期间,程序运行没有足够的内存,就会出现一次前文提到的Concurrent Mode Failure,这是就会临时采用Serial Old收集器,重新进行老年代的垃圾收集。

  1. 标记-清除算法所带来的弊端。标记-清除算法会在运行结束后,产生大量的内存碎片。当空间碎片过多时,会对大对象的内存分配造成阻碍,往往在老年代还有很大的空间时,大对象找不到分配空间从而引起一次不必要的Full GC,影响程序的性能。为此,CMS收集器提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器Full GC时,开启内存碎片合并整理,但是会增加Full GC的停顿时间。CMS收集器还提供了-XX:CMSFullGCsBeforeCompaction参数(默认值为0,表示每次Full GC都会进行内存碎片整理)来设置执行多少次不合并整理内存碎片的Full GC后,跟着来一次带合并整理内存碎片的Full GC。

G1收集器

G1收集器是JDK1.7的新特性之一,被视为JDK1.7Hotspot虚拟机的一个重要进化特征。G1收集器是一款面向服务端应用的垃圾收集器,未来的计划是替代CMS收集器。

相比于其他的垃圾收集器,G1收集器有如下特点:

  1. 并行与并发。G1收集器能充分利用多CPU,多核环境下的硬件优势,缩短Stop The World的时间。
  2. 分代收集
  3. 空间整合。G1收集器从整体上来看是使用的是标记整理算法,而从局部来看,使用的是标记-清除算法。
  4. 可预测的停顿。除了追求低停顿以外,还能建立可预测的停顿时间模型,让使用者明确指定在一个时间段内,消耗在垃圾收集上的时间不超过N毫秒。

既然G1收集器未来将与取缔CMS收集器,那么它与CMS收集器相比,具有以下优点:

  1. G1收集器在压缩空间发面有优势。
  2. G1收集器通过将内存空间分成区域(Region)的方式避免内存碎片的问题。它将整个Java堆都划分多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但是新生代和老年代的不再是物理隔离,而是一部分不需要连续的独立区域的集合。
  3. Eden、Survivor和Old区域不再固定,在内存使用效率上来说更灵活。
  4. 可以设置可预测的停顿,避免过长或过多的Full GC影响程序性能。它会跟踪每个Region区域的垃圾堆积的价值大小,并在后台维护一个优先列表,每次根据允许的时间,先回收价值最大的Region。这种使用Region划分内存空间以及优先回收的策略,保证了G1收集器在有限的时间内可以获取最高的收集效率。
  5. G1收集器虽然有分代收集的概念,但是G1收集器可以不需要其他收集器的配合就能独自管理整个Java堆。

但是就目前而言,回收老年代,CMS收集器仍然是首选,可能在一下场景下更适合G1收集器。

  1. 多核CPU。
  2. 经常有大量的内存碎片产生。
  3. 垃圾回收时间可控。

在G1收集器中,虚拟机是通过Remembered Set控制Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用来避免全堆扫描的。G1收集器中每个Region都会对应一个Remembered Set,虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Remembered Set中,当进行内存回收时,在GC根节点的枚举范围内加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可分为以下几个步骤:

  1. 初始标记。标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发执行时,能在正确的Region区域内创建对象。这一阶段需要停顿线程,但是时间很短。
  2. 并发标记。从GC Roots开始对堆中的对象进行可达性分析,找出存活的对象。这段时间消耗较长,可与用户线程并发执行。
  3. 最终标记。修正在并发标记过程中标记变动的一部分。虚拟机将这段时间对象的变化记录在Remembered Set Logs中,然后合并到Remembered Set中。最终标记阶段需要停顿线程,但是可以并行执行。
  4. 筛选回收。首先根据各个Region区域的回收价值进行排序,然后根据用户制定的计划进行垃圾回收。

注意:如果正在使用CMS收集器或者Parallel Old收集器作为老年代收集器,并且Full GC停顿的时间并不长,那么继续使用当前的垃圾收集器是一个明智的选择。升级JDK的版本后,并不一定要切换G1收集器。

赞(1) 投币

相关推荐

  • 暂无文章

评论 抢沙发

慕勋的实验室慕勋的研究院

码字不容易,路过请投币

支付宝扫一扫

微信扫一扫