Java 分代收集算法

3 篇文章 1 订阅

摘要

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。方法区永久代,回收方法同老年代。

对象分类

分代搜集算法是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生。与其说分代搜集算法是第四个算法,不如说它是对前三个算法的实际应用。
首先我们来探讨一下对象的不同特性,接下来和各位来一起给这些对象选择GC算法。
内存中的对象按照生命周期的长短大致可以分为三种,以下命名均为个人的命名。
1、夭折对象(新生代):朝生夕灭的对象,通俗点讲就是活不了多久就得死的对象。
例子:某一个方法的局域变量、循环内的临时变量等等
2、老不死对象(老年代):这类对象一般活的比较久,岁数很大还不死,但归根结底,老不死对象也几乎早晚要死的,但也只是几乎而已。
例子:缓存对象、数据库连接对象、单例对象(单例模式)等等。
3、不灭对象(永久代):此类对象一般一旦出生就几乎不死了,它们几乎会一直永生不灭,记得,只是几乎不灭而已。
例子:String池中的对象(享元模式)、加载过的类信息等等。

对象对应的内存区域

还记得前面介绍内存管理时,JVM对内存的划分吗?
我们将上面三种对象对应到内存区域当中,就是夭折对象和老不死对象都在JAVA堆,而不灭对象在方法区。
之前的一章中我们就已经说过,对于JAVA堆,JVM规范要求必须实现GC,因而对于夭折对象和老不死对象来说,死几乎是必然的结局,但也只是几乎,还是难免会有一些对象会一直存活到应用结束。然而JVM规范对方法区的GC并不做要求,所以假设一个JVM实现没有对方法区实现GC,那么不灭对象就是真的不灭对象了。
由于不灭对象的生命周期过长,因此分代搜集算法就是针对的JAVA堆而设计的,也就是针对夭折对象和老不死对象。

JAVA堆的对象回收(夭折对象和老不死对象)

有了以上分析,我们来看看分代搜集算法如何处理JAVA堆的内存回收的,也就是夭折对象与老不死对象的回收。
夭折对象:这类对象朝生夕灭,存活时间短,还记得复制算法的使用要求吗?那就是对象存活率不能太高,因此夭折对象是最适合使用复制算法的。
小疑问:50%内存的浪费怎么办?
答疑:因为夭折对象一般存活率较低,因此可以不使用50%的内存作为空闲,一般的,复制算法:使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,以此类推。
GC流程:

这里写图片描述

这里写图片描述

图中标注了三个区域中在各个阶段,各自内存的情况。相信看着图,它的GC流程已经不难理解了。
第一点是使用这样的方式,我们只浪费了10%的内存,这个是可以接受的,因为我们换来了内存的整齐排列与GC速度。第二点是,这个策略的前提是,每次存活的对象占用的内存不能超过这10%的大小,一旦超过,多出的对象将无法复制。
为了解决上面的意外情况,也就是存活对象占用的内存太大时的情况,高手们将JAVA堆分成两部分来处理,上述三个区域则是第一部分,称为新生代或者年轻代。而余下的一部分,专门存放老不死对象的则称为年老代。
是不是很贴切的名字呢?下面我们看看老不死对象的处理方式。
老不死对象:这一类对象存活率非常高,因为它们大多是从新生代转过来的。就像人一样,活的年月久了,就变成老不死了。
通常情况下,以下两种情况发生的时候,对象会从新生代区域转到年老带区域。

  1. 在新生代里的每一个对象,都会有一个年龄,当这些对象的年龄到达一定程度时(年龄就是熬过的GC次数,每次GC如果对象存活下来,则年龄加1),则会被转到年老代,而这个转入年老代的年龄值,一般在JVM中是可以设置的。
  2. 在新生代存活对象占用的内存超过10%时,则多余的对象会放入年老代。这种时候,年老代就是新生代的“备用仓库”。
    针对老不死对象的特性,显然不再适合使用复制算法,因为它的存活率太高,而且不要忘了,如果年老代再使用复制算法,它可是没有备用仓库的。因此一般针对老不死对象只能采用标记/整理或者标记/清除算法。
    以上两种情况已经解决了GC的大部分问题,因为JAVA堆是GC的主要关注对象,而以上也已经包含了分代搜集算法的全部内容,接下来对于不灭对象的回收,已经不属于分代搜集算法的内容。

方法区的对象回收(不灭对象)

不灭对象存在于方法区,在我们常用的hotspot虚拟机(JDK默认的JVM)中,方法区也被亲切的称为永久代,又是一个很贴切的名字不是吗?
其实在很久很久以前,是不存在永久代的。当时永久代与年老代都存放在一起,里面包含了JAVA类的实例信息以及类信息。但是后来发现,对于类信息的卸载几乎很少发生,因此便将二者分离开来。幸运的是,这样做确实提高了不少性能。于是永久代便被拆分出来了。
这一部分区域的GC与年老代采用相似的方法,由于都没有“备用仓库”,二者都是只能使用标记/清除和标记/整理算法。

查看GC对象是否存活的方法

1)引用计数法
基本思想:给对象中添加要给引用计数器,每当一个地方引用时,计数器值+1,当引用失效时,计数器值-1,任何时刻计数器为0的对象就不可能再被使用。
缺点:很难解决对象之间循环引用的问题。

2)可达性分析法
基本思想:通过一系列的称为“GC roots”的对象作为起始点,从这些节点,开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC root 没有任何引用链相连(用图论的话来说,就是从 GC roots 到这个对象不可达),则证明此对象是不可用的

若对象在进行可达性分析后发现没有与 GC roots 相连接的引用链,那么他将会被第一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行 finalize()方法,当对象没有重写finalize()方法或者 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没必要执行。
若该对象被判定为有必要执行 finalize方法,则这个对象会被放在一个 F-Queue 队列,
finalize方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-queue中的对象进行第二次小规模的标记,若对象要在 finalize中成功拯救自己—次要重新与引用链上的任何一个对象建立关联即可(即可以重写finalize()方法来实现,比如可以将自己赋值给某个类变量或者对象的成员变量),那么在第二次标记时他们将会被移出“即将回收”集合。
任何一个对象的 finalize()方法都只会被系统调用一次。

可作为 GC roots 的对象

1)java 虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中类的静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中 JNI 引用的对象

引用强度:强引用>软引用>弱引用>虚引用
1)强引用:类似Object obj = new Object()的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2)软引用:用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference类实现软引用。
3)弱引用:用来描述非必须对象,但其强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类实现弱引用。
4)虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。PhantomReference类实现虚引用。

Java内存分配机制

这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行。Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。如下图
  这里写图片描述

年轻代(Young Generation)

对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在年轻代上的GC。
年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再贴切不过)和两个存活区(Survivor 0 、Survivor 1)。内存分配过程为
  这里写图片描述

绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是复制算法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)。

年老代(Old Generation)

对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。  
可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

永久代(Permanent Generation方法区)

永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

  • 类的所有实例都已经被回收
  • 加载类的ClassLoader已经被回收
  • 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
    永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。HotSpot提供-Xnoclassgc进行控制
    使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息
    -verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
    -XX:+TraceClassUnLoading需要fastdebug版HotSpot支持
    垃圾收集器
参数内容
-Xms初始堆大小。如:-Xms256m
-Xmx最大堆大小。如:-Xmx512m
-Xmn新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%
-XssJDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
-XX:NewRatio新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
-XX:SurvivorRatio新生代中 Eden(8) 与 Survivor(1+1) 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
-XX:PermSize永久代(方法区)的初始大小
-XX:MaxPermSize永久代(方法区)的最大值
-XX:+PrintGCDetails打印 GC 信息
-XX:+HeapDumpOnOutOfMemoryError让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

Java GC机制

GC的概念

GC主要有YoungGC,OldGC,FullGC(还有G1中独有的Mixed GC,收集整个young区以及部分Old区)
YoungGC:回收Eden区,有些地方称之为Minor GC,或者简称YGC
OldGC:回收Old区,只单独回收Old区的只有CMS GC,且是CMS的concurrent collection模式。
FullGC:收集整个GC堆,也称之为Major GC。
当有人说“Major GC”的时候一定要问清楚他想要指的是上面的FullGC还是OldGC。对这个GC的误解最大,尤其最常用的ParNew+CMS组合,很多人误解FullGC可能是受到jstat结果的影响。
如果配置了CMS垃圾回收器,那么jstat中的FGC并不表示就一定发生了FullGC,很有可能是发生了CMS GC,而且每发生一次CMS GC,jstat中的FGC就会+2(因为CMS GC时初始化标记和重新标记都会STW,所以FGC的值会+2,可以通过让JVM按照预期GC提供的代码验证)
事实上,FullGC的触发条件比较苛刻,判断是否发生了FullGC最好通过GC日志,所以强烈建议生产环境开启GC日志,它的价值远大于它对性能的影响。

FullGC触发条件

1.没有配置 -XX:+DisableExplicitGC的情况下System.gc()会触发FullGC;
2.Promotion Failed;
3.Concurrent mode failure;
4.Metaspace Space达到MaxMetasapce 阈值;
5.执行jmap histo : live 或 jmap -dump :live;

说明:统计发现之前YGC的平均晋升大小比目前old gen剩余的空间大,触发CMS GC;Metaspace Space使用达到Metaspace阈值是触发CMS GC;

健康的GC

如何判断JVM的GC是否正常?
首先就要看YoungGC,OldGC和FullGC是否正常;
无论是定位YoungGC,OldGC,FullGC哪一种GC,判断其是否正常主要从两个维度:GC频率和STW时间;
要得到这两个维度的值,我们需要知道JVM运行了多久,执行如下命令即可:

ps -p pid -o etime
可参考的健康的GC状况

  1. YoungGC频率5秒/次;
  2. CMS GC频率不超过1天/次;
  3. 每次YoungGC的时间不超过30ms;
  4. FullGC频率尽可能完全杜绝;

G1&CMS时,FullGC回收算法会退化成Serial+SerialOld,即单线程串行回收,且完全STW,影响很大且STW时间完全不可预估,所以FullGC频率尽可能完全杜绝。

YoungGC

YoungGC是最频繁发生的,发生的概率是OldGC和FullGC的的10倍,100倍,甚至1000倍。同时YoungGC的问题也是最难定位的。这里给出YoungGC定位的三种方法:

  1. 查看服务器SWAP&IO情况,如果服务器发生SWAP,会严重拖慢GC效率,导致STW时间异常长,拉长接口响应时间,从而影响用户体验;(sar,yum install sysstat);
  2. 查看StringTable情况(参考:探索StringTable提升YGC性能);
  3. 排查每次YoungGC后幸存对象大小(JVM模型基于分配的对象朝生夕死的假设设计,如果每次YoungGC后幸存对象较大,可能存在问题)。

可参考的健康的GC状况给出建议YoungGC频率5秒/次,经验值3秒~6秒/次都是比较合理的YoungGC频率;

  • 如果YoungGC频率远高于这个值,例如20秒/次,30秒/次,甚至60秒/次,这种情况下,JVM相当空闲,处于基本上无事可做的状态。建议缩容,减少服务器浪费;
  • 如果YoungGC频率远低于这个值,例如1秒/次,甚至1秒/好多次,这种情况下,JVM相当繁忙,建议follow如下步骤进行初步症断:
  1. 检查Young区,Young区在整个堆占比在25%~40%比较合理,如果Young区太小,建议扩大Xmn。
  2. 检查SurvivorRatio,保持默认值8即可,Eden:S0:S1=8:1:1是一个比较合理的值;

OldGC

只单独回收Old区的只有CMS GC

触发CMS GC 条件比较简单,JVM有一个线程定时扫描Old区,如果发现Old区占比超过参数-XX:CMSInitiatingOccupancyFraction=75设定值(CMS条件下默认为68%),就会触发CMS GC。

(定时时间设置XX:CMSWaitDuration=2000)(Old区占比设置:-XX:CMSInitiatingOccupancyFraction=75)(-XX:+UseCMSInitiatingOccupancyOnly 只有在Old区占比满足条件的情况下才触发CMS GC)

可参考的健康的GC状况给出建议CMS GC频率不超过1天/次,如果CMS GC频率1天发生数次,甚至上10次,说明你的GC情况病的不轻了,建议follow如下步骤进行初步症断:

  1. 检查Young区与Old区比值,尽量留60%以上的堆空间给Old区;

  2. 通过jstat查看每次YoungGC后晋升到Old区对象占比,如果发现每次YoungGC后Old区涨好几个百分点,甚至上10个点,说明有大对象,建议dump(jmap -dump:format=b,file=app.bin pid)后用MAT分析;

  3. 如果不停的CMS GC,Old区降不下去,建议先执行jmap -histo pid | head -n20 查看TOP20对象分布,如果除了[B和[C,即byte[]和char[],还有其他占比较大的实例,如下图所示中TOP1的Object数组,也可通过dump后用MAT分析问题;

  4. 如果TOP20对象中有StandartSession对象,排查你的业务代码中有没有显示使用HttpSession,例如String id = request.getSession().getId();,一般的OLTP系统几乎不会使用HttpSession,且HttpSession的的生命周期很长,会加快Old区增长速度;

FullGC

  1. 如果配置CMS,由于CMS采用标记清理算法,会有内存碎片的问题,推荐配置一个查看内存碎片程度的JVM参数PrintFLSStatistics。

  2. 如果配置ParallelOldGC,那么每次Old区满后,会触发FullGC,如果FullGC频率过高,也可以通过上面OldGC段落提及的排查方法;

  3. 如果没有配置-XX:+DisableExplicitGC,即没有屏蔽System.gc()触发FullGC,那么可以通过排查GC日志中有System字样判断是否System.gc()触发;或者通过jstat -gccause pid 2s pid判定,LGCC表示最近一次GC原因,如果为"System.gc",表示由System.gc()触发,GCC表示当前GC原因,如果当前没有GC,那么就是No GC。

3种GC算法

标记-清除算法

在这里插入图片描述

标记-清除算法(老年代):分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记对象。

缺点:1)产生大量不连续的内存碎片 2)标记和清除效率都不高

复制算法

在这里插入图片描述

复制算法(新生代):它将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,则就将还存活的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。使得每次都是对整个半区进行内存回收。

优点:1)不会出现内存碎片。2)只需移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:1)将内存缩小为原来的一半。2)在对象存活率较高时会进行较多复制操作,效率较低。

商业虚拟机的分配担保机制:将内存分为一块较大的 eden 空间和两块较小的 survivor 空间,默认比例是 8:1:1,即每次新生代中可用内存空间为整个新生代容量的 90%,每次使用 eden 和其中一个 survivour。当回收时,将 eden 和 survivor 中还存活的对象一次性复制到另外一块 survivor 上,最后清理掉 eden 和刚才用过的 survivor,若另外一块 survivor 空间没有足够内存空间存放上次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

标记-清理算法

在这里插入图片描述

标记-清理算法(老年代): 标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清除,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

7种垃圾收集器

在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用):
在这里插入图片描述

并发收集和并行收集的区别

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上。 如CMS、G1(也有并行)。

并行(Parallel):指多个垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;

serial (复制算法)

单线程(单线程的意义不仅仅说明它会使用一个 cpu或一条垃圾收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候,必须暂停其他所有工作线程,直到他收集结束)。
在这里插入图片描述
应用场景:
对于运行在 client 模式下的虚拟机来说是个很好的选择。

优点:
1)简单高效(与其他收集器的单线程相比);
2)对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
3)在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的。

parNew (复制算法)

serial 收集器的多线程版本,是许多运行在 server 模式下的虚拟机首选的新生代收集器。
在这里插入图片描述
应用场景:
Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

parallel scaverge(复制算法)

其他与ParNew类似,特别之处在于:CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而其目标是达到一个可控制的吞吐量,适合在后台运算,没有太多的交互。

应用场景:
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

serial old(标记-清理)

serial 的老年代版本,单线程,
在这里插入图片描述

应用场景:
主要用于Client模式;而在Server模式有两大用途:

  1. 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配。
  2. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

parallel old(标记-清理)

parallel scaverge 老年代的版本,多线程
在这里插入图片描述
应用场景:
JDK1.6及之后用来代替老年代的Serial Old收集器;特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合。

cms(标记-清除)

一种以获取最短回收停顿时间为目标的收集器 “标记-清除”,有 4 个过程:

1)初始标记(仅标记一下GC Roots能直接关联到的对象;速度很快;但需要"Stop The World";)
2)并发标记(进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;应用程序也在运行;并不能保证可以标记出所有的存活对象;)
3)重新标记(为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;)
4)并发清除 (回收所有的垃圾对象)。
在这里插入图片描述
优点:并发收集,低停顿
缺点:
1)不能处理浮动垃圾,由于 cms 并发清除阶段,用户线程还在继续执行,伴随程序进行,还有新的垃圾产生,这一部分垃圾发生在标记之后,cms 无法在当次收集时处理他们,只能留到下一次gc。可能出现"Concurrent Mode Failure"失败。这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生。
2)对cpu 资源敏感。并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。cms 默认启动的回收线程数是(cpu 数量+3)/4。当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
3)产生大量内存碎片 ,大对象分配困难,需要提前触发另一次Full GC动作。

G1

是一款面向服务端应用的商用垃圾收集器。具备四个特点:
1)并行与并发:能充分利用多CPU、多核环境下的硬件优势;可以使用多个CPU并行来缩短"Stop The World"停顿时间;也可以并发让垃圾收集与用户程序同时进行。
2)分代收集:能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。
3)空间整合,不产生碎片:从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法;都不会产生内存碎片,有利于长时间运行,不会提前触发一次GC。
4)可预测的停顿:低停顿的同时实现高吞吐量;G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。

如何减少GC出现的次数:

  1. 对象不用时显示置null。
  2. 少用System.gc()。
  3. 尽量少用静态变量。
  4. 尽量使用StringBuffer,而不用String累加字符串。
  5. 分散对象创建或删除的时间。
  6. 少用finalize函数。
  7. 如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起OOM。
  8. 能用基本类型就不用基本类型封装类。
  9. 增大-Xmx的值。

参考:
https://www.cnblogs.com/cing/p/8652081.html (JVM的分区+查看GC对象是否存活+3种GC算法+7种垃圾收集器+如何减少GC次数)
http://www.it165.net/pro/html/201501/32890.html
http://blog.csdn.net/ochangwen/article/details/51407574
http://blog.csdn.net/ochangwen/article/details/51407167
http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值