JVM 垃圾回收(GC)机制

目录java

1、背景面试

2、 哪些内存须要回收?算法

一、引用计数算法缓存

2 、可达性分析算法多线程

3、 四种引用状态并发

一、强引用ide

二、软引用布局

三、弱引用性能

四、虚引用优化

对象死亡(被回收)前的最后一次挣扎

方法区如何判断是否须要回收

4、垃圾收集算法

一、标记-清除(Mark-Sweep)算法

二、复制(Copying)算法

三、标记-整理(Mark-Compact)算法

四、分代收集算法

5、垃圾收集器

一、Serial收集器

二、ParNew收集器

三、Parallel Scavenge收集器

四、Serial Old收集器

五、Parallel Old收集器

六、CMS收集器

七、G1收集器

八、经常使用的收集器组合

6、理解GC日志

7、GC是何时触发的(面试最多见的问题之一)

7.1 Scavenge GC

7.2 Full GC


1、背景

垃圾回收(GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史比Java久远,早在1960年Lisp这门语言中就使用了内存动态分配和垃圾回收技术。

2、 哪些内存须要回收?

JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,所以这几个区域的内存分配和回收都具有肯定性,就不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。而Java堆区和方法区则不同,这部份内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

垃圾收集器在对堆区和方法区进行回收前,首先要肯定这些区域的对象哪些能够被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法!

1引用计数算法

1.1 、算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每一个对象实例都有一个引用计数。当一个对象被建立时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例能够被看成垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

1.2 、优缺点

优势:引用计数收集器能够很快的执行,交织在程序运行中。对程序须要不被长时间打断的实时环境比较有利。

缺点:没法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

在vm options处加入-XX:+PrintGCDetails

/**
 * 虚拟机参数:-verbose:gc
 */
public class ReferenceCountingGC
{
    private Object instance = null;
    private static final int _1MB = 1024 * 1024;
    
    /** 这个成员属性惟一的做用就是占用一点内存 */
    private byte[] bigSize = new byte[2 * _1MB];
    
    public static void main(String[] args)
    {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
        objectA = null;
        objectB = null;
        
        System.gc();
    }
}

看下运行结果:

[GC 4417K->288K(61440K), 0.0013498 secs]
[Full GC 288K->194K(61440K), 0.0094790 secs]

这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将objectAobjectB赋值为null,也就是说objectAobjectB指向的对象已经不可能再被访问,可是因为它们互相引用对方,致使它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2 可达性分析算法

这个算法的基本思想是经过一系列称为“GC Roots”的对象做为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证实此对象是不可用的。

那么问题又来了,如何选取GCRoots对象呢?在Java语言中,能够做为GCRoots的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫作局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中常量引用的对象。

(4). 本地方法栈中JNI(Native方法)引用的对象。

下面给出一个GCRoots的例子,以下图,为GCRoots的引用链。

由图可知,obj八、obj九、obj10都没有到GCRoots对象的引用链,即使obj9和obj10之间有引用链,他们仍是会被当成垃圾处理,能够进行回收

3、 四种引用状态

在JDK1.2以前,Java中引用的定义很传统:若是引用类型的数据中存储的数值表明的是另外一块内存的起始地址,就称这块内存表明着一个引用。这种定义很纯粹,可是太过于狭隘,一个对象只有被引用或者没被引用两种状态。咱们但愿描述这样一类对象:当内存空间还足够时,则能保留在内存中;若是内存空间在进行垃圾收集后仍是很是紧张,则能够抛弃这些对象。不少系统的缓存功能都符合这样的应用场景。在JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次减弱。

一、强引用

代码中广泛存在的相似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

二、软引用

描述有些还有用但并不是必需的对象。在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围进行二次回收。若是此次回收尚未足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。

三、弱引用

描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收以前,垃圾收集器工做以后,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。

四、虚引用

这个引用存在的惟一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间彻底不要紧。Java中的类PhantomReference表示虚引用。

对象死亡(被回收)前的最后一次挣扎

即便在可达性分析算法中不可达的对象,也并不是是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

  • 第一次标记:若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记;
  • 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有从新与引用链创建关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,若是对象在finalize()方法中从新与引用链创建了关联关系,那么将会逃离本次回收,继续存活。

package com.demo;

/*
 * 此代码演示了两点:
 * 1.对象能够再被GC时自我拯救
 * 2.这种自救的机会只有一次,由于一个对象的finalize()方法最多只会被系统自动调用一次
 * */
public class FinalizeEscapeGC {
    
    public String name;
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public FinalizeEscapeGC(String name) {
        this.name = name;
    }

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        System.out.println(this);
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    @Override
    public String toString() {
        return name;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC("leesf");
        System.out.println(SAVE_HOOK);
        // 对象第一次拯救本身
        SAVE_HOOK = null;
        System.out.println(SAVE_HOOK);
        System.gc();
        // 由于finalize方法优先级很低,因此暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }

        // 下面这段代码与上面的彻底相同,可是这一次自救却失败了
        // 一个对象的finalize方法只会被调用一次
        SAVE_HOOK = null;
        System.gc();
        // 由于finalize方法优先级很低,因此暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }
    }
}

运行结果以下

leesf
null
finalize method executed!
leesf
yes, i am still alive :)
no, i am dead :

由结果可知,该对象拯救了本身一次,第二次没有拯救成功,由于对象的finalize方法最多被虚拟机调用一次。此外,从结果咱们能够得知,一个堆对象的this(放在局部变量表中的第一项)引用会永远存在,在方法体内能够将this引用赋值给其余变量,这样堆中对象就能够被其余变量所引用,即不会被回收。

方法区如何判断是否须要回收

方法区存储内容是否须要回收的判断是不同的。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可经过引用的可达性来判断,可是对于无用的类则须要同时知足下面3个条件:

  • 该类全部的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

4、垃圾收集算法

一、标记-清除(Mark-Sweep)算法

这是最基础的算法,标记-清除算法就如同它的名字样,分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,标记完成后统一回收全部被标记的对象。这种算法的不足主要体如今效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会致使之后程序运行过程当中在须要分配较大对象时,没法找到足够的连续内存而不得不提早触发一次垃圾收集动做。标记-清除算法执行过程如图:

二、复制(Copying)算法

复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,而后再把已经使用过的内存空间一次性清理掉。这样每次只须要对整个半区进行内存回收,内存分配时也不须要考虑内存碎片等复杂状况,只须要移动指针,按照顺序分配便可。复制算法的执行过程如图:

不过这种算法有个缺点,内存缩小为了原来的一半,这样代价过高了。如今的商用虚拟机都采用这种算法来回收新生代,不过研究代表1:1的比例很是不科学,所以新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。固然,咱们没有办法保证每次回收都只有很少于10%的对象存活,当Survivor空间不够用时,须要依赖老年代进行分配担保(Handle Promotion)。

三、标记-整理(Mark-Compact)算法

复制算法在对象存活率较高的场景下要进行大量的复制操做,效率很低。万一对象100%存活,那么须要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,所以通常不能直接选用复制算法。根据老年代的特色,有人提出了另一种标记-整理算法,过程与标记-清除算法同样,不过不是直接对可回收对象进行清理,而是让全部存活对象都向一端移动,而后直接清理掉边界之外的内存。标记-整理算法的工做过程如图:

四、分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区以外还有一个代就是永久代(Permanet Generation)。老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。

大批对象死去、少许对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法

4.1 年轻代(Young Generation)的回收算法

a) 全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。

b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(通常而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,而后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另外一个survivor1区,而后清空eden和这个survivor0区,此时survivor0区是空的,而后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫作Minor GC,MinorGC发生频率比较高(不必定等Eden区满了才触发)。

4.2 年老代(Old Generation)的回收算法

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大不少(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

4.3 持久代(Permanent Generation)的回收算法

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。持久代也称方法区

对象优先在年轻态区中分配,若没有足够空间,Minor GC; 
大对象(须要大量连续内存空间)直接进入年老态;长期存活的对象进入年老态。若是对象在年轻态出生并通过第一次MGC后仍然存活,年龄+1,若年龄超过必定限制(15),则被晋升到年老态。

5、垃圾收集器

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

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

一、Serial收集器

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

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

二、ParNew收集器

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

三、Parallel Scavenge收集器

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

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

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

四、Serial Old收集器

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

五、Parallel Old收集器

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

六、CMS收集器

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

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

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

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

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

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

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

七、G1收集器

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

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

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

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

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

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

八、经常使用的收集器组合

、理解GC日志

在vm options处加入-XX:+PrintGCDetails

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

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)
  eden space 2176K,   2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)
  from space 256K,   0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)
  to   space 256K,   0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)
 tenured generation   total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)
   the space 5312K,   3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)
 compacting perm gen  total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)
   the space 21248K,  14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000)
No shared spaces configured.

一、日志的开头“GC”、“Full GC”表示此次垃圾收集的停顿类型,而不是用来区分新生代GC仍是老年代GC的。若是有Full,则说明本次GC中止了其余全部工做线程(Stop-The-World)。看到Full GC的写法是“Full GC(System)”,这说明是调用System.gc()方法所触发的GC。

二、“GC”中接下来的“[DefNew”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,因此显示的是“[DefNew”。若是是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。若是采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

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

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

五、“Heap”后面就列举出堆内存目前各个年代的区域的内存状况。

7、GC是何时触发的(面试最多见的问题之一)

因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GC和Full GC。

7.1 Scavenge GC

通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

7.2 Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个堆进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于Full GC的调节。有以下缘由可能致使Full GC:

a) 年老代(Tenured)被写满;

b) 持久代(Perm)被写满;

c) System.gc()被显示调用;

d) 上一次GC以后Heap的各域分配策略动态变化;

相关文章
相关标签/搜索