首页 > 代码库 > java 中的gc问题

java 中的gc问题

前几天服务器出现了一些问题,然后组内同学就这些问题写过一篇文章,下面是顺着那篇文章,接着写一些内容。

一:GC算法的类型

      GC也就是垃圾回收,和我们日常中的垃圾回收一样,语言中的垃圾回收也是表示对已经不再使用的对象进行清理,获得更多的内存空间;日常生活中也是一样的,比如去餐厅吃饭,每个人都要用到碗筷进行回收,一直都用之前的话,肯定是会用完的,那这个碗筷回收的算法时怎么样的呢?对于餐厅来说最好的方式是,在每个人吃完之后,自己将碗筷放到垃圾回收的地方,但是这样有两个问题,一个是餐厅就餐人的素质要比较高,并且比较自觉,另外是针对就餐完后的一些其他剩余(比如吐过的鱼刺等)还是要进行清理。然后餐厅出台一个政策就是吃完之后,不用就餐人自己清理,餐厅来自动清理。餐厅自己清理的策略就有以下几种

     1、引用计数法(reference counting)

          餐厅会记录每幅碗筷的使用情况,比如在使用的时候,会记录为1,如果父亲的筷子,儿子也要用,那么就记录为2,以此类推,如果我用完了,我需要自觉的将这个计数值减一,如果儿子也用完了,也会减一,有一个专门的机器人,会来看这个数字,当看到这个数字为0时,会自动的将这幅碗筷给清理。

          这个算法比较简单,但是问题也比较突出,一是必须有一个专门的地方来记录这个数字,并且要不停的检查这个数字,增加了餐厅的负担,另一个是不能解循环引用的问题,三个对象之间互相引用,此时引用计数都是1,都不会为0,因此永远不会被回收,影响使用效率。

     2、标记-清除算法(mark-sweep)

         这个是第一种实用的垃圾回收算法,J. McCarthy 等人在 1960 年提出并成功地应用于 Lisp 语言的。仍然以餐厅清理碗筷为例,当回收机器人想要回收的时候,会大喝一声,大家停止用餐,机器人开始工作,会走到每一个就餐的人旁边,问:你是吃饭的么?你用的是那副碗筷?用到的碗筷会被做标记,说明正在被使用。餐厅全部问完之后,回收机器人会做第二部,回收掉那些没有做过标记的碗筷。

        从名字就可以看出,标记-清除算法分为两个步骤,首先进行标记,这个过程是暂停运行(stop-the-world)的,然后标记完成后,再进行清除工作,和引用计数不同的是,不需要运行环境监控每一次内存的分配和指针的操作,仅仅是在标记的时候,记录每一个指针对应的引用。并且可以完美的解决循环引用问题,因此在lisp中得到的广泛的引用,后面的很多算法,都有这个算法的影子。

     3、复制算法(copying)

        标记-清除算法虽然比引用计数方法好,但是有一个很严重的问题,就是效率问题,标记和清除两个步骤都是耗时操作。特别是标记操作是在暂停引用的情况询问每一个对象,这个耗时就特别的引人注目。为了解决这个效率的缺陷,M. L. Minsky 于 1963 年发表了著名的论文“一种使用双存储区的 Lisp 语言垃圾收集器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。 M. L. Minsky 在该论文中描述的算法被人们称为复制算法。

       这个算法的特别之处是将内存分为两块,通过简单的复制操作,就能完成垃圾回收工作,不能不说这个想法非常奇妙。以餐厅回收碗筷为例,此时餐厅老板为了提高效率,想了一个高明的办法,将餐厅就餐区域分为了两个相同的区块(先不要考虑浪费问题),同一时刻只能在一个区块就餐,比如A,B两个区块,开始时,大家现在A区块就餐,吃完后的碗筷也会遗留在A区块,在某一个时刻(A区块位置快被占满了),回收机器人大喝一声,大家都统一到B区块就餐,然后就餐的人群就带着自己的东西去了B区块,并且可以要求座位的间距也可以非常紧凑,最重要的是,当大家都到B区块就餐之后,垃圾回收机器人,就会在A区块进行工作了,这个时候,会将所有还残留在A区块的碗筷全部进行回收。然后机器人只要等到B区块快被用完之后,再要求大家去A区块就餐就好了,垃圾机器人的工作就会很简单。

      这个算法大大提高了垃圾回收的效率,并且也将之前复杂的内存分配算法进行了简化,因为既然内存回收是对整个半区进行回收,那么在内存分配的时候,就不用考虑内存碎片的问题,直接顺序分配内存就可以了,因为在进行垃圾收集的时候,可以自动将内存进行对齐的,真是一个神奇,但是这个神奇也是有代价的,就是一整块内存在同一时刻使用的只能是半块,这在当时计算价格还是以kb为单位的时候,不得不说是一个极大的浪费。

       需要指出的是,上面三个算法,在1960年代就被发明了出来,后面的研究者主要是针对三个算法的优缺点进行改进来满足未来的要求

     4、标记-整理算法(mark-compact)

       标记-整理算法是标记-清除算法和复制算法的一个结合,将以上两种算法的优点进行结合,创造一种既不会产生内存碎片,又不会占用太多内存的算法肯定是一种方向。在1970年后,这个算法逐渐清晰

         这个算法的思想很简单,在第一阶段,会手下进行标记,然后在标记完成后,不是进行清理,而是让存活对象向一端进行移动,在移动的过程中,清理掉可回收的对象,这个过程就是清理,一方面可以回收掉不再使用的对象,另一方面也可以避免内存碎片,让内存更加紧凑。机器人首先标记所有正在被使用的碗筷,然后让人拿着那些碗筷向南边移动,在移动的过程中,如果有一个位置上的碗筷没有被标记,则直接回收掉,然后让人占住这个位置。

     5、分代收集算法

      餐厅老板开了几年之后,发现用餐的一个规律,就是大部分用餐都非常快,并且在用餐完后,会很快的离开位置,只有少部分人或者团体会在用完餐后,继续在餐厅内(此时不能收掉别人的碗筷),于是餐厅老板就想到一个更好的方式来进行收集碗筷,他将餐厅分为了好几个区域,有快速用餐区A,B 2个,有慢速用餐区1个,如果有人就餐就先到快速用餐区A用餐,如果快速用餐区A快被用完了,就让大家移动到B用餐区进行用餐,大部分情况下,经过这一次移动,就会剩余很多空间,如果有人经过了很多次(比如8次)移动,还在就餐,就把那个人移动到慢速用餐区就餐;当然如果在就餐时,就向老板说没 用餐的速度就是慢,也会被放到慢速用餐区。

      分代收集算法,其实不能算为一种算法,仅仅是根据对象的存活时间,来使用不同的收集算法,分代收集算法是使用上面除了引用计数算法其他3种算法的一种中和而已

     java的虚拟机中一般把堆内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

      新生代一般采用复制和标记-清理算法,新生代分为Eden区,Survivor from和Survivor to三部分,新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。java对新生代的垃圾收集称为minor gc,次数比较多,时间比较短

     年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。java对年老代的垃圾收集称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

二:GC收集器

      GC在有了以上算法的基础上,还有7种垃圾收集器,分别如下:

     1、Serial(串行)收集器

         Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。

     2、ParNew(并行)收集器

         ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

     3、Parallel Scavenge(并行回收GC)收集器

         Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

     4、Serial Old(串行GC)收集器

         Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

      5、Parallel Old(并行GC)收集器

         Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

      6、CMS(并发GC)收集器

          CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,主要是处理年老代的垃圾回收,减少应用的暂停时间,减少full gc发生的频率,整个收集过程大致分为4个步骤:

          ①.初始标记(CMS initial mark)
          ②.并发标记(CMS concurrenr mark)
          ③.重新标记(CMS remark)
          ④.并发清除(CMS concurrent sweep)

          从上面可以看出,CMS并非没有暂停,而是用两次较短的暂停标记来代替串行标记引起的长暂停,第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后,暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且remark这个阶段可以并行标记。

          而并发标记,并发清除中的并发,是指一个或者多个垃圾收集线程和应用线程并发的执行,不会暂停引用线程的运行。CMS的年轻代,仍然使用ParNew来进行收集。CMS的主要优点是和应用线程并发执行收集,并且做到低停顿,但是还是要注意以下问题

         1、在并发执行阶段,虽然不会导致应用线程的暂停,但是会占用CPU资源,有可能导致程序运行缓慢

         2、CMS不会整理碎片,从上面可以看到CMS用的是标记-清除算法,不可避免的会引入内存碎片,为了防止碎片引起fullgc,需要设置-XX:+UseCMSCompactAtFullCollection,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效,也可以设置-XX:+CMSFullGCBeforeCompaction,此参数设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程。

         3、从上面的步骤可以看到,在并发清除阶段,引用程序仍然在运行,伴随着程序的运行,自然仍然有新的对象被创建,因此需要预留足够的内存控件给应用程序使用,因此这种垃圾收集器不能像新生代的survivor区那样,几乎被占满之后,再进行收集,默认配置为68%,也就是说CMS收集器在老年代使用了68%的空间时就会被激活,可以通过参数XX:CMSInitiatingOccupancyFraction的值来设置触发百分比,要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。这个时候,可以设置参数小一点,让CMS执行的更加频繁,降低被占满的可能。

       7、G1收集器

       详细的请参考这两篇文章  jvm调优,Garbage First介绍

        最后以一幅图来结束垃圾收集算法和收集器

              

二:创建类的过程

三:读懂GC日志