zoukankan      html  css  js  c++  java
  • Java学习之垃圾回收

    垃圾回收(GC)

    GC需要完成的三件事情:

    1. 哪些内存需要回收?
    2. 什么时候回收?
    3. 如何回收?

    为什么“GC自动化”之后还要研究GC?当需要排查各种内存溢出、内存泄漏问题时,当GC成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。

    计数算法

    package com.xiaoyu.chap3.GC;
    
    /**
     * Created by xiaoyu on 16/4/4.
     *
     * testGC()执行后,objA和objB会不会被GC呢?
     */
    public class ReferenceCountingGC {
    
        public Object instance = null;
        private static final int _1MB = 1024*1024;
    
        //搞个成员占点内存
        private byte[] bigSize = new byte[2*_1MB];
    
        public static void testGC(){
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
    
            System.gc();
        }
    
        public static void main(String[] args) {
            ReferenceCountingGC.testGC();
        }
    
    }
    
    output:
    [GC (Allocation Failure)  512K->440K(65024K), 0.0022170 secs]
    [GC (System.gc())  5037K->4656K(65536K), 0.0014100 secs]
    [Full GC (System.gc())  4656K->532K(65536K), 0.0074590 secs]
    

    从输出结果上可以看出,jvm并没有因为这两个对象互相引用而不回收它们,说明用的不是计数算法。

    可达性分析算法

    可作为“GC Roots的对象“

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI引用的对象

    引用

    四种引用强度:

    • 强引用:类似Object obj = new Object(),只要强引用还在,GC永远不会回收。
    • 软引用:有用但非必须。必要时,第二次回收。
    • 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
    • 虚引用:存在与否对对象无任何影响,唯一目的就是在这个对象被GC时收到一个系统通知。

    live or die?

    要真正宣告一个对象死亡,只要要经历两次标记过程。
    第一次标记:如果可达性分析后发现没有与GC Roots相连接的引用链,就会被第一次标记。
    第二次标记:如果第一次标记后,对象没有必要进行finalize()方法,则被第二次标记

    何为没有必要进行finalize()?

    1. 对象没有覆盖finalize()方法
    2. finalize()方法已经被虚拟机调用过

    如果”被认为有必要执行finalize()方法“,那么对象会被放置在F-Queue队列中,并由一个虚拟机生成的低优先级的Finalizer线程去执行它。

    finalize()缺点:运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不建议覆盖。

    判定一个常量是否无用:没有引用就是无用~
    判定一个类是否无用:1.Java堆中不存在该类的任何实例;2.加载该类的ClassLoader已经被回收;3.没有反射机制访问该类

    GC算法

    标记-清除算法:

    标记后清除。
    缺点:效率低,空间碎片太多

    复制算法:

    将内存等分,一次只用一边,每次内存回收时,把存货的对象复制到另一块,然后回收一整块。
    实现简单,运行高效,没有碎片问题
    缺点:需要将内存缩小为原来的一般

    标记-整理算法:

    标记如同标记清楚算法,后续把所有存活的对象往一端移动,然后直接清理掉边界外的内存。

    新生代死去的对象非常多,因此使用复制算法;老年代对象存活率高,因此使用标记算法。

    垃圾收集器

    HotSpot虚拟机的垃圾收集器:

    Serial收集器
    新生代虚拟机。
    单线程的收集器,在它进行GC时,必须暂停其他所有的工作线程(所谓的Stop The World)
    优点:简单而高效,由于没有线程交互的开销,因此专心做GC。。。对于运行在Client模式下的虚拟机是很好的选择
    新生代采用复制算法,暂停所有用户线程。(GC线程只有一个)

    ParNew收集器
    新生代虚拟机。
    多线程版本的Serial收集器。
    因为目前只有Serial和ParNew能和CMS收集器合作,因此它是很多Server模式的虚拟机的首选。
    新生代采用复制算法,暂停所有用户线程。老年代使用标记-整理算法,暂停所有用户线程。(GC线程有多个)

    Parallel Scavenge收集器
    新生代收集器。
    并行的多线程收集器,也是使用复制算法。
    Parallel Scavenge收集器的目的是达到一个可控制的吞吐量
    自适应调节策略

    并发与并行

    • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾手机程序运行于另一个CPU中。

    Serial Old收集器
    Serial收集器的老年代版本。使用标记-整理算法。

    Parallel Old收集器
    Parallel Scavenge收集器的老年代版本。
    使用多线程和标记-整理算法。
    用于和Parllel Scavenge收集器配合,达到“吞吐量优先”组合。

    CMS收集器
    CMS收集器是一种以获取最短回收停顿时间为目标的老年代收集器。

    • 初始标记(CMS initial mark)
    • 并发标记(CMS concurrent mark)
    • 重新标记(CMS remark)
    • 并发清除(CMS concurrent sweep)

    缺点:1.对CPU资源非常敏感,可能造成用户程序执行速度降低(采用过增加GC过程的时间,但是效果不好)
    2.CMS收集器无法处理浮动垃圾,由于CMS并发处理过程中用户进程还在运行,部分垃圾出现在标记结束之后,因此得等待下次GC,即所谓“浮动垃圾”。
    3.由于CMS使用的是“标记-清除”算法,因此会有大量的空间碎片。

    G1收集器
    当今收集器技术最前沿成果之一。

    特点:

    • 并行与并发:能充分利用多CPU,缩短STW停顿的时间,可以通过并发来让Java程序在GC时继续运行
    • 分代收集:G1可以不需要其他收集器配合就独立管理整个GC堆,但它能够采取不同方式来处理不同状态的对象
    • 空间整合:整体上看使用“标记-整理”算法,局部上看使用复制算法,因此不存在内存空间碎片问题
    • 可预测的停顿:除了追求低停顿外,还能建立可预测的停顿时间模型
    • 内存“化整为零“:将整个Java堆划分为多个大小相等的独立区域(Region),根据允许的收集时间优先收回价值最大的Region。通过Remembered Set技术来实现不同Region的对象引用问题

    G1运作步骤:

    1. 初始标记(Initial Marking)
    2. 并发标记(Concurrent Marking)
    3. 最终标记(Final Marking)
    4. 筛选回收(Live Data Counting and Evacuation)

    理解GC日志

    33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
    100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
    

    最前面的数字:GC发生的时间,从虚拟机启动以来经过的秒数.
    “[GC”和“[Full GC”:Full代表这次GC发生了STW.
    “[Defnew”等等:GC发生的区域,不同的收集器有不同的名称
    “3324K->152K(3712K)”:GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
    “3324K->152K(11904K)”:GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)
    “0.0025925sec”:该内存区域GC所占用的时间
    user、sys、real:用户态、内核态、操作开始到结束所经过的墙钟时间(包括各种如磁盘IO、等待线程阻塞等时间)

    垃圾收集器参数总结

    内存分配与回收策略

    对象优先在Eden分配(使用Serial/SerialOld收集器组合)

    package com.xiaoyu.chap3.GC;
    
    /**
     * Created by xiaoyu on 16/4/6.
     * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRation=8 UseSerialGC
     */
    public class TestAllocation {
    
        private static final int _1MB = 1024*1024;
    
        public static void testAllocation(){
            byte[] allocation1,allocation2,allocation3,allocation4;
    
            allocation1 = new byte[2 * _1MB];
            allocation2 = new byte[2 * _1MB];
            allocation3 = new byte[2 * _1MB];
            allocation4 = new byte[4 * _1MB];  //出现一次Minor GC
    
        }
    
        public static void main(String[] args) {
            TestAllocation.testAllocation();
        }
    }
    
    output:
    [GC (Allocation Failure) [DefNew: 7635K->533K(9216K), 0.0070160 secs] 7635K->6677K(19456K), 0.0070590 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 4931K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
      eden space 8192K,  53% used [0x00000007bec00000, 0x00000007bf04ba80, 0x00000007bf400000)
      from space 1024K,  52% used [0x00000007bf500000, 0x00000007bf5854b8, 0x00000007bf600000)
      to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
     tenured generation   total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
       the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
     Metaspace       used 3056K, capacity 4494K, committed 4864K, reserved 1056768K
      class space    used 336K, capacity 386K, committed 512K, reserved 1048576K
    

    从上面可以看出:
    ①前6MB数据分配到Eden区后,Eden区所剩的内存已经不足以分配allocation4了,因此发生MinorCG
    ②MinorGC之后虚拟机发现已有的3个2MB大小的对象无法放入Survivor空间,因此只能通过分配担保机制提前转移到老年代。
    ③GC结束后,allocation4被分配在Eden区,Survivor空闲,老年代被占用6MB(allocation1、2、3)

    大对象直接进入老年代

    package com.xiaoyu.chap3.GC;
    
    /**
     * Created by xiaoyu on 16/4/6.
     * VM args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
     * -XX:PretenureSizeThreshold参数令大小大于设定值的对象直接在老年代分配
     */
    public class TestPretenureSizeThreshold {
    
        private static final int _1MB = 1024*1024;
        
        public static void testPretenureSizeThreshould(){
            byte[] allocation;
            allocation = new byte[4*_1MB];
        }
    
        public static void main(String[] args) {
            testPretenureSizeThreshould();
        }
    
    }
    output:
    Heap
     def new generation   total 9216K, used 1655K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
      eden space 8192K,  20% used [0x00000007bec00000, 0x00000007bed9dd60, 0x00000007bf400000)
      from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
      to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
     tenured generation   total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
       the space 10240K,  40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
     Metaspace       used 3033K, capacity 4494K, committed 4864K, reserved 1056768K
      class space    used 334K, capacity 386K, committed 512K, reserved 1048576K
    
    

    ①可以发现,allocation对象直接被分配到了老年代中。
    ②PretenureSizeThreshold参数只对Serial和ParNew收集器有效!

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

    package com.xiaoyu.chap3.GC;
    
    /**
     * Created by xiaoyu on 16/4/6.
     *
     * VM args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:UseSerialGC
     * -XX:MaxTenuringThreshold来设置对象晋升老年代的年龄阈值
     */
    
    public class TestTenuringThreshold {
    
        private static final int _1MB  = 1024*1024;
    
        @SuppressWarnings("unused")
        public static void testYenuringThreshold(){
            byte[] allocation1,allocation2,allocation3;
            allocation1 = new byte[_1MB/4];
    
            //什么时候进入老年代取决于XX:MaxTenuringThreshold设置
            allocation2 = new byte[_1MB*4];
            allocation3 = new byte[_1MB*4];
            allocation3 = null;
            allocation3 = new byte[_1MB*4];
        }
    
        public static void main(String[] args) {
            testYenuringThreshold();
        }
    
    }
    
    

    ①MaxTenuringThreshold=1时,allocation1对象在第二次GC时就会进入老年代,新生代已使用的内存GC后会就变为0KB
    ②MaxTenuringThreshold=15时,allocation1对象在第二次GC后还会留在Survivor。

    动态对象年龄判断

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代。

    总结

    内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。

  • 相关阅读:
    C++笔记(2018/2/6)
    2017级面向对象程序设计寒假作业1
    谁是你的潜在朋友
    A1095 Cars on Campus (30)(30 分)
    A1083 List Grades (25)(25 分)
    A1075 PAT Judge (25)(25 分)
    A1012 The Best Rank (25)(25 分)
    1009 说反话 (20)(20 分)
    A1055 The World's Richest(25 分)
    A1025 PAT Ranking (25)(25 分)
  • 原文地址:https://www.cnblogs.com/xiaoYu3328/p/5360996.html
Copyright © 2011-2022 走看看