JVM之GC原理解析

1. GC ROOT

  首先要说的还应该是垃圾回收首先要做的一件事情:判断一个对象是否已经GG需要被回收?垃圾回收时是依据这一步判断哪些对象是否需要回收来继续进行的,现在主流的JVM用的基本都是可达性分析算法,即所谓的GC ROOT。该算法的核心思想是通过某些初始化的对象节点(GC-ROOTS)开始,将任意两个有关联的对象之间建立建立连接,最终通过这些初始节点开始向下不断延伸,最终得到类似于一个或多个无向图,而判断某个对象是否能够被垃圾回收的依据就是是否有一个或一个以上的初始节点通过无向图能够抵达该对象节点,如果不可达则能够进行回收,否则不行。
  常用的GC-ROOTS主要来源于堆内存以外的jvm内存,例如jvm栈中引用的对象、方法区中引用的静态变量和对象、本地方法区中引用的对象等。

2. 垃圾回收算法

  在通过GC ROOT算法判断完哪些对象可以被回收以后,接下来就是要准备回收这些对象了,回收需要制定相关的回收规范/策略,这也就是我们说的垃圾回收算法,这里主要介绍标记-清除算法、复制算法、标记-整理算法。

2.1 标记-清除算法

  这个算法思想十分简单,分为标记和清除两个阶段,标记就是通过GC ROOT标记哪些对象可以被回收,标记完成后就进入清除阶段,清除能够被回收的对象,这里在清除的时候只是把对象占据的内存空间给释放出来了,并没有做其它事情,而jvm内存空间是一块连续着的区域,这样经历了若干次回收之后,会导致存在较多的内存碎片,导致内存利用率不行。

2.2 复制算法

复制算法就是我们常用的几个垃圾回收器年轻代回收算法的延伸,最初的复制算法是把内存分为两块一样大小的区域,每次只用其中一半存储对象,然后其中正在使用的这一半需要GC的时候,在清除的时候,直接把所有存活的对象复制到另外空闲的那边,然后需要回收的这块内存就可以安心做全部清理了,这样实现起来较为简单,也不会出现内存碎片问题,不过最初的这种思想会导致内存中始终是有一半是处于空闲的,利用率太低了。
  所以针对这一问题,经过相关牛B公司的分析,发现绝大多数对象都是短命鬼,所以每次垃圾回收后的幸存者是很少的,所以如果每次都为了这少数的幸存者而腾出一半的空间未免过于浪费。因此,就有了压缩空闲内存部分的占比,并且在具体垃圾回收器中优化为内存分为三部分,分别就是我们常说的eden、s0、s1三部分,默认比例是8:1:1,分为三部分的作用是,每次jvm申请对象只会使用eden中内存,在eden分区满了以后,会触发垃圾回收,然后会将对象从eden和s0、s1中在使用的那一部分中对象一起进行回收,复制到另外一部分survivor分区中,这样其实最后空闲的内存就只有10%了,对空间的利用率就高了很多,这就是在复制算法思想基础上进化而来的 复制EX算法(个人命名!!)。

2.3 标记-整理算法

  复制EX算法在绝大多数情况下表现很好,但是记住核心的一点这个算法的需要绝大多数对象是短命鬼的情况下,如果出现极端情况,某些对象的生命力那叫一个顽强啊,而且还是成群结队的来到了我们的内存中,导致每次存活的对象特别多,如果这个时候还用复制EX算法,会导致每次需要copy的对象非常多,效率会有明显影响。这一点尤其是在老年代中,由于老年代都是一些久经沙场活下来的战士,生命力普遍顽强不是年轻代里的渣渣们能比的,所以如果在老年代里需要进行垃圾回收,复制EX直接GG!
  针对这种情况,我们就需要继续回到最初的标记-清除算法祖宗这里取经,得到的启示是标记-清除算法的EX版本 — 标记-整理算法!!!这个算法的标记阶段和老祖宗一样,但是对清除过程的内存碎片不足进行了改良,改良的办法是,当我标记完后,我不马上直接进行清除释放内存,而是先把幸存者们依次排排站,向内存空间中的一端一起移动,大家聚拢起来在末日抱团取暖,这样就使得所有幸存者占据的地盘(内存)是连续起来的了,然后对于幸存者占据的营地以外的地方,直接全部进行人道毁灭,杀死所有丧尸(不可达对象们),这样完毕后,就不存在内存碎片一说了。
  以上就是回收算法的介绍,接下来就是针对这些算法,来调教调教实际的那些个垃圾回收器们了。

3. 垃圾回收器们

针对以上各种理论算法,其实各有各的优缺点,由于实际应用的场景和需求的复杂性,造物主们创造了一个个针对不同情况下的垃圾回收器们,它们主要有:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1等等。造物主太能创造了,物种过于多,我们这里主要挑几个常用到的(或者说我遇到的)来介绍。

Serial/Serial Old收集器

  Serial收集器属于设计给年轻代处理战五渣们用的收集器,它内部用到的是复制EX算法来进行垃圾回收,它属于一个串行工作的收集器,即在进行垃圾回收时,用户的工作线程必须停止,而且它在清理时用的是单CPU单线程进行清理,从而导致它缺点是STOP THE WORLD(清理时停止用户线程)的时间太长,现在的计算机大多数都是多核多CPU的,所以用Serial的地方不多,但是书上说它在Client模式下作为垃圾回收器是很好的选择,这一点我暂未遇到,估计也不会遇到,忽略之。
  和Serial对应的,Serial Old则是为了专门为老年代的战士们设计的,它和Serial区别在于它清理内存时的回收算法用的是标记-整理算法,从而有效避免内存碎片过多而导致如果有大的对象来到老年代,由于无法分配到足够大的连续空间而提前触发FULL GC。

ParNew收集器

  这个家伙就是Serial收集器的多线程版本,即在进行垃圾回收的时候它用的是多线程,用在年轻代中,所以会比单线程的Serial快一些,从而对用户的STOP THE WORLD耗时少一些。当然使用它的一个额外也是十分重要的原因是,只有它能够和CMS收集器配合使用,分别处理年轻代和老年代的垃圾回收。

CMS收集器

  这个家伙可老牛逼了,造物主们创造它的意图就是尽量减少STOP THE WORLD的时间,它用到的垃圾回收算法是标记-清除算法,运行过程的整个流程分为四个阶段:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

  这四个步骤中只有初始标记和重新标记会导致STOP THE WORLD,不过初始标记只是 标记直接和GC Roots有关联的对象们,所以这一步的速度和光一样快。初始标记完成后,依据标记结果,启动多个线程来标记后续的不是直接和GC Roots们直连的对象们,但是这个过程用户线程也会正常运行,所以有一定可能性会出现前一刻标记为不可达的对象,下一秒由于用户线程中某个对象和刚标记的那个对象建立了关系,导致会存在部分对象标记有误。所以,完成并发标记后,还不能够进行垃圾回收,不然会错杀无辜。对此,就有了第三步的触发STOP THE WORLD让用户线程干瞪眼的重新标记阶段,这个阶段用户线程停止运行,收集器启动多个线程再次进行一次标记,这个过程虽然会比初始标记慢,但是比不进行步骤1、2直接停止用户线程进行标记来说,要快得多。最后,标记完成后就是进行最终的垃圾回收了,这个过程用户线程是可以一起进行的。
  针对CMS,由于为了尽量减少STOP THE WORLD耗时,使用的是标记-清除算法,那么很显然会导致内存碎片问题,进行了若干次CMS垃圾收集后,可能会导致内存碎片较多,无法给大的对象分配空间,导致频繁出发FULL GC,对此,虚拟机中有一个参数-XX:+UseCMSCompactAtFullCollection是用来配置在FULL GC触发时,是否开启内存合并整理,即把存活的对象集中到一块连续的内存中。这个开关默认是开启的,即默认情况下发生一次FULL GC时,是会进行内存整理的,而且要注意内存整理过程中也是会停止用户线程的,当然这种情况虽然碎片问题没了但是时间自然会消耗更多。对此,一个JVM提供了另外一个参数-XX:CMSFullGCsBeforeCompaction控制执行了多少次FULL GC后进行内存整理,默认是0,即每次FULL GC都会进行碎片整理,这两个参数书中说的,本人暂时未使用过。
  另外一点,由于并发清除阶段,用户线程会继续运行,所以这个过程中可能会产生新的对象之类的,对此在进行垃圾回收的时候,还需要腾出一部分的内存给这些线程来进行内存分配之类的,所以一般在老年代内存还未占满之前就会提前触发GC,1.6以后的版本这个阀值是92%,即老年代内存使用达到92%时会触发FULL GC。尤其是,如果老年代预留的空间不够,那么会导致对象无法进入老年代,那么会导致CMS垃圾回收器失效,这个时候就会启动备用方案用Serial Old来进行老年代的垃圾回收。

以上就是常用的垃圾回收器,那么了解了这下垃圾回收器后,我们还需要会看GC日志啊,不然都是白搭,这里用CMS的日志来做个例子说明:

1193.683: [GC [1 CMS-initial-mark: 2674952K(3268608K)]   4260378K(7876608K), 0.8219810 secs] [Times: user=0.80 sys=0.02, real=0.82 secs] 
 时间           当前阶段名称   当前老年代大小(老年代总大小) 当前已有堆大小(堆总大小) 耗时
1194.505: [CMS-concurrent-mark-start]
1194.607: [CMS-concurrent-mark: 0.102/0.102 secs] [Times: user=0.61 sys=0.00, real=0.11 secs] 
1194.607: [CMS-concurrent-preclean-start]
1194.616: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
1194.616: [CMS-concurrent-abortable-preclean-start]
 CMS: abort preclean due to time 1199.770: [CMS-concurrent-abortable-preclean: 4.553/5.153 secs] [Times: user=8.79 sys=0.20, real=5.16 secs] 
1199.770: [GC[YG occupancy: 3590133 K (4608000 K)]1199.770: [Rescan (parallel) , 2.6855350 secs]1202.456: [weak refs processing, 0.0000340 secs]1202.456: [scrub string table, 0.0006550 secs] [1 CMS-remark: 2674952K(3268608K)] 6265085K(7876608K), 2.6863330 secs] [Times: user=42.25 sys=0.90, real=2.68 secs] 
1202.456: [CMS-concurrent-sweep-start]
1202.590: [CMS-concurrent-sweep: 0.132/0.133 secs] [Times: user=0.14 sys=0.01, real=0.14 secs] 
1202.590: [CMS-concurrent-reset-start]
1202.599: [CMS-concurrent-reset: 0.009/0.009 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

首先每一行最前面的数字代表的是JVM启动开始至当前这一步骤的时间,单位是秒,主要是处理前的:

1193.683: [GC [1 CMS-initial-mark: 2674952K(3268608K)]   4260378K(7876608K), 0.8219810 secs] [Times: user=0.80 sys=0.02, real=0.82 secs] 
 时间           当前阶段名称   当前老年代大小(老年代总大小) 当前已有堆大小(堆总大小) 耗时

垃圾回收后的:

1199.770: [GC[YG occupancy: 3590133 K (4608000 K)]1199.770: [Rescan (parallel) , 2.6855350 secs]1202.456: [weak refs processing, 0.0000340 secs]1202.456: [scrub string table, 0.0006550 secs] [1 CMS-remark: 2674952K(3268608K)] 6265085K(7876608K), 2.6863330 secs] [Times: user=42.25 sys=0.90, real=2.68 secs] 

其中:

[YG occupancy: 3590133 K (4608000 K)] 清理前表示当前年轻代大小(总年轻代大小);
Rescan (parallel)  重新扫描,步骤三的重新标记。
[1 CMS-remark: 2674952K(3268608K)]  清理完毕后,老年代占大小(老年代总大小)
6265085K(7876608K)  清理完毕后,当前堆中对象大小(堆总大小)

  当然,由于是第三步得到的结果,这里应该是依据统计了会被回收的总大小和当前堆内所有对象总大小后,计算得到的结果,真正执行时,由于用户线程还会一起执行,这个过程中会产生浮动垃圾,所以真正执行完毕那一刻的堆大小是不确定的,只能依据执行前的数据进行计算。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值