哪些内存要回收?

        方法区和堆的内存只有在程序运行期间才能直到会创建哪些对象,这部分内存分配和回收都是动态的。这部分内存是垃圾收集器所关注的。

        程序计数器,虚拟机栈,本地方法栈是线程隔离的,当方法结束或线程结束时,内存就回收了。


    什么时候回收?

        (1)可达性分析算法中不可达的对象(第一次标记)

        (2)筛选出有必要执行finalize()方法的对象(此对象没有覆盖finalize()方法,或者finalize()方法已被虚拟机调用过则视为没必要执行)

        (3)放置在F-Queue队列中

    如何回收?

        垃圾收集器


一. 判断对象需要回收

    1.可达性分析算法:

        当一个对象到GC Root没有任何引用链相连时,则证明此对象是不可用的。

    2.引用

        强引用>弱引用>软引用>虚引用

    3.是否确定死亡

    4.回收方法区

        回收两部分内容:废弃常量和无用的类。

        废弃常量:没有任何地方引用这个字面量,如果这时发生内存回收且有必要的话,该常量会被系统清理出常量池。

        无用的类:

            -- 该类所有的实例都已经被回收,java堆中不存在该类的任何实例;

            -- 加载该类的ClassLoader已经被回收;

            --该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。


二. 垃圾收集算法

    1.标记-清除算法

        标记出所有需要回收的对象,标记完后同意回收被标记的对象。

    2.复制算法(新生代)

        将可用内存两等分,当一块用完后,将还存活的对象复制到另一块中,把已使用的一块清除。

        对象存活率较高时要进行较多复制操作,效率低,还需要有额外的空间进行分配担保,故不适合老年代。

    3.标记-整理算法(老年代)

        标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。


. HotSpot算法实现

    1.枚举根节点:

        在进行可达性分析时必须停顿所有Java执行线程。

        在HotSpot中使用一组OopMap的数据结构来得知哪些地方存放着对象引用。

        在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。

        在OopMap协助下,HotSpot可以快速且准确地完成GC Root枚举。

    2.安全点(SafePoint):

        程序执行时只有在到达安全点才能暂停,开始GC。

        安全点的选定:程序”是否具有让程序长时间执行的特征“为标准进行选定。

            -- 因为每条指令执行的时间都非常短,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特性就是指令序列复用,例如方法调用、循环跳转、异常跳转等。

        如何在发生GC时让所有线程都跑到最近的安全点上再停顿下来:

            -- 抢先式中断(不采用):不需要线程执行代码主动去配合。在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就回复线程,让它跑到安全点上。

            -- 主动式中断:当GC需要中断线程时,不直接对线程操作,设置一个标志,各线程执行时主动去轮询这个标志,发现中断标志位真时就自己中断挂起。

                                    轮询标志的地方和安全点是重合的。

        Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。

    3.安全区域(Safe Region):

        在线程处于sleep状态或Blocked状态,程序无法响应JVM的中断请求,走到安全的地方去中断挂起,这种情况需要安全区域来解决。

        安全区域:在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。Safe Region可以看成是被扩展的Safepoint。

        当线程执行到Safe Region中的代码,首先标识自己已经进入Safe Region,当在这点时间里JVM发起GC时,就不用管标识自己为Safe Region状态的线程了。

        在线程要离开Safe Region时要检查系统是否已经完成了根节点枚举(或是整个GC过程),如果完成线程就继续执行,否则它必须等待直到收到可以安全离开Safe Region的信号为止。

        

四. 垃圾收集器

    垃圾收集器是内存回收的具体实现。

    图为HotSpot虚拟机的垃圾收集器:(存在连线说明他们之间能搭配使用)

    并行和并发在垃圾收集器中的解释:

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

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

    1.Serial(串行GC)收集器

        Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,它不仅只会使用一个CPU或者一条收集线程去完成垃圾收集作,而且必须暂停其他所有的工作线程(用户线程),直到它收集完成。

 

          

 

              Serial/Serial Old收集器运行示意图(表示Serial和Serial Old搭配使用)

        是Jvm client模式下默认的新生代收集器对于限定单个CPU的环境来说,简单高效,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,因此是运行在Client模式下的虚拟机的不错选择(比如桌面应用场景)。

    2.ParNew(并行GC)收集器

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

          

        ParNew/Serial Old收集器运行示意图(表示ParNew和Serial Old搭配使用)

        是运行在Service模式下虚拟机中首选的新生代收集器,其中一个与性能无关的原因就是除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。

PreNew收集器在单CPU环境中绝对没有Serial的效果好,由于存在线程交互的开销,该收集器在超线程技术实现的双CPU中都不能一定超过Serial收集器。默认开启的垃圾收集器线程数就是CPU数量,可通过-XX:parallelGCThreads参数来限制收集器线程数。

        PreNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。        

    3.Parallel Scavenge(吞吐量优先)收集器

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

          

        Parallel Scavenge/Parallel Old收集器运行示意图(表示Parallel Scavenge和Parallel Old搭配使用)

        短停顿时间适合和用户交互的程序,体验好。高吞吐量适合高效利用CPU,主要用于后台运算不需要太多交互。

        提供了两个参数来精确控制吞吐量:1.最大垃圾收集器停顿时间(-XX:MaxGCPauseMillis    大于0的毫秒数,停顿时间小了就要牺牲相应的吞吐量和新生代空间),2.设置吞吐量大小(-XX:GCTimeRatio    大于0小于100的整数,默认99,也就是允许最大1%的垃圾回收时间)。

        还有一个参数表示自适应调节策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。这是一个开关参数,当这个参数打开之后,就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)今生老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行情况手机监控信息,动态调整停顿时间和吞吐量大小。也是其与PreNew收集器的一个重要区别,也是其无法与CMS收集器搭配使用的原因(CMS收集器尽可能地缩短垃圾收集时用户线程的停顿时间,以提升交互体验)。

    4.Serial Old(MSC)收集器

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


        

              Serial/Serial Old收集器运行示意图(表示Serial和Serial Old搭配使用 

  如果在Service模式下使用:1.一种是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,因为那时还没有Parallel  Old老年代收集器搭配;2.另一种就是作为CMS收集器的后备预案,在并发收集发生Concurrent Model Failure时使用。

    5.Parallel Old收集器

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

        由于之前有一个Parallel Scavenge新生代收集器,,但是却无老年代收集器与之完美结合,只能采用Serial Old老年代收集器,但是由于Serial Old收集器在服务端应用性能上低下(毕竟单线程,多CPU浪费了),其吞吐量反而不一定有PreNew+CMS组合。

        

        Parallel Scavenge/Parallel Old收集器运行示意图(表示Parallel ScavengeParallel Old搭配使用)

    6.CMS收集器

        CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是HotSpot虚拟机中的一款真正意义上的并发收集器,第一次实现了让垃圾回收线程和用户线程(基本上)同时工作。用CMS收集老年代的时候,新生代只能选择Serial或者ParNew收集器。

        CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

            ①.初始标记(CMS initial mark)

            ②.并发标记(CMS concurrenr mark)

            ③.重新标记(CMS remark)

            ④.并发清除(CMS concurrent sweep)

                其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程(Stop The World)初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

                由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是HotSpot虚拟机中的一款真正意义上的并发收集器,第一次实现了让垃圾回收线程和用户线程(基本上)同时工作。用CMS收集老年代的时候,新生代只能选择Serial或者ParNew收集器。

  CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)

②.并发标记(CMS concurrenr mark)

③.重新标记(CMS remark)

④.并发清除(CMS concurrent sweep)

     其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程(Stop The World)初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

     由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。


      

                                                                            CMS收集器运行示意图

        CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:

            1.CMS收集器对CPU资源非常敏感。在并发(并发标记、并发清除)阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。收集器线程所占用的CPU数量为:(CPU+3)/4=0.25+3/(4*CPU因此这时垃圾收集器始终不会占用少于25%的CPU,因此当进行并发阶段时,虽然用户线程可以跑,但是很缓慢,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种情况,产生了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占方式来模拟多任务机制,就是在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽量减少GC线程独占CPU,这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经被声明为“deprecated”。

            2.CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full  GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以降低内存回收次数提高性能。JDK1.6中,CMS收集器的启动阈值已经提升到92%。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。

            3.最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full  GC之后增加一个内存碎片的合并整理过程,但是内存整理过程是无法并发的,因此解决了空间碎片问题,却使停顿时间变长。还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程(默认值是0,表示每次进入Full GC时都进行碎片整理)。

    7.G1收集器

G1收集器具体介绍:https://www.cnblogs.com/jing99/p/6072059.html


五.GC日志查看

2018-04-24T09:52:41.903+0800: 20.076: [GC (Metadata GC Threshold) [PSYoungGen: 1208285K->104653K(1273344K)] 1257730K->154106K(4135424K), 0.1108126 secs] [Times: user=0.20 sys=0.02, real=0.11 secs]

    20.076::GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数;

    [GC或[Full GC:这次垃圾收集的停顿类型,不是用来区分新生代GC和老年代GC的,如果有Full代表这次GC是发生了Stop-The-World,如果是[Full GC (System)则说明是调用System.gc()方法所触发的收集;

    [PSYoungGen:表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。

区域

GC收集器

[DefNew

Serial收集器中的新生代名为“Default New Generation”

[Tenured


[Perm


[ParNew

Parallel New Generation

[PSYoungGen

Parallel Scavenge收集器

    方括号内的1208285K->104653K(1273344K):GC前该内存区域已使用容量 -> GC后该内存区域已使用容量(该内存区域总容量)

    方括号外的1257730K->154106K(4135424K):GC前Java堆已使用容量 -> GC后Java堆已使用容量(Java堆总容量)

    0.1108126 secs:表示该内存区域GC所占用的时间,单位是秒。

    [Times: user=0.20 sys=0.02, real=0.11 secs]:更具体的时间。用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束所经过的墙钟时间。

        墙钟时间包括各种非运算的等待耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超富哦real时间是完全正常的。

2018-04-24T09:52:42.014+0800: 20.187: [Full GC (Metadata GC Threshold) [PSYoungGen: 104653K->0K(1273344K)] [ParOldGen: 49452K->129000K(2862080K)] 154106K->129000K(4135424K), [Metaspace: 58095K->58034K(1101824K)], 0.2967997 secs] [Times: user=0.88 sys=0.05, real=0.30 secs]

2018-04-24T09:52:45.314+0800: 23.487: [GC (Allocation Failure) [PSYoungGen: 1121792K->68747K(1282560K)] 1250792K->197755K(4144640K), 0.0823790 secs] [Times: user=0.14 sys=0.00, real=0.08 secs]