zoukankan      html  css  js  c++  java
  • @JVM垃圾收集器种类

    为什么要分代

    之所以采用分代垃圾收集机制是因为不同的对象生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

    在Java程序运行的过程中,会产生大量的对象,其中有些对象与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

    试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

    如何分代

    如图所示:

    虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

    年轻代:

    所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。

    年老代:

    在年轻代中经历了N(这个值是可以配置的)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    持久代:

    用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响。

    什么情况下触发垃圾回收

    由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GCFullGC

    Minor GC

    一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发ScavengeGC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法(年轻代采用复制算法),使Eden去能尽快空闲出来。

    Full GC

    整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆对进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致FullGC:

    • 年老代(Tenured)被写满
    • 持久代(Perm)被写满
    • System.gc()被显示调用
    • 上一次GC之后Heap的各域分配策略动态变化

    垃圾收集器就是上面讲的理论知识的具体实现了。不同虚拟机所提供的垃圾收集器可能会有很大差别,我们使用的是HotSpot,HotSpot这个虚拟机所包含的所有收集器如图:

    上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,那说明它们可以搭配使用。虚拟机所处的区域说明它是属于新生代收集器还是老年代收集器。多说一句,我们必须明确一个观点:没有最好的垃圾收集器,更加没有万能的收集器,只能选择对具体应用最合适的收集器。这也是HotSpot为什么要实现这么多收集器的原因。OK,下面一个一个看一下收集器。

    1、Serial收集器

    最基本、发展历史最久的收集器,这个收集器是一个采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。后者意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这对很多应用是难以接受的。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿是完全可以接受的。Serial收集器运行过程如下图所示:

    说明:1.需要STW(Stop The World),停顿时间长。2.简单高效,对于单个CPU环境而言,Serial收集器由于没有线程交互开销,可以获取最高的单线程收集效率。

    2、ParNew收集器

    ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法。ParNew收集器除了多线程以外和Serial收集器并没有太多创新的地方,但是它却是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作(看图)。CMS收集器是一款几乎可以认为有划时代意义的垃圾收集器,因为它第一次实现了让垃圾收集线程与用户线程基本上同时工作。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于线程交互的开销,该收集器在两个CPU的环境中都不能百分之百保证可以超越Serial收集器。当然,随着可用CPU数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。ParNew收集器运行过程如下图所示:

    3、ParallelScavenge收集器

    ParallelScavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,但是它的特点是它的关注点和其他收集器不同。介绍这个收集器主要还是介绍吞吐量的概念。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,ParallelScavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器

    停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。

    虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,ParallelScavenge收集器也被称为"吞吐量优先收集器"。ParallelScavenge收集器有一个-XX:+UseAdaptiveSizePolicy参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,使用ParallelScavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择

    4、SerialOld收集器

    Serial收集器的老年代版本,同样是一个单线程收集器,使用"标记-整理算法",这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

    5、ParallelOld收集器

    ParallelScavenge收集器的老年代版本,使用多线程和"标记-整理"算法。这个收集器在JDK1.6之后的出现,"吞吐量优先收集器"终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑ParallelScavenge收集器+ParallelOld收集器的组合。运行过程如下图所示:

    6、CMS收集器

    CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记-清除算法,收集过程分为如下四步:

    (1).初始标记,标记GCRoots能直接关联到的对象,时间很短。

    (2).并发标记,进行GCRootsTracing(可达性分析)过程,时间很长。

    (3).重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。

    (4).并发清除,回收内存空间,时间很长。

    其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示:

    说明:1.对CPU资源非常敏感,可能会导致应用程序变慢,吞吐率下降。2.无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。3.由于采用的标记-清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次FullGC。虚拟机提供了-XX:+UseCMSCompactAtFullCollection参数来进行碎片的合并整理过程,这样会使得停顿时间变长,虚拟机还提供了一个参数配置,-XX:+CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的FullGC后,接着来一次带压缩的GC。

    7、G1收集器

    G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1收集器有以下特点:

    (1).并行和并发。使用多个CPU来缩短StopTheWorld停顿时间,与用户线程并发执行。

    (2).分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。

    (3).空间整合。基于标记-整理算法,无内存碎片产生。

    (4).可预测的停顿。能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region的集合。

    8、常用的收集器组合

    理解GC日志

    每种收集器的日志形式都是由它们自身的实现所决定的,换言之,每种收集器的日志格式都可以不一样。不过虚拟机为了方便用户阅读,将各个收集器的日志都维持了一定的共性,来看下面的一段GC日志:

    [GC[DefNew:310K->194K(2368K),0.0269163secs]310K->194K(7680K),0.0269513secs][Times:user=0.00sys=0.00,real=0.03secs]
    [GC[DefNew:2242K->0K(2368K),0.0018814secs]2242K->2241K(7680K),0.0019172secs][Times:user=0.00sys=0.00,real=0.00secs]
    [FullGC(System)[Tenured:2241K->193K(5312K),0.0056517secs]4289K->193K(7680K),[Perm:2950K->2950K(21248K)],0.0057094secs][Times:user=0.00sys=0.00,real=0.00secs]
    Heap
    defnewgenerationtotal2432K,used43K[0x00000000052a0000,0x0000000005540000,0x0000000006ea0000)
    edenspace2176K,2%used[0x00000000052a0000,0x00000000052aaeb8,0x00000000054c0000)
    fromspace256K,0%used[0x00000000054c0000,0x00000000054c0000,0x0000000005500000)
    tospace256K,0%used[0x0000000005500000,0x0000000005500000,0x0000000005540000)
    tenuredgenerationtotal5312K,used193K[0x0000000006ea0000,0x00000000073d0000,0x000000000a6a0000)
    thespace5312K,3%used[0x0000000006ea0000,0x0000000006ed0730,0x0000000006ed0800,0x00000000073d0000)
    compactingpermgentotal21248K,used2982K[0x000000000a6a0000,0x000000000bb60000,0x000000000faa0000)
    thespace21248K,14%used[0x000000000a6a0000,0x000000000a989980,0x000000000a989a00,0x000000000bb60000)
    Nosharedspacesconfigured.

    1、日志的开头"GC"、"FullGC"表示这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有Full,则说明本次GC停止了其他所有工作线程(Stop-The-World)。看到FullGC的写法是"FullGC(System)",这说明是调用System.gc()方法所触发的GC。

    2、"GC"中接下来的"[DefNew"表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为"DefaultNewGeneration",所以显示的是"[DefNew"。如果是ParNew收集器,新生代名称就会变为"[ParNew",意为"ParallelNewGeneration"。如果采用ParallelScavenge收集器,那它配套的新生代称为"PSYoungGen",老年代和永久代同理,名称也是由收集器决定的。

    3、后面方括号内部的"310K->194K(2368K)"、"2242K->0K(2368K)",指的是该区域已使用的容量->GC后该内存区域已使用的容量(该内存区总容量)。方括号外面的"310K->194K(7680K)"、"2242K->2241K(7680K)"则指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆总容量)

    4、再往后"0.0269163secs"表示该内存区域GC所占用的时间,单位是秒。最后的"[Times:user=0.00sys=0.00real=0.03secs]"则更具体了,user表示用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束经过的墙钟时间。后面两个的区别是,墙钟时间包括各种非运算的等待消耗,比如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以如果看到user或sys时间超过real时间是完全正常的。

    5、"Heap"后面就列举出堆内存目前各个年代的区域的内存情况。

  • 相关阅读:
    JAVA深入研究——Method的Invoke方法。
    java String->float,float->int
    Java中Object转化为int类型
    Android使用SeekBar
    转: 最值得阅读学习的 10 个 C 语言开源项目代码
    Eclipse快捷键列表大全
    Android使用的Eclipse NDK开发较详细一篇文章
    rm 删除带空格的文件或者目录
    man命令中的文本操作
    androidSDK无法更新的解决方法之一
  • 原文地址:https://www.cnblogs.com/winner-0715/p/5022263.html
Copyright © 2011-2022 走看看