【JVM基础】深入了解Java虚拟机(五)

垃圾回收算法

一、标记-清除算法

在这里插入图片描述

  • 这种算法分为两部份:标记、清除两个阶段, 标记阶段是从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态,清除阶段在扫描完成之后将没有标记的对象给清除掉。
  • 这种算法在垃圾收集器进行GC时,必须停止所有Java执行线程(也称“Stop The World”),原因是在标记阶段进行可达性分析时,不可以出现分析过程中对象引用关系还在不断变化的情况,否则的话可达性分析结果的准确性就无法得到保证。在等待标记清除结束后,应用线程才会恢复允许。
  • 缺点:
    • 效率问题:
      标记和清除两个阶段的效率都不高,因为这两个阶段都需要遍历内存中的对象,很多时候内存中的对象实例数量是非常庞大的,这无疑很耗费时间,而且GC时需要停止应用程序,这会导致非常差的用户体验。
    • 空间问题:
      标记清除之后会产生大量不连续的内存碎片,内存空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
二、复制算法

复制算法是将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块内存上,然后把这一块内存中所有的对象一次性清理掉。

在这里插入图片描述

  • 复制算法每次都是对整个半区进行内存回收,这样就减少了标记对象遍历的时间,在清除使用区域对象时,不用进行遍历,直接情况整个区域内存,而且在将存活对象复制到保留区域时也是按地址顺序存储的,这样就解决了内存碎片的问题,在分配对象内存时不用考虑内存碎片等复杂问题,只需要按顺序分配内存即可。
  • 复制算法简单高效,优化了标记-清除算法的效率低、内存碎片多等问题,但带来了其本身的缺点:
    • 1、将内存缩小为原来的一半,浪费了一半的内存空间,代价太高。
    • 2、如果对象的存活率很高,极端一点的情况假设对象的存活率为100%,那么我们需要将所有存活的对象复制一遍,耗费的时间代价也是不可忽视的。
三、标记-整理算法

标记-整理算法与标记-清除算法很像,事实上,标记-整理算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。

在这里插入图片描述

  • 可以看到,回收后可回收对象被清理掉了,存活的对象按规则排列存放在内存中。这样一来,当我们给新对象分配内存时,JVM只需要持有内存的起始地址即可。标记-整理算法弥补了标记-清除算法存在内存碎片的问题,消除了复制算法内存减半的高额代价,可谓一举两得。
  • 标记-整理算法的缺点:
    • 效率不高:不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。
四、分代回收算法

分代收集算法的思想是按对象的存活周期不同将内存划分为几块,一般是把Java堆分为新生代和老年代(还有一个永久代,是HotSpot特有的实现,其他的虚拟机实现没有这一概念,永久代的收集效果很差,一般很少对永久代进行垃圾回收),这样就可以根据各个年代的特点采用最合适的收集算法。

特点:

  • 新生代:朝生夕灭,存活时间很短。采用复制算法来收集。
  • 老年代:经过多次Minor GC而存活下来,存活周期长。采用标记-清除算法或者标记-整理算法来收集。

新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象的复制成本就可以完成收集。

老年代中对象的存活率高,不适合采用复制算法,而且如果老年代采用复制算法,它是没有额外的空间进行分配担保的,因此必须使用标记-清除算法或者标记-整理算法来进行回收。

在这里插入图片描述

新生代中的对象几乎都是“朝生夕灭”的(达到98%),现在的商业虚拟机都采用复制算法来回收新生代。由于新生代的存活率低,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的From Survivor空间、To Survivor空间,三者的比例为8:1:1.每次使用Eden和From Survivor区域,To Survivor作为保留空间。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的。GC进行时,Eden区中所有存活对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1)的对象会被移到老年代中,没有达到阈值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代存活的对象都在To Survivor区。接着From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区。总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。此时复制算法就不合适了。

分代回收:

我们从一个object1来说明其在分代垃圾回收算法中的回收轨迹。

  • 1、object1新建,出生于新生代的Eden区域。

在这里插入图片描述

  • 2、Minor GC,object1还存活,移动到From Survivor空间,此时还在新生代。

在这里插入图片描述

  • 3、Minor GC,object1仍然存活,此时会通过复制算法,将object1移动到To Survivor区域,此时object1的年龄age+1。

在这里插入图片描述

  • 4、Minor GC,object1仍然存活,此时survivor中和object1同龄的对象并没有达到survivor的一半,所以此时通过复制算法,将From Survivor和To Survivor区域进行互换,存活的对象被移动到了To Survivor。

在这里插入图片描述

  • 5、Minor GC,object1仍然存活,此时survivor中和object1同龄的对象已经达到survivor的一半以上(To Survivor区域已经满了),object1被移动到了老年代区域。

在这里插入图片描述

  • 6、object1存活一段时间后,发现此时object1不可达GCROOTS,而且此时老年代空间比率已经超过了阈值,触发了Maior GC(也可以认为是Full GC,但具体需要垃圾收集器来联系),此时object1被回收了。Full GC会触发Stop The World。

在这里插入图片描述

在以上的新生代中,我们有提到对象的age,对象存活于survivor状态下,不会立即晋升为老年代对象,以避免给老年代造成过大的影响,它们必须要满足以下条件才可以晋升:

  • 1、Minor GC之后,存活于survivor区域的对象的age会+1,当超过(默认)15的时候,转移到老年代。
  • 2、动态对象,如果survivor空间中相同年龄的所有的对象大小的综合和大于survivor空间的一半时,年纪大于或等于该年纪的对象就可以直接进入老年代。

内存的分配和回收

分配策略:
  • 对象优先分配在Eden区。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。
  • 动态对象的年龄判定(年龄超过阈值或survivor空间相同年龄所有对象大小总和大于survivor区的一半,年龄大于或等于该年龄的对象直接进入老年代)。

堆分了Eden、两个Survivor、Tenured共四个区域,Eden与Survivor的大小比是8:1,Eden和Survivor称为新生代,Tenured称为老年代(JDK8已经没有了持久代)。

当新对象产生时,存放在Eden区,当Eden区存放不下时会触发Minor GC,将Eden中存活的对象复制到一个Survivor区中,然后继续存放对象到Eden区,当Eden区放不下时触发Minor GC,将Eden区和非空闲的Survivor区中存活的对象复制到空闲的Survivor区中,往复操作。

每经过一次Minor GC,对象的年龄加1,当对象的年龄达到阈值(默认15)进入Tenured区。如果在Minor GC期间发现存活的对象无法进入空闲的Survivor区,则会通过空间分配担保机制使对象提前进入Tenured区。如果在Survivor空间中的相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到指定的阈值。

空间分配担保机制

在执行Minor GC前,JVM会首先检查Tenured区是否有足够的空间存放新生代尚存活的对象,由于新生代使用复制收集算法,为了提升内存利用率,只使用了其中一个Survivor区作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor区无法容纳的对象直接进入老年代,但前提是老年代需要有足够的空间容纳这些存活对象。但存活对象的大小在实际完成GC前是无法明确知道的,因此Minor GC前,JVM会首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小,如果条件成立,则进行Minor GC,否则进行Full GC(让老年代腾出更多的空间)。然而取历次晋升的对象的平均大小也是有一定风险的,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure,老年代也无法存放这些对象了),此时就只好在失败后重新发起一次Full GC(让老年代腾出更多的空间)。

垃圾收集器

我们知道,JVM堆内存分为新生代和老年代,新生代采用复制算法,老年代采用标记-清除或者标记-整理算法来收集和清理垃圾,关于算法的具体实现便是接下来要了解的垃圾收集器。

收集器的发展历程:Serial收集器 -> Parallel收集器 -> CMS收集器(Concurrent Mark Sweep) -> G1收集器(Garbage First)。

在进行垃圾回收时,会暂停所有的工作线程,直到垃圾回收完成,垃圾收集器的不断迭代就是为了优化减少停顿的时间。

在这里插入图片描述

使用垃圾收集器,可以设置垃圾收集器的相关参数:

  • -XX:+UseSerialGC,虚拟机运行在client模式下的默认值,Serial+Serial Old。
  • -XX:+UseParNewGC,ParNew+Serisl Old,在JDK1.8被废弃,在JDK1.7还可以使用。
  • -XX:UseConcMarkSweepGC,ParNew+CMS+Serial Old。
  • -XX:+UseG1GC,G1+G1。

更多相关介绍

一、Serial收集器

Serial收集器是单一线程收集器,运行在client端,是在JDK1.3.1之前唯一的垃圾收集器。

优势:

简单高效,对于单个CPU的环境,Serial收集器由于没有线程交互的开销,专心做垃圾回收,因此可以获得最高的单线程收集效率。

在这里插入图片描述

Serial收集器是最原始的一款垃圾收集器,也称之为串行收集器,顾名思义,它是单线程运行的,而且不止如此,它在收集垃圾的时候,会暂停其他所有的工作线程,直到收集结束,被称之“Stop The World”。想象一下,比如你在看电影,每看五分钟需要暂停几秒钟,这显然是令人难以接受的。

二、ParNew收集器

ParNew收集器是Serial收集器的多线程版本,运行在Server端。

特点:

多线程进行垃圾回收、其余行为(控制参数、收集算法、Stop The World、对象分配规则、回收策略)与Serial收集器完全一致,随着CPU的数量增加,对于GC时系统资源的有效利用还是很有好处的,默认开启的收集线程数与CPU的数量相同。

控制参数:

  • -XX:ParallelGCThreads 参数:限制垃圾收集的线程。

优势:

除了Serial收集器,只有它能与CMS收集器配合使用。

在这里插入图片描述

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程收集垃圾之外,其他行为基本和Serial收集器的实现完全一样。ParNew收集器只有在多核CPU的环境下才能发挥出它的优势(多线程收集速度快,停顿时间缩短),如果是单核CPU它甚至不如Serial收集器的效果好(单核CPU的线程切换导致额外开销)。

三、Parallel Scavenge收集器

Parallel Scavenge与ParNew类似,也是一款并行多线程收集器,相比于ParNew,它的目标则是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),如果虚拟机总共运行了100分钟,垃圾收集花了1分钟,那么吞吐量变为100-1/100=99%。

停顿时间越短越适合与用户进行交互的程序,良好的响应速度可以提升用户体验,而高吞吐量则是可以高效利用cpu,主要用于在后台运算不需要进行用户交互的任务。

Parallel Scavenge提供了两个参数来控制吞吐量:

  • -XX:MaxGCPauseMillis (控制停顿时间(jvm尽量不超过设置的时间),单位ms)
  • -XX:GCTimeRatio (吞吐量大小,大于0小于100)

但你千万不要以为把停顿时间的参数设小,吞吐量参数设大就可以让垃圾收集的速度变快,停顿时间的缩短是靠牺牲吞吐量和新生代空间来换取的:系统把新生代调小,比如由1000兆调节为700兆,收集700兆的空间速度必然比1000兆快,但是相应的收集频率会增高,原来10s收集一次,每次停顿100ms,现在需要5s收集一次,每次停顿70ms(相当于10s停顿140ms),停顿时间确实下降了,但是吞吐量也降了下来。

所以,Parallel Scavenge也被称为“吞吐量优先收集器”,此收集器还有一个参数:-XX:+UseAdaptiveSizePolicy,打开这个参数之后,不需要我们再额外设置新生代的大小以及新生代eden和survivor的比例等参数了,jvm会根据当前系统的运行情况动态调整这些参数以提供最合适的停顿时间和吞吐量,这种调节方式称为GC自适应的调节策略,同时这也是Parallel Scavenge和ParNew的重要区别之一。

四、Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,是一个单线程收集器,使用标记-整理算法,运行在client端。

在这里插入图片描述

另外还可以在Server模式下:JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用,可以作为CMS的后备方案,在CMS发生Concurrent Mode Failure时使用。

五、Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,是一个多线程收集器,使用标记-整理算法,JDK1.6中开始提供使用。

在这里插入图片描述

Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合。

六、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现。

因为它基于标记-清除算法,并发收集、低停顿,运行过程复杂,共分为四步:

  • 初始标记:
    仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”。
  • 并发标记:
    就是进行追踪引用链的过程,可以和用户线程并发执行。
  • 重新标记:
    修正并发标记阶段因用户线程继续运行导致标记发生变化的那部份对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”。
  • 并发清除:
    清除标记为可以回收的对象,可以和用户线程并发执行。

由于整个过程耗时最长的并发标记和并发清除都可以和用户线程同时进行,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

在这里插入图片描述

缺点:

  • 对CPU资源敏感。事实上,面向并发设计的程序对CPU资源都较为敏感,在并发阶段,他虽然不会使用户线程停顿,但是也会因为占用了一部分CPU资源而使应用程序变慢,总吞吐量就会降低。
  • 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”导致另一次Full GC(收集老生代成为Full GC)。由于CMS在并发清理阶段用户线程依然运行着并不断产生垃圾,这部分垃圾出现在重新标记之后,所以在本次GC中无法清理,这部分垃圾就称为浮动垃圾。CMS在垃圾收集的时候用户线程仍在运行,所以他不能向其他收集器一样等到老生代几乎填满再进行回收,需要预留一部分空间供并发时的程序使用,可以通过:-XX:CMSInitIatingOccupancyFaction的参数值来调节触发收集的百分比,一般不需要特意动它。如果预留空间无法满足程序运行的需要,那么就会出现Concurrent Mode Failure,这个时候就轮到Serial Old收集器登场了,JVM会临时使用Serial Old来重新对老年代进行垃圾收集,这同时也就意味着系统停顿时间变长,所以此参数设置过高容易引起大量Concurrent Mode Failure,反而降低性能!
  • 产生大量内存碎片。CMS利用的是标记-清除算法来进行垃圾收集(比标记-整理快),这必然会不可避免的产生内存碎片,内存碎片过多时,就算剩余空间很足,但是无法找到连续的内存空间去分配新来的大对象,就会不得不提前触发Full GC。我们可以通过开启:-XX:UseCMSCompactAtFullCollection参数来解决此问题(默认开启),这样CMS在顶不住要进行Full GC时会对内存碎片进行合并整理,但这也会使得停顿时间变长(内存整理无法并发执行)。通过-XX:CMSFullGCsBeforeCompaction可以设置执行多少次不合并整理的Full GC后,执行一次带合并整理的Full GC,默认为0,即每次进入Full GC时都会进行碎片整理。
七、G1收集器

收集器发展最前沿的成果之一,可以运行与服务端和客户端。

特征:

  • 并行与并发:
    能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。
  • 分代收集:
    G1收集器可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生代和已经存活一段时间的对象。
  • 空间整合:
    整体上看采用标记-整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。
  • 可预测的停顿:
    除了追求低停顿外,还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过M毫秒,这点优于CMS收集器。

为什么能做到可预测的停顿?
是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1收集器将内存分为大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。G1跟踪各个Region区域获得其收集价值大小【注:《深入理解JAVA虚拟机》中这样解释:回收所获得的空间大小以及回收所需要的时间的经验值】,在后台维护一个优先列表;每次根据允许的收集时间,优先回收价值最大的Region区域(名称Garbage-First的由来);这就保证了在有限的时间内可以获取尽可能高的收集效率。

回收过程步骤(与CMS收集器较为相似):

  • 初始标记:
    仅仅标记GC Roots能直接关联到的对象,并修改TAMS(Next Top At Mark Start)的值,让下一阶段用户程序并发运行时能在正确可用的Region区域中创建对象,需要“Stop The World”。
  • 并发标记:
    从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行。
  • 最终标记:
    修正并发标记阶段因用户线程继续运行导致标记发生变化的那部分对象的标记记录。并发标记时虚拟机将对象变化记录在线程Remember Set Logs里面,最终标记阶段将Remember Set Logs整合到Remember Set中,比初始标记时间长但远比并发标记时间短,需要“Stop The World”。
  • 筛选回收:
    首先对各个Region区域的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来定制回收计划,最后按计划回收一些价值高的Region区域中的垃圾对象。回收时采用复制算法,从一个或多个Region区域复制存活对象到堆上的另一个空的Region区域,并且在次=此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值