zoukankan      html  css  js  c++  java
  • 垃圾收集器与内存分配策略 (深入理解JVM二)

    1.概述

    GC需要考虑的三件事;

    1.哪些内存需要回收?

    2.什么时候回收?

    3.如何回收?

      我们知道程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊的入栈和出栈。每一个栈帧分配多少内存基本上是在类结构确定下来就知道的,因此这几个区域的内存分配和回收都具有确定性,在这几个区域就不需要过多的考虑回收的问题,因为方法结束或者线程结束,内存自然就回收了。而java堆和方法区则不一样,一个接口中的多个实现类可能需要的内存不一样,一个方法中的多个分支需要的内存可能不一样,我们只有在程序处于运行期间时才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器关注的也是这部分的内存。 

    2.哪些内存需要回收?

      在堆里面存放着java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是确定这些对象中哪些还活着,哪些已经死去(即不可能再被任何途径使用的对象)。

    1、引用计数法

      这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。看一段代码:

    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();
        }
    }

    看下运行结果:

    (1)加JVM参数-verbose:gc打印GC简要信息

    [GC (System.gc()) 6068K->656K(123904K), 0.0251712 secs]
    [Full GC (System.gc()) 656K->536K(123904K), 0.0045734 secs]

    (2)加 -XX:+PrintGC 打印GC的简要信息,同上面(1) 

    [GC (System.gc()) 6068K->672K(123904K), 0.0008420 secs]
    [Full GC (System.gc()) 672K->536K(123904K), 0.0046327 secs]

    (3)加 -XX:+PrintGCDetails 打印GC详细信息

    [GC [PSYoungGen: 5406K->568K(37888K)] 5406K->568K(123392K), 0.0299252 secs] [Times: user=0.00 sys=0.00, real=0.11 secs] 
    [Full GC [PSYoungGen: 568K->0K(37888K)] [ParOldGen: 0K->471K(85504K)] 568K->471K(123392K) [PSPermGen: 2561K->2560K(21504K)], 0.0264858 secs] [Times: user=0.02 sys=0.00, real=0.03 secs] 
    Heap
    PSYoungGen total 37888K, used 983K [0x00000007d6200000, 0x00000007d8c00000, 0x0000000800000000)
    eden space 32768K, 3% used [0x00000007d6200000,0x00000007d62f5cf8,0x00000007d8200000)
    from space 5120K, 0% used [0x00000007d8200000,0x00000007d8200000,0x00000007d8700000)
    to space 5120K, 0% used [0x00000007d8700000,0x00000007d8700000,0x00000007d8c00000)
    ParOldGen total 85504K, used 471K [0x0000000782600000, 0x0000000787980000, 0x00000007d6200000)
    object space 85504K, 0% used [0x0000000782600000,0x0000000782675c48,0x0000000787980000)
    PSPermGen total 21504K, used 2567K [0x000000077d400000, 0x000000077e900000, 0x0000000782600000)
    object space 21504K, 11% used [0x000000077d400000,0x000000077d681d08,0x000000077e900000)

    (4)加-XX:+PrintGCDetails -XX:+PrintGCTimeStamps 打印GC发生的时间戳

    0.094: [GC [PSYoungGen: 5406K->600K(37888K)] 5406K->600K(123392K), 0.0017123 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    0.096: [Full GC [PSYoungGen: 600K->0K(37888K)] [ParOldGen: 0K->471K(85504K)] 600K->471K(123392K) [PSPermGen: 2561K->2560K(21504K)], 0.0091825 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
    PSYoungGen total 37888K, used 983K [0x00000007d6200000, 0x00000007d8c00000, 0x0000000800000000)
    eden space 32768K, 3% used [0x00000007d6200000,0x00000007d62f5cf8,0x00000007d8200000)
    from space 5120K, 0% used [0x00000007d8200000,0x00000007d8200000,0x00000007d8700000)
    to space 5120K, 0% used [0x00000007d8700000,0x00000007d8700000,0x00000007d8c00000)
    ParOldGen total 85504K, used 471K [0x0000000782600000, 0x0000000787980000, 0x00000007d6200000)
    object space 85504K, 0% used [0x0000000782600000,0x0000000782675c48,0x0000000787980000)
    PSPermGen total 21504K, used 2567K [0x000000077d400000, 0x000000077e900000, 0x0000000782600000)
    object space 21504K, 11% used [0x000000077d400000,0x000000077d681d08,0x000000077e900000)

    (5)-XX:+PrintGCDetails -Xloggc:f:gc.log将GC信息输出到log日志文件:

    Java HotSpot(TM) 64-Bit Server VM (24.80-b11) for windows-amd64 JRE (1.7.0_80-b15), built on Apr 10 2015 11:26:34 by "java_re" with unknown MS VC++:1600
    Memory: 4k page, physical 8227852k(2748284k free), swap 18296072k(9431636k free)
    CommandLine flags: -XX:InitialHeapSize=131645632 -XX:MaxHeapSize=2106330112 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
    0.091: [GC [PSYoungGen: 5406K->536K(37888K)] 5406K->536K(123392K), 0.0019933 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    0.093: [Full GC [PSYoungGen: 536K->0K(37888K)] [ParOldGen: 0K->471K(85504K)] 536K->471K(123392K) [PSPermGen: 2561K->2560K(21504K)], 0.0117376 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
     PSYoungGen      total 37888K, used 983K [0x00000007d6200000, 0x00000007d8c00000, 0x0000000800000000)
      eden space 32768K, 3% used [0x00000007d6200000,0x00000007d62f5cf8,0x00000007d8200000)
      from space 5120K, 0% used [0x00000007d8200000,0x00000007d8200000,0x00000007d8700000)
      to   space 5120K, 0% used [0x00000007d8700000,0x00000007d8700000,0x00000007d8c00000)
     ParOldGen       total 85504K, used 471K [0x0000000782600000, 0x0000000787980000, 0x00000007d6200000)
      object space 85504K, 0% used [0x0000000782600000,0x0000000782675c48,0x0000000787980000)
     PSPermGen       total 21504K, used 2567K [0x000000077d400000, 0x000000077e900000, 0x0000000782600000)
      object space 21504K, 11% used [0x000000077d400000,0x000000077d681d08,0x000000077e900000)

      从上面的结果可以看到,两个对象相互引用着,但是虚拟机还是把这两个对象回收掉了,这也说明虚拟机并不是通过引用计数法来判定对象是否存活的。

    注意:运行的时候加参数-verbose:gc的方法。如果cmd运行直接加参数,如果是eclipse运行,配置如下:

    2、可达性分析法

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

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

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

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

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

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

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

    由图可知,obj8、obj9、obj10都没有到GCRoots对象的引用链,即便obj9和obj10之间有引用链,他们还是会被当成垃圾处理,可以进行回收。

    3. 四种引用状态

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

    1、强引用

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

    2、软引用

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

    3、弱引用

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

    4、虚引用

    这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。

    4. 生存还是死亡

      对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。

      1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法, 若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将 会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中, 之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程 去处理。

      2.对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋 值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。

    如下代码演示了一个对象如何在 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引用赋值给其他变量,这样堆中对象就可以被其他变量所引用,即不会被回收。

      finalize方法不适合做回收资源操作,因为它的运行代价昂贵,而且具有不确定性,无法保证各个对象的调用顺序。finalize能做的所有工作,使用try-finaly都可以做的更好。

    5.方法区的垃圾回收

      方法区的垃圾回收主要回收两部分内容:1. 废弃常量。2. 无用的类。既然进行垃圾回收,就需要判断哪些是废弃常量,哪些是无用的类。

      如何判断废弃常量呢?以字面量回收为例,如果一个字符串“abc”已经进入常量池,但是当前系统没有任何一个String对象引用了叫做“abc” 的字面量,那么,如果发生垃圾回收并且有必要时,“abc”就会被系统移出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

      如何判断无用的类呢?需要满足以下三个条件

    1. 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。

    2. 加载该类的ClassLoader已经被回收。

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

    满足以上三个条件的类可以进行垃圾回收,但是并不是无用就被回收。是否对类进行回收,HotSpot虚拟机提供了一些参数供我们配置。例如:-Xnoclassgc等参数

      在大量使用反射,动态代理、CGLIB、等byteCode框架中、动态生成JSP、以及OSGI等这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。  

    3. 垃圾收集算法

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

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

    2、复制(Copying)算法

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

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

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

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

    4、分代收集算法

    根据上面的内容,用一张图概括一下堆内存的布局

         现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。

     

    4. 垃圾收集器

    垃圾收集器就是上面讲的理论知识的具体实现了。不同虚拟机所提供的垃圾收集器可能会有很大差别,我们使用的是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、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收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择

    4、Serial Old收集器

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

    5、Parallel Old收集器

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

    6、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。

    7、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的集合。

    8.理解GC日志

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

    (1) 日志1

    解释:

      最前面的数字"33.125"和"100.667"代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。

       GC开头的"[GC"和"[Full GC"说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC。如果有“Full” 说明这个GC是发生了"stop-the-world"的GC。例如下面这段新生代收集器ParaNew的日志也会出现"[Full GC"(一般是因为分配担保失败的问题,才导致STW)。如果是调用System.gc()方法触发的GC,那么在这里将显示"[Full GC(System)"。

      

       接下来的"[DefNew"、"Tenured","Perm"表示GC发生的区域,这里显示的GC区域名称与使用的GC收集器有关。上面样例是使用Serial收集器的新生代名为"Default New Generation",所以显示的是"DefNew"。如果是ParaNew收集器,新生代会变会"ParNew",意为"Parallel New Generation"。如果采用Parallel Scavenge收集器会变为"PSYoungGen",老年代和永久带同理,名称也是由收集器决定的。

      后面方括号内部的 3324K->152K(3712K)的含义是"GC前该区域已使用容量->GC后该内存区域已使用容量(该区域总容量)"。而在方括号之外的"3324K->152K(11904K)"表示"GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)"。

      再往后,"0.0031680 secs" 表示该区域GC所占用的时间,单位是秒。有的收集器会给出具体的时间数据,如"[Times: user=0.01 sys=0.00, real=0.02secs ]",这里面的user、sys和real与linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间与墙钟时间的区别是:墙钟时间包括各种非运算的等待时间,例如等待磁盘的IO、等待线程阻塞,而CPU时间不包含这些,但是当系统有多CPU或多核的话,多线程会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是正常的。

    9. 垃圾收集器参数总结

    5. 内存分配与回收策略

      java体系的自动内存管理可以归结为两个问题:给对象分配内存以及回收分配给对象的内存。

      主要的几点规则如下:

     1.对象优先在Eden分配

       大多数情况下,对象在Eden区分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。

      如下使用SerialGC收集器进行测试,并指定JVM参数限定堆大小为20m,不可扩展,其中10m分配给新生代,剩下的10mb分配给老年代。-XX:SurvivorRatio=8指定一个Eden区与其中一个Survivor区的空间比例是8:1。从输出的结果也可以看到 Eden 8192, from 1024, to 1024;老年代的 tenured: 10240。

    代码:

    public class Client {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
            byte[] allocation1, allocation2, allocation3, allocation4;
            allocation1 = new byte[2 * _1MB];
            allocation2 = new byte[2 * _1MB];
            allocation3 = new byte[2 * _1MB];
    
            // 在这里发生依次GC
            allocation4 = new byte[4 * _1MB];
        }
    }

    结果:(加JVM参数   -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails  指定用SerialGC收集器,并指定JVM参数 )

    [GC[DefNew: 6991K->472K(9216K), 0.0048280 secs] 6991K->6616K(19456K), 0.0048707 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
    def new generation total 9216K, used 4734K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
    eden space 8192K, 52% used [0x00000000f9a00000, 0x00000000f9e297e0, 0x00000000fa200000)
    from space 1024K, 46% used [0x00000000fa300000, 0x00000000fa376080, 0x00000000fa400000)
    to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
    tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
    the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2566K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb081848, 0x00000000fb081a00, 0x00000000fc2c0000)
    No shared spaces configured.

      代码执行allocation4 = new byte[4 * _1MB];的时候发生一次Minor GC,这次GC的结果是新生代从6991K->472K,而总堆内存几乎没变,因为那三个对象都是存活的。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间不足以分配allocation4所需的4MB,因此发生MinorGC。GC期间又发现3个2MB的对象全部无法放入Survivor空间(Survivor空间只有1MB),所以只好通过分配担保机制提前转到老年代。

      这次GC结束后,4MB的allocation4对象顺利分配在Eden区域,因此程序执行完的结果是Eden占用4MB(allocation4),Survivor空闲,to空闲,老年代被占用6MB(allocation1,allocation2,allocation3)。

      通过上面的日志也发现:堆大小总共是9216K,也就是Eden区域(8M)加1个幸存者区(1M)的大小,虽然有两个幸存者区,但是每次只能使用1个,因此是9MB。

    补充:MinorGC和MajorGC(Full GC)的区别?

    新生代GC(minor GC):指的是发生在新生代的GC,因为JAVA对象都具备朝生夕灭的特点,因此MinorGC非常频繁,一般回收速度也比较快

    MajorGC(FullGC):指发生在老年代的GC,出现了MajorGC一般伴随至少一次的MinorGC。MajorGC的速度一般比MonorGC的速度慢10倍以上。

     

    2.  大对象直接进入老年代

      所谓的大对象是指,需要大量连续内存空间的JAVA对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机来说就是一个坏消息,经常出现大对象容易导致内存不足还有不少空间时就提前出发垃圾收集器以获取足够的空间来安置他们。

      虚拟机提供了一个 PretenureSizeThreshold 参数,大于这个参数值的对象将直接进入老年代。这样做的目的是避免在Eden区以及两个Survivor之间复制。

    上面的代码加运行时JVM参数为:  -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728

    Heap
    def new generation total 9216K, used 7154K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
    eden space 8192K, 87% used [0x00000000f9a00000, 0x00000000fa0fcbf0, 0x00000000fa200000)
    from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
    to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
    tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
    the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2566K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb081848, 0x00000000fb081a00, 0x00000000fc2c0000)
    No shared spaces configured.

      可以看到没有发生GC,allocation4直接被分配到老年代。

    3. 长期存活的对象将进入老年代

      既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应 放在新生代,哪些应该放在老年代中。为了做到这点,虚拟机给每个对象定义了一个年龄(Age)计数器。如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移到Survivor区域,并且对象年龄设为1。对象在Survivor区域每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定值(默认15岁),将会被晋升到老年代中。对象晋升到老年代的阀值,可以通过-XX:MaxTenuringThreshold 设置。

    代码如下:

    public class Client {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
            byte[] allocation1, allocation2, allocation3;
            allocation1 = new byte[_1MB / 4];
            System.out.println(1);
            allocation2 = new byte[4 * _1MB];
            System.out.println(2);
            allocation3 = new byte[4 * _1MB];
            System.out.println(3);
            allocation3 = null;
            System.out.println(4);
            allocation3 = new byte[4 * _1MB];
            System.out.println(5);
        }
    }

    JVM参数:-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1,结果如下:

    1
    2
    [GC[DefNew
    Desired survivor size 524288 bytes, new threshold 1 (max 1)
    - age 1: 746344 bytes, 746344 total
    : 5199K->728K(9216K), 0.0060252 secs] 5199K->4824K(19456K), 0.0060749 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
    3
    4
    [GC[DefNew
    Desired survivor size 524288 bytes, new threshold 1 (max 1)
    - age 1: 136 bytes, 136 total
    : 5001K->0K(9216K), 0.0016002 secs] 9097K->4824K(19456K), 0.0016263 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    5
    Heap
    def new generation total 9216K, used 4239K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
    eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e23d00, 0x00000000fa200000)
    from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200088, 0x00000000fa300000)
    to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
    tenured generation total 10240K, used 4824K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
    the space 10240K, 47% used [0x00000000fa400000, 0x00000000fa8b6160, 0x00000000fa8b6200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2566K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb081908, 0x00000000fb081a00, 0x00000000fc2c0000)
    No shared spaces configured.

       

      从以上日志看到,第二次GC的时候,allocation1和allocation2从新生代移到老年代,新生代从5001K变为0K。

    JVM参数:-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution,结果如下:

    1
    2
    [GC[DefNew
    Desired survivor size 524288 bytes, new threshold 1 (max 15)
    - age 1: 746344 bytes, 746344 total
    : 5199K->728K(9216K), 0.0043791 secs] 5199K->4824K(19456K), 0.0044172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    3
    4
    [GC[DefNew
    Desired survivor size 524288 bytes, new threshold 15 (max 15)
    - age 1: 136 bytes, 136 total
    : 5001K->0K(9216K), 0.0016693 secs] 9097K->4824K(19456K), 0.0016986 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    5
    Heap
    def new generation total 9216K, used 4239K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
    eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e23d00, 0x00000000fa200000)
    from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200088, 0x00000000fa300000)
    to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
    tenured generation total 10240K, used 4824K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
    the space 10240K, 47% used [0x00000000fa400000, 0x00000000fa8b6160, 0x00000000fa8b6200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2566K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb081908, 0x00000000fb081a00, 0x00000000fc2c0000)
    No shared spaces configured.

    4. 动态对象年龄判断

      为了更好地适应不同程序的内存情况,虚拟机并不永远地要求对象的年龄必须达到MaxTenuringThreshold中要求的年龄才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以用直接进入老年代,无需等到MaxTenuringThreshold要求的年龄。   

    5.空间分配担保

       那么当发生MinorGC时,JVM会首先检查老年代最大的可用连续空间是否大于新生代所有对象的总和,如果大于,那么这次YGC是安全的,如果不大于的话,JVM就需要判断HandlePromotionFailure是否允许空间分配担保。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时要进行一次FullGC。

      解释一下"冒险" :新生代采用的是复制收集算法,S0和S1始终只是用其中一块内存区,当出现YGC后大部分对象仍然存活的话,就需要老年代进行分配担保,把survior区无法容纳的对象直接晋升到老年代。那么这种空间分配担保的前提是老年代还有容纳的空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每次回收晋升到老年代对象容量的平均值大小作为经验值,与老年代的剩余空间比较,决定是否进行FGC来让老年代腾出更多空间。

      取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次MinorGC存活后的对象突增,远远高于平均值的话,依然会担保失败( Handle Promotion Failure)。如果出现担保失败,那就只好在失败后重新发起一次FullGC。虽然担保失败时绕的圈子最大,但是大部分情况还是会打开HandlePromotionFailure开关,防止FullGC过于频繁。

    测试代码:

    public class Client {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) {
            byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
            allocation1 = new byte[_1MB * 2];
            allocation2 = new byte[_1MB * 2];
            allocation3 = new byte[_1MB * 2];
    
            allocation1 = null;
    
            allocation4 = new byte[_1MB * 2];
            allocation5 = new byte[_1MB * 2];
            allocation6 = new byte[_1MB * 2];
    
            allocation4 = null;
            allocation5 = null;
            allocation6 = null;
    
            allocation7 = new byte[_1MB * 2];
        }
    }

    加JVM参数:-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+HandlePromotionFailure=false

    Java HotSpot(TM) 64-Bit Server VM warning: ignoring option HandlePromotionFailure=false; support was removed in 6.0_24
    [GC[DefNew: 6991K->472K(9216K), 0.0044849 secs] 6991K->4568K(19456K), 0.0045304 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC[DefNew: 6789K->471K(9216K), 0.0011929 secs] 10885K->4567K(19456K), 0.0012158 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
    def new generation total 9216K, used 2601K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
    eden space 8192K, 26% used [0x00000000f9a00000, 0x00000000f9c14820, 0x00000000fa200000)
    from space 1024K, 46% used [0x00000000fa200000, 0x00000000fa275ef0, 0x00000000fa300000)
    to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
    tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
    the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800020, 0x00000000fa800200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2566K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb0818a8, 0x00000000fb081a00, 0x00000000fc2c0000)
    No shared spaces configured.

    加JVM参数:-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+HandlePromotionFailure=true

    Java HotSpot(TM) 64-Bit Server VM warning: ignoring option HandlePromotionFailure=true; support was removed in 6.0_24
    [GC[DefNew: 6991K->472K(9216K), 0.0036154 secs] 6991K->4568K(19456K), 0.0036799 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC[DefNew: 6789K->471K(9216K), 0.0018142 secs] 10885K->4567K(19456K), 0.0018414 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
    Heap
    def new generation total 9216K, used 2601K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
    eden space 8192K, 26% used [0x00000000f9a00000, 0x00000000f9c14820, 0x00000000fa200000)
    from space 1024K, 46% used [0x00000000fa200000, 0x00000000fa275ef0, 0x00000000fa300000)
    to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
    tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
    the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800020, 0x00000000fa800200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2566K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb0818a8, 0x00000000fb081a00, 0x00000000fc2c0000)
    No shared spaces configured.

       可以看出上面日志忽略了HandlePromotionFailure配置,在JDK6Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则将进行FullGC。

    补充:static修饰的静态变量也会被GC,测试代码如下:

    public class ReferenceCountingGC {
    
        private static final int _1MB = 1024 * 1024;
    
        /** 这个成员属性唯一的作用就是占用一点内存 */
        private static byte[] bigSize = new byte[20 * _1MB];
    
        public static void main(String[] args) {
            // 将引用置空,可以被GC
            bigSize = null;
    
            System.gc();
        }
    }

    结果:(加-verbose:gc查看结果)

    [GC 21790K->568K(123392K), 0.0013350 secs]
    [Full GC 568K->471K(123392K), 0.0089632 secs]

    补充: 查看当前使用的垃圾回收器

    C:UsersAdministrator>java -XX:+PrintCommandLineFlags -version
    -XX:InitialHeapSize=131645632 -XX:MaxHeapSize=2106330112 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
    java version "1.8.0_121"
    Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

    -XX:+UseParallelGC 参数表示使用ParallelScavenge + Serial Old(PS MarkSweep)收集器组合

  • 相关阅读:
    Python subprocess方法
    Python hashlib、hmac加密模块
    mysql binlog详解
    Nginx 关键字详解
    账号笔记
    Python configparser模块
    Python yaml处理
    Linux && 与 ||
    spring boot 学习(十一)使用@Async实现异步调用
    spring boot 学习(十)SpringBoot配置发送Email
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/7725122.html
Copyright © 2011-2022 走看看