JVM之---垃圾回收

JVM通过GC来回收堆和方法区中的内存,GC的基本原理首先会找到程序中不再被使用的对象,然后回收这些对象所占用的内存。

1.收集器

    通常采用收集器的方式实现GC,主要的收集器有引用计数收集器和跟踪收集器。

     1.1引用计数收集器

    引用计数收集器采用的是分散式的管理方式,通过计数器记录对象是否被引用。当计数器为零时,说明此对象已经不再被使用,于是可进行回收。引用计数需要在每次对象赋值时进行引用计数器的增减,它有一定的消耗。另外,引用计数器对于循环引用的场景没有办法实现回收。

     1.2跟踪收集器

    跟踪收集器采用的集中式的管理方式,全局记录数据的引用状态。基于一定条件的触发(例如定时、空间不足),执行时需要从根集合来扫描对象的引用关系,这可能会造成应用程序暂停,主要有复制(copying)、标记-清除(mark-sweep)、标记-压缩(mark-compact)三种实现算法。

   1)复制

      复制采用的方式为从根集合扫描出存活的对象,并将找到的存活对象复制到一块新的完全未使用的空间中。复制收集器方式仅需要从根集合扫描所有存活的对象,当要回收的空间中存活对象较少时,复制算法会比较高效,其带来的成本是要增加一块空的内存空间及进行对象的移动。

   2)标记-清除

    标记-清除采用的方式是从根集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未标记的对象,并进行回收。标记-清除动作不需要进行对象的移动,且仅对不存活的对象进行处理,在空间中存活对象较多的情况下较为高效。但由于标记-清除采用的是直接回收不存活对象所占用的内存,因此会造成内存碎片。

   3)标记-压缩

    标记-压缩采用和标记-清除一样的方式对存活的对象进行标记,但在清除时则不同。在回收不存活对象所占用的内存空间后,会将其他所有存活对象都往左端空闲的空间进行移动,并更新引用其他对象的指针。 标记-压缩在标记-清除的基础上还需要进行对象的移动,成本相对较高,好处则是不产生内存碎片。

2.根对象集合和引用

   跟踪收集器中几种收集方式都是从根集合对象开始寻找存活对象,那么什么的根集合对象呢?

   在Sun  JDK中,新生代中的根集合对象指的是这些对象:当前运行线程栈上的引用的对象、常量及静态变量、传到本地方法中,还没有被本地方法释放的对象引用。但如果只是从上面这些根集合对象中扫描新生代中的存活对象,则当旧生代中的对象也引用了新生代中的对象时就会出现问题,可能会把存活的对象当成非存活对象进行回收。如果扫描整个旧生代,由于旧生代通常比较大,JVM的性能会受到比较大的影响。为解决这个问题,Sun JDK采用了remember set方式来解决这个问题。

   Sun JDK在进行对象赋值时,如果发现赋值的为一个对象引用,则产生write barrier,然后检查需要赋值的对象是否在旧生代及赋值的对象引用是否指向新生代;如果满足条件,则在remember set中做个标识。

   因此,新生代在执行Minor GC时,完整的根集合为Sun JDK认为的根集合对象加上remember set 中标记的对象。

   在确定根对象后,即可进行扫描后寻找存活的对象。为了避免在扫描过程对象间的引用关系发生变化,Sun JDK采取暂停应用的方式。Sun JDK在编译代码时为每段方法注入了SafePoint,通常SafePoint位于方法中循环的结束点和方法执行完毕的点,在暂停应用时需要等待所有的用户现场都进入到SafePoint,在用户线程进入SafePoint后,如果发现此时要进行Minor GC,则将其内存页设置为不可读状态,从而实现暂停用户现场的执行。

   在扫描时,GC会首先判断所扫描的对象引用关系是否为Reference类型(SoftReference,WeakReference,PhantomReference),如果是Reference类型,且其所引用的对象无强引用,则认为该对象为相应的Reference类型,之后GC在进行回收这些对象则根据Reference类型的不同进行相应的处理。

3.新生代可用GC(Minor GC)

    IBM研究:通常运行的程序有98%是临时对象,这些对象在新生代中存活时间较短,因而Sun JDK选择了基于Copying算法来实现对新生代对象的回收,根据上面对Copying算法的介绍,在执行复制时,需要一块未使用的空间来存放存活的对象,这是新生代被划分成Eden、S0、S1三块空间的原因。新生代分配内存采用空闲指针的方式,指针保持最后一个分配的对象在新生代内存区间的位置,当有新的对象要分配内存时,只须检查剩余的空间是否够存放新的对象,如果够,则直接创建对象并更新指针;否则就执行新生代GC。

    根据扫描和复制的操作方式不同,新生代可以使用的GC共有三种:串行GC(-XX:+UseSerialGC)、并行回收GC(-XX:+UseParallelGC)、并行GC(-XX:+UseParNewGC).

    这三种的详细情况在网上资料有很多,再次就不罗列,只进行下小结:

     1)对象从新生代转入旧生代的条件

     串行和并行采用的规则是相同的:只有经历几次Minor GC仍然存活的对象,才放入旧生代中,但也并不是说对象一定会存活MaxTenuringThreshold次才会晋升到旧生代。

     并行回收的规则是:当需要给对象分配内存时,Edge Space空间不够的情况下,如果此对象的大小大于或等于Edge Space的一半时,直接在旧生代上分配。

    2)Edge Space、S0、S1空间大小可变性

      在并行回收和并行这两种方式进行gc时,可以根据Minor GC的频率、消耗时间来动态调整EdgeSpace、S0、S1的大小。通过参数-XX:(+/-)UseAdaptiveSizePolicy进行设置。

      但串行方式无法动态调整空间大小。

    3)与旧生代cms回收的关系

      当旧生代采用CMS GC时,有些过程是并发进行的,如此时发生Minor GC,需要进行相应的处理,而并行回收GC是没有做这些处理的,此时新生代GC可以采用串行和并行回收,默认情况下新生代采用并行GC方式。

   4)SurvivorRatio默认值

    串行和并行GC的SurvivorRatio默认值都是8。

    并行回收GC时采用的是InitialSurvivorRatio,这个值的默认值也是8。该值减去2就是SurvivorRatio的值,也就是说在并行回收时SurvivorRatio的默认值就是6.

4.旧生代可用GC(Major GC)

   旧生代GC可用的回收方式有串行、并行、及并发三种。虽然名称和旧生代大体相同,但其实现方式却有很大不同。

   1)串行(-XX:+UseSerialGC)

    旧生代的串行基于Mark-Sweep-Compact实现,该方法结合了Mark-Sweep、Mark-Compact做了一些改进。串行执行的整个过程需暂停应用,且采用的为单线程方式,通常要耗费较长的时间。

   2)并行(-XX:+UseParallelGC、-XX:+UseParallelOldGC)

     并行GC采用Mark-Compact实现,首先将旧生代空间划分为并行线程个数的region,经过分析,大部分情况下经过多次GC后,通常旧生代空间左边存放的是有些活跃的对象。对这些对象进行压缩移动是不值得的,因此并行GC对此进行了优化,方式是从最左边开始向右扫描regions,直到找到第一个值得进行压缩移动的region,并将此region左边的region作为密度高区,对这些区域则不进行回收;然后继续向右扫描,对于一遍的regions根据存活的空间来决定压缩移动的源region和目标region,切换引用这些对象的指针,并在region上做标识,同时清除regions中其他不存活对象所占用的空间,目前此过程为单线程进行。基于regions上分析的信息,找到需要操作的目标region及完全没有存活对象的region,并行的进行对象移动和region的回收。

     并行大部分操作是多线程同时进行操作的,在加上进行的密度高区(dense prefix)的优化,因此其对应用造成的暂停时间会缩短。但由于旧生代通常都比较大,扫描和标识对象上花费的时间还是比较长,也会造成应用暂停一定的时间。

     指定并行GC可以用两个参数:-XX:+UseParallelGC制定使用Parallel Mark Sweep、-XX:+UseParallelOldGC制定Parallel Compacting

   3)并发(-XX:+UseconcMarkSweepGC)

     为满足对应用响应时间的高要求,Sun JDK提供了CMS GC,好处是GC的大部分动作均于应用并发进行,因此大大缩短了GC造成应用暂停的时间。但CMS GC需要执行三次标记,因此其完整的因此GC执行时间会比并行GC长,因而该中GC方式不适用于对吞吐量有高要求的应用。另外,CMS回收内存的方式使得其容易产生内存碎片,降低了内存空间的利用率,为了克服该弊端,CMS提供了一个整理碎片的功能,可以通过参数-XX: +UseCMSCompactAtFullCollection来启动此功能。

     CMS GC触发的条件为旧生代已使用的空间达到总空间设定的百分比(-XX:CMSInitiatingOccupancyFraction=80)或者持久代已使用的空间达到设定的百分比

5.Full GC

   除CMS GC外,当旧生代和持久化触发GC时,其实是对新生代、旧生代及持久代都进行GC,因此通常又称为Full GC。当Full GC被触发时,首先按照新生代配置的GC方式对新生代进行GC,然后按照旧生代的GC方式对旧生代、持久代进行GC。但其中有一个特殊情况,如在进行新生代GC前,JVM根据之前的统计信息估算到Minor GC后需要移动到旧生代的对象大小可能大于旧生代的剩余空间,这种情况下Minor GC就不会执行了,而是执行采用旧生代的GC方式对新生代、旧生代及持久代进行回收。

   除直接调用System.gc()外,出发Full GC的情况有下面四种:

   1)旧生代空间不足
   2)持久代空间不足
   3)统计得到Minor GC后移到旧生代的对象平均大小大于旧生代的空间大小
   4) CMS GC 时出现promotion failed和Concurrent mode failure

      promotion failed是在进行Minor GC时,S区间放不下、对象只能放到旧生代,而此时旧生代也放不下。

      Concurrent mode failure是在执行CMS GC的过程中同时又有对象要放入到旧生代,而此时旧生代空间不足造成的。

6.GC选择基本策略

     Sun JDK提供了两种简单的方式来帮助选择GC:吞吐量(GC所耗费的时间占应用运行总时间的百分比)优先和暂停时间(每次GC造成应用的停顿时间)优先。

     默认情况下首先满足暂停时间优先策略,再满足吞吐量优先策略。大多数情况下只须配置JVM堆的大小及持久代的大小就可以让GC符合应用的要求运行,对于访问量、数据量较大的应用而言,如果确定GC对应用的运行状况造成了影响,则可根据应用状况来精确控制内存空间中每个代的大小、选择GC方式及调整GC参数,尽可能降低GC对应用运行的影响。

7.查看JVM参数常用命令

  • jps  查看java进程id
  • Jconsole,一个图形化工具,可以显示内存、线程、类和MBean等信息
  • Jmap –heap pid  查看JVM内存状况
  • Jmap  -histo pid 查看JVM堆中对象的详细占用情况
  • jstat –gcutil pid 1000 10 查看在执行Minor  gc时新生代旧生代的各项变化
  • jinfo –flag 参数名  pid  查看某一个参数的值
  • java   -XX:+PrintFlagsFinal  输出jvm所有参数的值
posted on 2013-12-20 16:59  孙振超  阅读(2570)  评论(0编辑  收藏  举报