三:GC回收机制

GC是如何去判断对象是否能被回收的

     jvm中有个垃圾回收线程,它是低优先级的,当虚拟机空闲或堆内存不足时,它就会去清除不可达对象。

  早期GC判断对象是否能被回收时用的引用计数法,后来改进成了可达性分析法。

  引用计数法:比如A引用了B它就会记录下一个1 ,同理B也去引用A。引用计数法只有在为0的时候才回收对象,像这种相互引用,循环引用”的对象其中一个没回收都是回收不了的。

  可达性分析法:通过GCRoot去引用该对象,若不为空则不回收,为空则回收。

  gcroot对象:是指线程在运行状态中不允许被回收的对象。主要包括:

    1.Java虚拟机栈(局部变量表 也就是方法中参数和方法局部变量)引用的对象。
    2.方法区中(类信息)静态引用(也就是当前类静态引用)的对象。(普通类变量不是哦)
    3.处于存活状态的线程对象(注意是线程对象 而不是线程的对象)。
    4.本地native方法jni引用的对象。

 

 

对象在jvm中的状态

Java对象在内存中有三种状态:

   可   达:   对象创建之后,能被变量引用就是可达的。  

   可恢复   对象没有引用指向时就成了可恢复状态,回收该对象之前调用finalize()进行清理,若在finalize()方法中重新被引用就会变成可达状态,反之就成为不可达状态。

   不可达:   不被引用且finalize清理时也不能重新被引用就成了不可达状态。

强引用、软引用、弱引用、虚引用

  强引用、软引用、弱引用、虚引用以及他们之间和gc的关系 

  强引用:指向通过new得到的内存空间的引用叫强引用,只要强引用还在,它宁愿 OOM 内存溢出也不会回收。BigObject obj = new BigObject(); 就是个强引用。

  

  软引用:通过SoftReference类将对象给包裹起来,内存快溢出时会回收。此时 objb变量 对 BigObject对象 的引用就是软引用了,一般情况下不会回收这个对象,但是内存快溢出时会回收的。哪怕他被变量引用了,但是因为他是软引用,所以还是要回收。(此时程序一直在运行中,但是一直没有溢出)。

  

  弱引用:通过weakReference类来实现,不管内存是否充足都会回收。其他方面和软引用一样。

  

  虚引用:虚引用通过PhantomRefence类实现,如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

  

* 其实这里比较常用的,就是强引用和软引用,强引用就是代表绝对不能回收的对象,软引用就是说有的对象可有可无,如果内存实在不 够了,可以回收他。

年轻代和老年代的GC回收算法

  引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;计数器值为0的对象就是不再被使用的,垃圾收集器将回收该对象,若频繁被使用就将对象放到老年代中。

    特点:执行速度比较快。但是"互相引用,循环引用"的对象却会都无法回收。(正是由于这个缺陷,这个算法不再被使用)

  复制回收算法:(用在新生代中);新生代中分为 eden  s0  s1 三个区域,默认比例为8:1:1。我们创建对象时,对象存在eden区,当eden区块满时,就会触发GC将经常使用的对象放到s0区,如此反复当s0快满的时候,就将经常使用且存活的对象放入s1区,将s0中不可达的对象清理掉,下次eden区再有对象要移出来的时候就直接放到s1区。而s1快满的时候就会将常用且存活的对象放到s0区,其余的对象全部清理掉。当新生代的空间耗尽时,就会把经常使用的对象转移到老年代中。或者新生代放不下某个对象时就直接放到老年代中。

    特点:保证数据最大限度的停留在新生代中,新生代内存空间利用率达到90%。

  标记-清除算法:根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。

    特点:标记与清除没有连续性效率低下,清除之后内存会产生大量碎片(由于这个缺陷,所以也不怎么用)。

  标记-压缩算法:(用在老年代中);就是基于标记清除算法之上做的优化,把存活的对象压缩到内存一端,而后进行垃圾清理。不会造成内存碎片化!

垃圾收集器有哪些?

Serial

  Serial和Serial Old垃圾回收器(串行收集器)分别用来回收新生代和老年代的垃圾对象。工作原理就是单线程的串行收集器,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然后让他们垃圾回收,这个现在一般写后台Java系统几乎不用。

ParNew

  ParNew是新生代的垃圾回收器,它是多线程并发的。充分的利用了CPU资源。因为在进行垃圾回收的时候,是没法继续创建对象的,所以大大的提升了回收速度,缩短了“Stop the World”时间。

  

  我们只要设置“-XX:+UseParNewGC”,那么新生代就是用ParNew回收器了,它默认线程数和cpu核数是相同的。我们也可以指定“-XX:ParallelGCThreads”来手动指定垃圾回收线程数量。

CMS 

  CMS是用在老年代的垃圾回收器,也是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。CMS收集器是基于“标记-清除”算法实现的,总共有5个步骤。

  (1)初始标记(gc直达):标记出 GC Roots 能直接关联到的对象(这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态,但是其实影响不大,因为他的速度很快)

  

   (2)并发标记(顺藤摸瓜):从GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行

  

  (3)重新标记:因为第2步的并行生成了对象和垃圾,所以要修正并发标记期间因用户程序导致标记产生变动的标记记录,程序再次进入“Stop the World”状态

  

  (4)并发清除:清除之前标记的对象

  

  (5)整理内存:由于“标记-清除”算法的缺陷性,我们从上图就可以看到内存是很零散的,这样没法存大对象、造成内存浪费。CMS默认开启了“-XX:+UseCMSCompactAtFullCollection”参数,他意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。(他还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。)

  

G1

  G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,采用了更加优秀的"标记-压缩"算法来实现的。主要的优点是:预判回收停顿时间,优先选择停顿时间最短以及回收对象最多的Region回收。
  G1是将堆内存分成了多个Region,这个默认情况下自动计算和设置的,最多允许分成2048个,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类的。以使用“-XX:+UseG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048。比如说堆大小是4G,那么就是4096MB,此时除以2048个Region,大概每个Region的大小就是2MB。一般保持默认就好了,也可以“-XX:G1HeapRegionSize”手动指定大小。Region可能属于新生代也可能属于老年代(他只是个内存块,现在被拿来装新生代对象,被回收干净后,这个内存块也有可能拿来装老年代对象)。

 

Region

  还是上面那个例子,4g内存分为2048个region,每个region是2M,它的运行过程是这样的:
  1. 一开始会有5%的region分给新生代,差不多就是200mb,也就是100个region(可通过“-XX:G1NewSizePercent”设置占比),默认不最大超过60%,但可以通过“-XX:G1MaxNewSizePercent”设置。默认“-XX:SurvivorRatio=8”则eden:s0:s1还是8:1:1。

  2. 进入老年代有两个规则:1.年龄超过15了的对象;2.gc后存活的对象超过了Survivor的50% 。用了G1回收器后,大对象不会放入老年代,而是一个region中。整个堆分为三块:新生代、老年代、和装大对象的region。

G1回收过程

  G1和CMS一样,也是 "初始标记"——"并发标记"——"重新标记"——"并发清除",原理都是一样的,只是不需要"整理内存"了。因为G1的标记压缩其实就是“局部复制回收,整体是标记清理”都是基于region操作的。g1最核心的点就是可以设置垃圾回收的预期停顿时间。追踪每个Region里的回收性价比。比如一个内存块有10mb垃圾回收需要500ms;一个有20mb垃圾回收需要200mb,它会先清理第二个。

  

Minor GC与Full GC的触发时机

其实新生代每次 Minor GC 之前都会去检查一下老年代的剩余可用空间,能否大于新生代所有对象的总和。主要原因是 极端情况下 Minor GC 后新生代所有的对象都存活了下来(是有这种可能的对吧)那就会触发 “空间担保机制”, 它又分两种情况:
1. 如果老年代剩余内存是可以装下本次新生代所有对象的,那么则放心大胆的Minor GC,因为即使Survivor区放不下了,也可以转移到老年代去。
2. 如果装不下呢,就会判断之前每次 Minor GC 后进入老年代的对象平均大小。比入前几次平均有10mb进入老年代,那么估计这次应该也是10mb,如果此时老年代剩余空间大于10mb呢,则认为是可以装下的;如果小于,此时就会直接触发一次“FullGC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC。

 

  新生代回收:Minor GC / Yong GC 新生代(eden/s0/s1)垃圾收集,eden一满就触发和s0 s1 没关系。
  老年代回收:Major GC / Old GC 老年代的垃圾回收
  目前只有CMS有单独收集老年代的行为,在触发 Major GC 之前往往也会触发一次 Minor GC
  混合回收:Mixed GC 只有 G1 才能触发,回收整个新生代和部分老年代
  整堆回收:Full GC 回收整个java堆和方法区,很多时候 Major GC 等价于 Full GC ,只是后者多一个触发时机,那就是元空间不足

  

为什么Full GC比Minor GC慢很多

  一般Full GC要比新生代的Minor GC慢10倍左右,其实我们在上面分析它的执行过程就应该知道了:新生代直接用GC Root追踪就知道哪些对象是获得了,而新生代活对象是很少的,所以速度快;而老年代要追踪到所有的存活对象,而且回收的对象都是零零散散的,所以就慢。

OOM触发时机

posted @ 2018-08-12 18:07  吴磊的  阅读(1138)  评论(0编辑  收藏  举报
//生成目录索引列表