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

总结

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

原文地址:https://www.cnblogs.com/xiaoYu3328/p/5360996.html