二、垃圾回收

垃圾回收

1. 如何判断对象可以回收/是垃圾

1.1 引用计数法

  • 早期python虚拟机使用这种方法
  • 存在2个互相引用,进而不能释放的问题
  • Java虚拟机没有采用这种方法

    1.2 可达性分析算法

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以 回收
  • 哪些对象可以作为 GC Root(是一系列对象) ?

Memory Analyzer(MAT) - eclipse提供的著名的专业的Java heap 分析工具

/**
 * 演示GC Roots,通过看list1没值和有值前后对比,看到底哪些对象可以作为根对象
 */
public class Demo2_2 {

    public static void main(String[] args) throws InterruptedException, IOException {
        List list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        System.out.println(1);
        System.in.read();//暂停

        list1 = null;
        System.out.println(2);
        System.in.read();//暂停
        System.out.println("end...");
    }
}
  1. 使用MAT先用jps看进程id
  2. 分别用jmap 在list1有值和null时抓取内存快照: jmap -dump:format=b,live,file=1.bin 21384
    • live参数表示只抓存活的,并且抓取之前会进行一次GC
    • format=b 表示格式为二进制
    • file=1.bin表示将抓取的数据存在当前文件夹下1.bin文件中
    • 21384 是进程ID
  3. 用MAT打开上面的2个快照文件
  4. 查看当前快照的GC Roots对象有哪些

  • GC Roots对象是堆中的对象,不是栈帧中的局部变量引用
  • Busy Monitor:被加锁的对象
  • System Class:系统类对象
  • Thread:活动线程栈帧内使用的对象,比如下图中list1引用的对象就是,方法参数引用的也是

1.3 四种引用

  1. 强引用 (StrongReference) :只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
    1. 软引用(SoftReference:
      • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象
      • 可以配合引用队列来释放软引用自身
    2. 弱引用(WeakReference:
      • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
      • 可以配合引用队列来释放弱引用自身
    3. 虚引用(PhantomReference: 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
    4. 终结器引用(FinalReference: 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
    例:软引用
    //加上虚拟机参数 -Xmx20m -XX:+PrintGCDetails -verbose:gc  再执行下面代码terminal中观察内存使用情况
    public class Demo2_3 {
        private static final int _4MB = 4 * 1024 * 1024;
         public static void main(String[] args) throws IOException {
            /*List list = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                list.add(new byte[_4MB]);
            }
            System.in.read();*/
            soft();
        }
    
        public static void soft() {
            // list --> SoftReference --> byte[]
            Listbyte[]>> list = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
                System.out.println(ref.get());
                list.add(ref);
                System.out.println(list.size());
            }
            System.out.println("循环结束:" + list.size());
            for (SoftReference<byte[]> ref : list) {
                System.out.println(ref.get());
            }
        }
    }
    • soft方法上面的代码会发生呢次溢出异常,而soft方法不会
    • 一些不重要的资源用SoftReference引用它,当空间紧张时,GC就可以把它回收掉

      2. 垃圾回收算法

2.1 标记清除

定义: Mark Sweep

  • 速度较快 会造成内存碎片

2.2 标记整理

定义:Mark Compact

  • 速度慢 没有内存碎片

2.3 复制

定义:Copy

  • 不会有内存碎片
  • 需要占用双倍内存空间

3. 分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world(STW),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长

3.1 相关 VM 参数

​ 含义 参数

  • 堆初始大小 -Xms

  • 堆最大大小 -Xmx 或 -XX:MaxHeapSize=size

  • 新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )

    • 括号中分别为新生代初始大小和最大大小
  • 幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

  • 幸存区比例 -XX:SurvivorRatio=ratio

    • eg:ratio为8,新生代为10MB,则伊甸园为8M,from to 各1M
  • 晋升阈值 -XX:MaxTenuringThreshold=threshold

    • 新生代晋升到老年代的阈值
  • 晋升详情 -XX:+PrintTenuringDistribution

  • GC详情 -XX:+PrintGCDetails -verbose:gc

  • FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

3.2 示例

/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
  //堆初始大小和最大大小都为20M,新生代10M(输出的新生代大小为9M,包含伊甸园和from区域)
  //UseSerialGC表示用这个垃圾回收器,JDK1.8默认不是这个
  //ScavengeBeforeFullGC:表示FullGC前进行一次GC,默认也是开启的
    public static void main(String[] args) throws InterruptedException {
       
    }
}

初始main函数为空,输出如下

  • 对于大对象,新生代放不下,老年代能放下,会直接放入老年代,不会触发GC

对面上面的类,若main函数如下

public static void main(String[] args) public static void main(String[] args) throws InterruptedException {
       	list.add(new byte[_8MB]);
}

输出

  • 如果新生代和老年代都放不下,会触发OOM
  • 一个线程OOM不会导致整个进程终止
  public static void main(String[] args)   public static void main(String[] args) throws InterruptedException {
      new Thread(() -> {
          ArrayList<byte[]> list = new ArrayList<>();
          list.add(new byte[_8MB]);
          list.add(new byte[_8MB]);
      }).start();
  		System.out.println("sleep....");
  		Thread.sleep(1000L);
}

4. 垃圾回收器

4.1. 串行

  • 单线程
  • 堆内存较小,适合个人电脑

4.2. 吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短(比如1H内发生2次,共 0.2S 0.2S = 0.4S),垃圾回收时间占比最低,这样就称吞吐量高

4.3 响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu 尽可能让单次 STW 的时间最短比如(1h内发生了5次gc,共 0.1 0.1 0.1 0.1 0.1 = 0.5s)

5. 垃圾回收调优


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

 目录