JVM续篇—最精彩篇—终极篇

目录

1、分代回收:

2、内存的分配和回收

3、空间分配担保机制:

4、垃圾回收器

4.1、Serial收集器

4.2、ParNew收集器

4.3、Parallel Scavenge收集器

4.4、Serial Old收集器

4.5、Parallel Old收集器

4.6、CMS收集器

4.7、G1收集器


1、分代回收:

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

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

1.2、minor GC,object1 还存活,移动到From suvivor空间,此时还在新生代。

1.3、minor GC,object1 仍然存活,此时会通过复制算法,将object1移动到ToSuv区域,此时object1的年龄age+1。

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

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

1.6、object1存活一段时间后,发现此时object1不可达GcRoots,而且此时老年代空间比率已经超过了阈值,触发了majorGC(也可以认为是fullGC,但具体需要垃圾收集器来联系),此时object1被回收了。fullGC会触发 stop the world。

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

1、minor gc 之后,存活于survivor 区域的对象的age会+1,当超过(默认)15的时候,转移到老年代。

2、动态对象,如果survivor空间中相同年龄所有的对象大小的综合和大于survivor空间的一半,年级大于或等于该年纪的对象就可以直接进入老年代。

2、内存的分配和回收

分配策略:

  • 对象优先分配在Eden区

大多数情况,对象直接在Eden区中分配内存,当Eden区内存不足时,就会进行一次MinorGC(新生代垃圾回收,可以通过-XX:+PrintGCDetails这个参数打印GC日志信息)。

  • 大对象直接进入老年代

什么是大对象?虚拟机提供了一个参数:-XX:PretenureSizeThreshold,当对象大小大于该值时,该对象就会直接被分配到老年代中(该参数只对Serial和ParNew垃圾收集器有效)。为什么不分配到新生代中呢?因为在新生代中每一次MinroGC都会导致对象在Eden、from和sruvivor中复制,如果存在很多这样的大对象,那么新生代的GC和复制效率就会极低(关于垃GC的内容后面的文章会详细讲解)。

  • 长期存活的对象将进入老年代

既然对象优先在新生代中分配,那么什么时候会进入到老年代呢?这就和上文讲解的对象头中的分代年龄有关了,默认情况下超过15岁就会进入老年代,可以通过-XX:MaxTenuringThreshold参数进行设置。那岁数又是怎么增长的呢?每当对象熬过一次MiniorGC后年龄都会增加1岁。

  • 动态对象年龄判定(年龄超过阈值或survivor空间相同年龄所有对象大小总和大于survivor区一半,年龄大于或等于该年龄的对象直接进入老年代)

堆分了:Eden、两个Survivor、Tenured共4个区,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空间的一半,年龄大于和等于该年的对象就可以直接进入老年代,无需等到指定的阀值。

3、空间分配担保机制:

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

4、垃圾回收器

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

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

使用垃圾回收器,通过设置垃圾回收器参数

-XX:+UseSerialGC,虚拟机运行在Client模式下的默认值,Serial+Serial Old。

-XX:+UseParNewGC,ParNew+Serial Old,在JDK1.8被废弃,在JDK1.7还可以使用。

-XX:+UseConcMarkSweepGC,ParNew+CMS+Serial Old。

-XX:+UseParallelGC,虚拟机运行在Server模式下的默认值,Parallel Scavenge+Serial Old(PS Mark Sweep)。

-XX:+UseParallelOldGC,Parallel Scavenge+Parallel Old。

-XX:+UseG1GC,G1+G1。

参考资料:jvm系列(三):java GC算法 垃圾收集器 - 纯洁的微笑 - 博客园

4.1、Serial收集器

Serial收集器是单一线程收集器,运行在Client端,在JDK 1.3.1之前唯一的垃圾回收器

Serial收集器是最基本、历史最久的收集器,曾是新生代收集的唯一选择。他是单线程的,只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它在收集的时候,必须暂停其他所有的工作线程,直到它结束,即“Stop the World”。停掉所有的用户线程,对很多应用来说难以接受。

尽管如此,它仍然是虚拟机运行在client模式下的默认新生代收集器:简单而高效(与其他收集器的单个线程相比,因为没有线程切换的开销等)。

优势:

简单高效,对于单个CPU的环境,Serial收集器由于没有线程交互的开销,

专心做垃圾回收可以获得最高的单线程收集效率

4.2、ParNew收集器

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

特点:

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

控制参数:

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

优势:

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

4.3、Parallel Scavenge收集器

并行多线程收集器

新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

特点:

达到一个可控制的吞吐量(其他线程的关注点尽可能的缩短用户线程的停顿时间),吞吐量就是CPU用于运行用户代码的时间与CPU的总消耗时间的比值,吞吐量=运行用户代码时间/( 运行用户代码时间+垃圾收集时间)

区别:

停顿时间越短,交互响应时间越快,用户体验越好

高吞吐量则是高效利用CPU时间,尽快完成程序运算任务,适合后台运算

控制参数:

-XX:MaxGCPauseMillis 控制垃圾回收最大停顿时间

-XX:GCTimeRatio 设置吞吐量大小(大于0小于100)

4.4、Serial Old收集器

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

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

4.5、Parallel Old收集器

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

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

4.6、CMS收集器

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

基于标记-清除算法,并发收集、低停顿,运作过程复杂,分4步:

  • 初始标记:

仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”

  • 并发标记:

就是进行追踪引用链的过程,可以和用户线程并发执行。

  • 重新标记:

修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”

  • 并发清除:

清除标记为可以回收对象,可以和用户线程并发执行

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

缺点:

并发消耗CPU资源

并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。

CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

无法处理浮动垃圾

并发清理阶段,工作线程和垃圾回收线程并发工作的时候,此时工作线程会不断产生新的垃圾,但是垃圾回收线程并不会去处理这些新生成的垃圾对象,需要等到下次垃圾回收的时候才会去处理,这些垃圾对象称之为:浮动垃圾 。因为有这些浮动垃圾的存在,所以老年代不能在100%使用的时候才去进行垃圾回收,否则就放不下这些浮动垃圾了。有一个参数是“-XX:CMSInitiatingOccupancyFraction”,这个参数在jdk1.6里面默认是92%,意思是老年代使用了92%的空间就会执行垃圾回收了。

(在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现"Concurrent Mode Failure"失败。

并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;

产生大量内存碎片:

CMS基于"标记-清除"算法,清除后不进行压缩操作产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

4.7、G1收集器

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

特征:

  • 并行与并发:

能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行。

  • 分代收集:

G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象。

  • 空间整合:

整体上看采用标记整理算法,局部看采用复制算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器。

  • 可预测的停顿:

除了追求低停顿还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。

为什么能做到可预测的停顿?

是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。

G1收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。

G1跟踪各个Region获得其收集价值大小 [注:《深入理解JAVA虚拟机》这样解释:回收所获得得空间大小以及回收所需要的时间的经验值],在后台维护一个优先列表;

每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);这就保证了在有限的时间内可以获取尽可能高的收集效率

对象被其他Region的对象引用了怎么办?

判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出):新生代回收的时候不得不扫描老年代?无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:每个Region都有一个对应的Remembered Set;每次Reference类型数据写操作时,都会产生一个Write Barrier 暂时中断操作;然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象);如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;

进行垃圾收集时,在GC根节点的枚举范围加入 Remembered Set ,就可以保证不进行全局扫描,也不会有遗漏。

回收过程步骤(与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,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量。

各垃圾回收器总结:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值