• 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空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代。

    总结

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

  • 相关阅读:
    rCore-Tutorial-Book-v3学习笔记(五)
    rCore-Tutorial-Book-v3学习笔记(四)
    rCore-Tutorial-Book-v3学习笔记(三)
    rCore-Tutorial-Book-v3学习笔记(二)
    rCore-Tutorial-Book-v3学习笔记(一)
    OpenStack 命令行速查表(转载)
    mysql数据库总结笔记
    Owncloud迁移上云案例
    Python
    高等数学(微积分)
  • 原文地址:https://www.cnblogs.com/xiaoYu3328/p/5360996.html
Copyright © 2020-2023  润新知