美文网首页java
JVM系列(5) JVM 的垃圾回收机制 (GC)

JVM系列(5) JVM 的垃圾回收机制 (GC)

作者: suxin1932 | 来源:发表于2020-02-16 19:00 被阅读0次

    1.2 JVM垃圾回收算法

    JVM中数据类型

    Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。
    
    基本类型的变量保存原始值,即:他代表的值就是数值本身;
    引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
    
    基本类型包括:byte,short,int,long,char,float,double,boolean
    引用类型包括:类类型,接口类型和数组。
    

    引用类型

    对象引用类型分为强引用、软引用、弱引用和虚引用。
    
    #强引用:
    就是我们一般声明对象是虚拟机生成的引用。
    强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
    
    #软引用:
    软引用一般被做为缓存来使用。
    与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。
    如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;
    如果剩余内存相对富裕,则不会进行回收。
    换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。
    
    #弱引用:
    弱引用与软引用类似,都是作为缓存来使用。
    但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,
    因此其生命周期只存在于一个垃圾回收周期内。
    
    #虚引用
    虚引用的get方法总是返回null.
    
    强引用不用说,我们系统一般在使用时都是用的强引用。
    而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。
    因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。
    因而,他们常见的是被使用在桌面应用系统的缓存。
    
    // PhantomReference的应用并不多,其中一个应用是sun.misc.Cleaner类
    为什么要主动调用System.gc?
    既然要调用System.gc,那肯定是想通过触发一次gc操作来回收堆外内存,
    不过堆外内存不会对gc造成什么影响(这里的System.gc除外),
    但是堆外内存的回收其实依赖于gc机制,首先在java层面和在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,
    它记录了这块内存的基地址以及大小,那么既然和gc也有关,
    那就是gc能通过操作DirectByteBuffer对象来间接操作对应的堆外内存了。
    DirectByteBuffer对象在创建的时候关联了一个PhantomReference,
    说到PhantomReference它其实主要是用来跟踪对象何时被回收的,
    它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,
    并没有其他的地方引用它了,
    那将会把这个引用放到java.lang.ref.Reference.pending队列里,
    在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,
    而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,
    在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块
    
    虚-软-弱-->引用.png

    demo

    public class ReferenceDemo {
    
        private static final String SUCCESS = "success";
    
        @Test
        public void fn01() {
            // 软引用和弱引用都可以用来实现缓存功能
            SoftReference<String> softReference = new SoftReference<>("hello");
            WeakReference<String> weakReference = new WeakReference<>("world");
            WeakReference<Object> weakReference1 = new WeakReference<>(new Object());
            PhantomReference<String> phantomReference = new PhantomReference<>("tom", new ReferenceQueue<>());
            System.gc();
            // success ---> 强引用在 gc 后一定存在
            System.out.println(SUCCESS);
            // hello ---> 软引用在 gc 后可能还存在,如果一个对象只剩下一个soft引用,在jvm内存不足的时候会将这个对象进行回收
            System.out.println(softReference.get());
            // world ---> 弱引用在 gc 后可能还存在, 因为这里的 String 有常量池的引用
            System.out.println(weakReference.get());
            // null ---> 弱引用在 gc 后可能直接回收, 因为这个对象无人使用
            System.out.println(weakReference1.get());
            // null ---> 虚引用在 gc 后直接回收, 用来实现类似Object.finalize功能
            System.out.println(phantomReference.get());
        }
    
    }
    
    #Reference类
    Reference类是上面除了强类型引用外的其他引用的父类。存在四种不同的状态:
    >> Active,这是Reference对象初创的时候的状态
    >> Pending,对象在pending列表里,等待进入队列。没有注册队列的引用对象不存在这个状态
    >> Enqueued,引用对象进入队列,没有注册队列的引用对象不存在这个状态
    >> Inactive,没有注册队列的引用对象或者从队列中被移除之后所处的状态,这个状态下的对象不再变化,等待gc回收。
    
    #上面所说的pending列表是由一个静态字段和一个next对象组成,由gc负责维护:
    Reference next;
    private static Reference pending = null;
    
    #Reference中有一个ReferenceHandler线程对引用对象进行处理
    主要逻辑是从pending列表中取出一个引用对象(r.next=r),
    如果是Cleaner对象则调用其clean方法;否则加入到对应的队列中。
    
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }
    
        // Fast path for cleaners
        if (c != null) {
            c.clean();
            return true;
        }
    
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }
    

    https://www.jianshu.com/p/e66930caca9c
    http://lovestblog.cn/blog/2015/05/12/direct-buffer/

    1.2.1 按照基本回收策略分

    引用计数(Reference Counting)

    比较古老的回收算法。
    原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。
    垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
    

    标记-清除(Mark-Sweep)

    此算法执行分两阶段。
    第一阶段从引用根节点开始标记所有被引用的对象。
    第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
    
    Mark-Sweep GC.png

    复制(Copying)

    此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。
    垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。
    此算法每次只处理正在使用中的对象,因此复制成本比较小,
    同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。
    当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
    
    Copying-GC.png

    标记-整理(Mark-Compact)

    此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段:
    第一阶段从根节点开始标记所有被引用对象
    第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
    
    此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
    
    Mark-Compact-GC.png

    1.2.2 按分区对待的方式分

    1.2.2.1 增量收集(Incremental Collecting)

    实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。
    不知道什么原因JDK5.0中的收集器没有使用这种算法的。
    

    1.2.2.2 分代收集(Generational Collecting)

    基于对对象生命周期分析后得出的垃圾回收算法。
    把对象分为年轻代、年老代、持久代,对不同生命周期的对象使用对应代际的算法进行回收。
    现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
    

    为什么要分代

    分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。
    因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
    
    在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,
    比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。
    但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,
    比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
    
    试想,在不进行对象存活时间区分的情况下,
    每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,
    但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。
    因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
    

    如何分代

    jdk8垃圾分代.png
    虚拟机中的共划分为三个代:
    年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。
    其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。
    年轻代和年老代的划分是对垃圾收集影响比较大的。
    
    #年轻代:
    所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
    年轻代分三个区: 一个Eden区,两个Survivor区(一般而言)。
    大部分对象在Eden区中生成。
    >> 当Eden区满时,还存活的对象将被复制到Survivor1区,
    >> 当Survivor1区满时,此区的存活对象将被复制到Survivor2区,
    >> 当Survivor2区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。
    需要注意,Survivor的两个区是对称的,没先后关系,
    所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,
    而复制到年老区的只有从第一个Survivor去过来的对象。
    而且,Survivor区总有一个是空的。
    同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),
    这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
    
    #年老代:
    在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。
    因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    
    #持久代:
    用于存放静态文件,如今Java类、方法等。
    持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,
    例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
    持久代大小通过"-XX:MaxPermSize="进行设置。
    

    什么情况下触发垃圾回收

    由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。
    GC有两种类型:Scavenge GC和Full GC。
    
    #Scavenge GC
    一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,
    对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。
    然后整理Survivor的两个区。
    这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。
    因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。
    因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。
    
    #Full GC
    对整个堆进行整理,包括Young、Tenured和Perm。
    Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。
    在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
    
    有如下原因可能导致Full GC:
    >> 年老代(Tenured)被写满
    >> 持久代(Perm)被写满 
    >> System.gc()被显示调用 
    >> 上一次GC之后Heap的各域分配策略动态变化
    

    1.2.3 按系统线程分

    串行收集

    串行收集使用单线程处理所有垃圾回收工作, 因为无需多线程交互,实现容易,而且效率比较高。
    但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。
    当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
    
    "可以使用-XX:+UseSerialGC打开。"
    

    并行收集

    并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。
    而且理论上CPU数目越多,越能体现出并行收集器的优势。
    对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。
    "使用-XX:+UseParallelGC.打开。"
    并行收集器在javaSE5.6上引入,在Java SE6.0中进行了增强--可以对年老代进行并行收集。
    如果年老代不使用并发收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。
    "使用-XX:+UseParallelOldGC打开。
    使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数。"
    此值可以设置与机器处理器数量相等。
    
    此收集器可以进行如下配置:
    #最大垃圾回收暂停:
    指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=指定为毫秒.
    如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。
    设定此值可能会减少应用的吞吐量。
    #吞吐量:
    吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过
    "-XX:GCTimeRatio=来设定,公式为1/(1+N)。"
    例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。
    默认情况为99,即1%的时间用于垃圾回收。
    

    并发收集

    可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,
    此收集器适合对响应时间要求比较高的中、大规模应用。
    "使用-XX:+UseConcMarkSweepGC打开。"
    并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。
    在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。
    第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
    并发收集器使用处理器换来短暂的停顿时间。
    在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。
    在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
    
    #浮动垃圾
    由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,
    这些垃圾需要在下次垃圾回收周期时才能回收掉。
    所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
    
    #Concurrent Mode Failure
    并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,
    否则,垃圾回收还未完成,堆空间先满了。
    这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
    
    #启动并发收集器
    因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,
    否则会出现“Concurrent Mode Failure”。
    通过设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集
    

    summary

    #串行处理器:
    --适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。
    --缺点:只能用于小型应用
    
    #并行处理器:
    --适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。
    举例:后台处理、科学计算。
    --缺点:垃圾收集过程中应用响应时间可能加长
    
    #并发处理器:
    --适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。
    举例:Web服务器/应用服务器、电信交换、集成开发环境。
    

    1.3 JVM垃圾收集器汇总分析

    查看JVM所使用的收集器

    java -XX:+PrintCommandLineFlags -version
    
    图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用。
    虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
    
    新生代收集器:Serial、ParNew、Parallel Scavenge
    老年代收集器:CMS、Serial Old、Parallel Old
    整堆收集器: G1
    
    JVM垃圾收集器.png

    1.3.1 Serial 收集器

    Serial收集器是最基本的、发展历史最悠久的收集器。
    
    特点:单线程、简单高效(与其他收集器的单线程相比),
    对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,
    专心做垃圾收集自然可以获得最高的单线程手机效率。
    收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
    
    应用场景:适用于Client模式下的虚拟机。
    
    Serial / Serial Old收集器运行示意图.png

    1.3.2 Serial Old 收集器

    Serial Old是Serial收集器的老年代版本。
    
    特点:同样是单线程收集器,采用标记-整理算法。
    
    应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
    
    Server模式下主要的两大用途(在后续中详细讲解···):
    >> 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
    >> 作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
    

    1.3.3 ParNew收集器

    ParNew收集器其实就是Serial收集器的多线程版本。
    
    除了使用多线程外其余行为均和Serial收集器一模一样
    (参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
    
    特点:
    >> 多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同
    >> 在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
    >> 和Serial收集器一样存在Stop The World问题
    
    应用场景:
    ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,
    因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
    
    ParNew/Serial Old组合收集器.png

    1.3.4 Parallel Scavenge 收集器

    与吞吐量关系密切,故也称为 "吞吐量优先收集器"。
    
    特点:
    >> 属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
    >> 目标是达到一个可控制的吞吐量。
    >> GC自适应调节策略(与ParNew收集器最重要的一个区别)
    
    #GC自适应调节策略:
    Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。
    当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、
    晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,
    虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,
    这种调节方式称为GC的自适应调节策略。
    
    Parallel Scavenge收集器使用两个参数控制吞吐量:
    >> XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
    >> XX:GCRatio 直接设置吞吐量的大小。
    

    1.3.5 Parallel Old 收集器

    是Parallel Scavenge收集器的老年代版本。
    
    特点:多线程,采用标记-整理算法。
    
    应用场景:
    注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
    
    Parallel Scavenge/Parallel Old收集器工作过程图.png

    1.3.6 CMS收集器

    并发标记清理(Concurrent Mark Sweep,CMS)收集器
    也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器,
    是一种以获取最短回收停顿时间为目标的收集器。
    
    特点:基于标记-清除算法实现。并发收集、低停顿。
    
    #应用场景:
    适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。
    如web程序、b/s服务。
    
    #CMS收集器的运行过程分为下列4步:
    >> 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
    >> 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
    >> 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
    >> 并发清除:对标记的对象进行清除回收。
    
    CMS收集器的内存回收过程是与用户线程一起并发执行的。
    
    #CMS收集器的缺点:
    >> 对CPU资源非常敏感。
    >> 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
    >> 因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
    

    CMS的一些参数说明

    选项/默认值 说明
    -XX:+UseConcMarkSweepGC 激活CMS收集器
    -XX:ConcGCThreads 设置CMS线程的数量
    -XX:+UseCMSInitiatingOccupancyOnly 表示只在到达阀值的时候,才进行CMS回收
    -XX:CMSInitiatingOccupancyFraction 设置触发CMS老年代回收的内存使用率占比
    -XX:+CMSParallelRemarkEnabled 并行运行最终标记阶段,加快最终标记的速度
    -XX:+UseCMSCompactAtFullCollection 每次触发CMS Full GC的时候都整理一次碎片
    -XX:CMSFullGCsBeforeCompaction=* 经过几次CMS Full GC的时候整理一次碎片
    -XX:+CMSClassUnloadingEnabled 让CMS可以收集永久带,默认不会收集
    -XX:+CMSScavengeBeforeRemark 最终标记之前强制进行一个Minor GC
    -XX:+ExplicitGCInvokesConcurrent 当调用System.gc()的时候,执行并行gc,只有在CMS或者G1下该参数才有效
    CMS收集器的工作过程图.png

    1.3.7 G1收集器

    一款面向服务端应用的垃圾收集器。
    
    #特点如下:
    >> 并行与并发:
    G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。
    部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
    >> 分代收集:
    G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、
    熬过多次GC的旧对象以获取更好的收集效果。
    >> 空间整合:
    G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
    >> 可预测的停顿:
    G1除了追求低停顿外,还能建立可预测的停顿时间模型。
    能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
    
    #G1为什么能建立可预测的停顿时间模型?
    因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。
    G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,
    优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。
    
    #G1与其他收集器的区别:
    其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。
    在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。
    虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
    
    #G1收集器存在的问题:
    Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。
    在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。
    其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。
    
    #G1收集器是如何解决上述问题的?
    采用Remembered Set来避免整堆扫描。
    G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,
    会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),
    如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。
    当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
    
    #如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤:
    >> 初始标记:
    仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,
    让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
    >> 并发标记:
    从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
    >> 最终标记:
    为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。
    且对象的变化记录在线程Remembered Set  Logs里面,把Remembered Set  Logs里面的数据合并到Remembered Set中。
    (需要线程停顿,但可并行执行。)
    >> 筛选回收:
    对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
    
    G1收集器运行示意图.png

    G1的一些参数说明

    选项/默认值 说明
    -XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器
    -XX:MaxGCPauseMillis=n 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标.
    -XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45.
    -XX:NewRatio=n 新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2.
    -XX:SurvivorRatio=n eden/survivor 空间大小的比例(Ratio). 默认值为 8.
    -XX:MaxTenuringThreshold=n 提升年老代的最大临界值(tenuring threshold). 默认值为 15.
    -XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
    -XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
    -XX:G1ReservePercent=n 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
    -XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.

    1.4 常见配置汇总

    #堆设置
    -Xms:初始堆大小
    -Xmx:最大堆大小
    -XX:NewSize=n:设置年轻代大小
    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
    -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
    -XX:MaxPermSize=n:设置持久代大小
    
    #收集器设置
    -XX:+UseSerialGC:虚拟机运行在Client模式下的默认值,打开后,使用Serial和Serial Old两种组合的垃圾收集器进行GC。
    -XX:+UseParNewGC: 设置后,使用ParNew和Serial Old两种组合的垃圾收集器进行GC。
    -XX:+UseParallelGC:虚拟机运行在Server模式下的默认值,设置后,使用Parallel Scavenge 和Serial Old的组合垃圾收集器进行GC。
    -XX:+UseParalledlOldGC:设置并行年老代收集器, Parallel Scavenge+Parallel Old。
    -XX:+UseConcMarkSweepGC:设置后,使用ParNew、CMS和Serial Old组合的垃圾收集器进行GC。
    -XX:+UseG1GC: 使用G1收集器。
    
    #垃圾回收统计信息-->日志打印
    -XX:+PrintGC  // 打印GC日志
    -XX:+PrintGCDetails  // 打印详细GC日志
    -XX:+PrintHeapAtGC  // GC前后打印堆信息
    -XX:+PrintGCTimeStamps  // 打印GC发生的时间
    -XX:+PrintGCApplicationConcurrentTime  // 打印应用程序的执行时间
    -XX:+PrintGCApplicationStoppedTime  // 打印应用由于GC而产生的停顿时间
    -Xloggc:filename    // 保存GC日志
    
    #并行收集器设置
    -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    
    #并发收集器设置
    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
    
    本地GC日志配置.png

    相关文章

      网友评论

        本文标题:JVM系列(5) JVM 的垃圾回收机制 (GC)

        本文链接:https://www.haomeiwen.com/subject/nbsmfhtx.html