JVM垃圾回收的时候如何确定垃圾?是否知道 什么是GC Roots

什么是垃圾

简单来说就是内存中已经不再被使用的空间就是垃圾

如何判断一个对象是否可以被回收

引用计数法

Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。

因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器

每当有一个地方引用它,计数器值加1

每当有一个引用失效,计数器值减1

任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。

那么为什么主流的Java虚拟机里面都没有选用这个方法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题

该算法存在但目前无人用了,解决不了循环引用的问题,了解即可。

image-20200318213301603

枚举根节点做可达性分析

根搜索路径算法

为了解决引用计数法的循环引用个问题,Java使用了可达性分析的方法:

image-20200319113611244

所谓 GC Roots 或者说 Tracing Roots的“根集合” 就是一组必须活跃的引用

基本思路就是通过一系列名为 GC Roots的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的对象就被判定为死亡

image-20200319114526625

必须从GC Roots对象开始,这个类似于linux的 / 也就是根目录

蓝色部分是从GC Roots出发,能够循环可达

而白色部分,从GC Roots出发,无法到达

一句话理解GC Roots

假设我们现在有三个实体,分别是 人,狗,毛衣

然后他们之间的关系是:人 牵着 狗,狗穿着毛衣,他们之间是强连接的关系

有一天人消失了,只剩下狗狗 和 毛衣,这个时候,把人想象成 GC Roots,因为 人 和 狗之间失去了绳子连接,

那么狗可能被回收,也就是被警察抓起来,被送到流浪狗寄养所

假设狗和人有强连接的时候,狗狗就不会被当成是流浪狗

那些对象可以当做GC Roots 🤔

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

代码说明

/**
 * 在Java中,可以作为GC Roots的对象有:
 * - 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
 * - 方法区中的类静态属性引用的对象
 * - 方法区中常量引用的对象
 * - 本地方法栈中的JNI(Native方法)的引用对象
 */
public class GCRootDemo {

  	// 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
  	private byte[] byteArray = new byte[100 * 1024 * 1024];

    // 方法区中的类静态属性引用的对象
    private static GCRootDemo2 t2;

    // 方法区中的常量引用,GC Roots 也会以这个为起点,进行遍历
    private static final GCRootDemo3 t3 = new GCRootDemo3(8);

    public static void m1() {
        // 第一种,虚拟机栈中的引用对象
        GCRootDemo t1 = new GCRootDemo();
        System.gc();
        System.out.println("第一次GC完成");
    }
    public static void main(String[] args) {
        m1();
    }
}

Java中的引用

前言

在原来的时候,我们谈到一个类的实例化

Person p = new Person()

在等号的左边,就是一个对象的引用,存储在栈中

而等号右边,就是实例化的对象,存储在堆中

其实这样的一个引用关系,就被称为强引用

整体架构

image-20200323155120778

强引用

当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,打死也不回收~!

强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用于或者显示地将相应(强)引用赋值为null,一般可以认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾回收策略)

强引用小例子:

/**
 * 强引用
 */
public class StrongReferenceDemo {

    public static void main(String[] args) {
        // 这样定义的默认就是强应用
        Object obj1 = new Object();

        // 使用第二个引用,指向刚刚创建的Object对象
        Object obj2 = obj1;

        // 置空
        obj1 = null;

        // 垃圾回收
        System.gc();

        System.out.println(obj1);
        System.out.println(obj2);
    }
}

输出结果我们能够发现,即使 obj1 被设置成了null,然后调用gc进行回收,但是也没有回收实例出来的对象,obj2还是能够指向该地址,也就是说垃圾回收器,并没有将该对象进行垃圾回收

null
java.lang.Object@14ae5a5

软引用

软引用是一种相对弱化了一些的引用,需要用Java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲:

  • 当系统内存充足时,它不会被回收
  • 当系统内存不足时,它会被回收

软引用通常在对内存敏感的程序中,比如高速缓存就用到了软引用,内存够用的时候就保留,不够用就回收

具体使用

/**
 * 软引用
 */
public class SoftReferenceDemo {

    /**
     * 内存够用的时候
     */
    public static void softRefMemoryEnough() {
        // 创建一个强应用
        Object o1 = new Object();
        // 创建一个软引用
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        // 手动GC
        System.gc();

        System.out.println(o1);
        System.out.println(softReference.get());
    }

    /**
     * JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
     * -Xms5m -Xmx5m -XX:+PrintGCDetails
     */
    public static void softRefMemoryNoEnough() {

        System.out.println("========================");
        // 创建一个强应用
        Object o1 = new Object();
        // 创建一个软引用
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;

        // 模拟OOM自动GC
        try {
            // 创建30M的大对象
            byte[] bytes = new byte[30 * 1024 * 1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(o1);
            System.out.println(softReference.get());
        }

    }

    public static void main(String[] args) {

        softRefMemoryEnough();

        softRefMemoryNoEnough();
    }
}

我们写了两个方法,一个是内存够用的时候,一个是内存不够用的时候

我们首先查看内存够用的时候,首先输出的是 o1 和 软引用的 softReference,我们都能够看到值

然后我们把o1设置为null,执行手动GC后,我们发现softReference的值还存在,说明内存充足的时候,软引用的对象不会被回收

java.lang.Object@14ae5a5
java.lang.Object@14ae5a5

[GC (System.gc()) [PSYoungGen: 1396K->504K(1536K)] 1504K->732K(5632K), 0.0007842 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 228K->651K(4096K)] 732K->651K(5632K), [Metaspace: 3480K->3480K(1056768K)], 0.0058450 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

null
java.lang.Object@14ae5a5

下面我们看当内存不够的时候,我们使用了JVM启动参数配置,给初始化堆内存为5M

-Xms5m -Xmx5m -XX:+PrintGCDetails

但是在创建对象的时候,我们创建了一个30M的大对象

// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];

这就必然会触发垃圾回收机制,这也是中间出现的垃圾回收过程,最后看结果我们发现,o1 和 softReference都被回收了,因此说明,软引用在内存不足的时候,会自动回收

java.lang.Object@7f31245a
java.lang.Object@7f31245a

[GC (Allocation Failure) [PSYoungGen: 31K->160K(1536K)] 682K->811K(5632K), 0.0003603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 160K->96K(1536K)] 811K->747K(5632K), 0.0006385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 651K->646K(4096K)] 747K->646K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0067976 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 646K->646K(5632K), 0.0004024 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 646K->627K(4096K)] 646K->627K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0065506 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

null
null

弱引用

不管内存是否够,只要有GC操作就会进行回收

弱引用需要用 java.lang.ref.WeakReference 类来实现,它比软引用生存期更短

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的空间。

/**
 * 弱引用
 */
public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> weakReference = new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(weakReference.get());
        o1 = null;
        System.gc();
        System.out.println(o1);
        System.out.println(weakReference.get());
    }
}

我们看结果,能够发现,我们并没有制造出OOM内存溢出,而只是调用了一下GC操作,垃圾回收就把它给收集了

java.lang.Object@14ae5a5
java.lang.Object@14ae5a5

[GC (System.gc()) [PSYoungGen: 5246K->808K(76288K)] 5246K->816K(251392K), 0.0008236 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->675K(175104K)] 816K->675K(251392K), [Metaspace: 3494K->3494K(1056768K)], 0.0035953 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

null
null

软引用和弱引用的使用场景

场景:假如有一个应用需要读取大量的本地图片

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中,又可能造成内存溢出

此时使用软引用可以解决这个问题

设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题

Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

WeakHashMap是什么?

比如一些常常和底层打交道的,mybatis等,底层都应用到了WeakHashMap

WeakHashMap和HashMap类似,只不过它的Key是使用了弱引用的,也就是说,当执行GC的时候,HashMap中的key会进行回收,下面我们使用例子来测试一下

我们使用了两个方法,一个是普通的HashMap方法

我们输入一个Key-Value键值对,然后让它的key置空,然后在查看结果

private static void myHashMap() {
  Map<Integer, String> map = new HashMap<>();
  Integer key = new Integer(1);
  String value = "HashMap";

  map.put(key, value);
  System.out.println(map);

  key = null;

  System.gc();

  System.out.println(map);
}

第二个是使用了WeakHashMap,完整代码如下

/**
 * WeakHashMap
 */
public class WeakHashMapDemo {
    public static void main(String[] args) {
        myHashMap();
        System.out.println("==========");
        myWeakHashMap();
    }

    private static void myHashMap() {
        Map<Integer, String> map = new HashMap<>();
        Integer key = new Integer(1);
        String value = "HashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;

        System.gc();

        System.out.println(map);
    }

    private static void myWeakHashMap() {
        Map<Integer, String> map = new WeakHashMap<>();
        Integer key = new Integer(1);
        String value = "WeakHashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;

        System.gc();

        System.out.println(map);
    }
}

最后输出结果为:

{1=HashMap}
{1=HashMap}
==========
{1=WeakHashMap}
{}

从这里我们看到,对于普通的HashMap来说,key置空并不会影响,HashMap的键值对,因为这个属于强引用,不会被垃圾回收。

但是WeakHashMap,在进行GC操作后,弱引用的就会被回收

虚引用

概念

虚引用又称为幽灵引用,需要java.lang.ref.PhantomReference 类来实现

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列ReferenceQueue联合使用

虚引用的主要作用和跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。

PhantomReference的get方法总是返回null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作

**换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理**,Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前,做必要的清理工作

这个就相当于Spring AOP里面的后置通知

场景

一般用于在回收时候做通知相关操作

引用队列 ReferenceQueue

软引用,弱引用,虚引用在回收之前,需要在引用队列保存一下

我们在初始化的弱引用或者虚引用的时候,可以传入一个引用队列

Object o1 = new Object();

// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);

那么在进行GC回收的时候,弱引用和虚引用的对象都会被回收,但是在回收之前,它会被送至引用队列中

完整代码如下:

/**
 * 虚引用
 */
public class PhantomReferenceDemo {

    public static void main(String[] args) {

        Object o1 = new Object();

        // 创建引用队列
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

        // 创建一个弱引用
        WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);

        // 创建一个虚引用
//        PhantomReference<Object> weakReference = new PhantomReference<>(o1, referenceQueue);

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取队列中的内容
        System.out.println(referenceQueue.poll());

        o1 = null;
        System.gc();
        System.out.println("执行GC操作");

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取队列中的内容
        System.out.println(referenceQueue.poll());

    }
}

运行结果

java.lang.Object@14ae5a5
java.lang.Object@14ae5a5
null
执行GC操作
null
null
java.lang.ref.WeakReference@7f3124

从这里我们能看到,在进行垃圾回收后,我们弱引用对象,也被设置成null,但是在队列中还能够导出该引用的实例,这就说明在回收之前,该弱引用的实例被放置引用队列中了,我们可以通过引用队列进行一些后置操作

小结

java提供了4种引用类型,在垃圾回收的时候,都有自己各自的特点。

ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。

创建引用的时候可以指定关联的队列,当GC 释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于是一种通知机制。

当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。

GCRoots和四大引用小总结

  • 红色部分在垃圾回收之外,也就是强引用的

  • 蓝色部分:属于软引用,在内存不够的时候,才回收
  • 虚引用和弱引用:每次垃圾回收的时候,都会被干掉,但是它在干掉之前还会存在引用队列中,我们可以通过引用队列进行一些通知机制

image-20200324123829937

垃圾收集器

GC垃圾回收算法和垃圾收集器关系

天上飞的理念,要有落地的实现(垃圾收集器就是GC垃圾回收算法的实现)

GC算法是内存回收的方法论,垃圾收集器就是算法的落地实现

GC算法主要有以下几种

  • 引用计数(几乎不用,无法解决循环引用的问题)
  • 复制拷贝(用于新生代)
  • 标记清除(用于老年代)
  • 标记整理(用于老年代)

因为目前为止还没有完美的收集器出现,更没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集(那个代用什么收集器)

四种主要的垃圾收集器

  • Serial:串行回收 -XX:+UseSeriallGC
  • Parallel:并行回收 -XX:+UseParallelGC
  • CMS:并发标记清除
  • G1
  • ZGC:(java 11 出现的)

image-20200325084453631

Serial

串行垃圾回收器,它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境

image-20200325085320683

Parallel

并行垃圾收集器,多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和 Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短

image-20200325085729428

CMS

并发标记清除,用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。并发是可以有交互的,也就是说可以一边进行收集,一边执行应用程序。

image-20200325090858921

G1

G1垃圾回收器将堆内存分割成不同区域,然后并发的进行垃圾回收

image-20200325093222711

垃圾收集器总结

注意:并行垃圾回收在单核CPU下可能会更慢

image-20200325091619082

查看默认垃圾收集器

使用下面JVM命令,查看配置的初始参数

-XX:+PrintCommandLineFlags

然后运行一个程序后,能够看到它的一些初始配置信息

-XX:InitialHeapSize=266376000 -XX:MaxHeapSize=4262016000 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

移动到最后一句,就能看到 -XX:+UseParallelGC 说明使用的是并行垃圾回收

-XX:+UseParallelGC

默认垃圾收集器有哪些

Java中一共有7大垃圾收集器

  • UseSerialGC:串行垃圾收集器
  • UseSerialOldGC:串行老年代垃圾收集器(已经被移除)
  • UseParallelGC:并行垃圾收集器
  • UseParNewGC:年轻代的并行垃圾回收器
  • UseParallelOldGC:老年代的并行垃圾回收器
  • UseConcMarkSweepGC:(CMS)并发标记清除,用于老年代
  • UseG1GC:G1垃圾收集器

底层源码

image-20200325100653829

各垃圾收集器的使用范围

image-20200325101451849

新生代使用的:

  • Serial Copying: UserSerialGC,串行垃圾回收器
  • Parallel Scavenge:UserParallelGC,并行垃圾收集器
  • ParNew:UserParNewGC,新生代并行垃圾收集器

老年区使用的:

  • Serial Old:UseSerialOldGC,老年代串行垃圾收集器
  • Parallel Compacting(Parallel Old):UseParallelOldGC,老年代并行垃圾收集器
  • CMS:UseConcMarkSwepp,并行标记清除垃圾收集器

各区都能使用的:

G1:UseG1GC,G1垃圾收集器

垃圾收集器就来具体实现这些GC算法并实现内存回收,不同厂商,不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:

image-20200325102329216

部分参数说明

  • DefNew:Default New Generation
  • Tenured:Old
  • ParNew:Parallel New Generation
  • PSYoungGen:Parallel Scavenge
  • ParOldGen:Parallel Old Generation

Java中的Server和Client模式

使用范围:一般使用Server模式,Client模式基本不会使用

操作系统

  • 32位的Window操作系统,不论硬件如何都默认使用Client的JVM模式
  • 32位的其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
  • 64位只有Server模式

image-20200325175208231

新生代下的垃圾收集器

串行GC(Serial)

串行GC(Serial)/(Serial old)

  • 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束,适合于单核cpu的场景。所以,串行回收默认被应用在客户端的Client模式下的 JVM中。
  • Serial收集器采用复制算法、串行回收和”stop-the-world”机制的方式执行内存回收。是HotSpot中client模式下的默认新生代垃圾回收器。
  • Serial old收集器也采用了同样的机制,只不过内存回收算法使用的是标记-整理算法。同样也是HostSpot虚拟机默认的老年代垃圾回收器。

image-20200325175704604

串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在垃圾收集过程中可能会产生较长的停顿(Stop-The-World 状态)。 虽然在收集垃圾过程中需要暂停所有其它的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器。

对应JVM参数是:-XX:+UseSerialGC

开启后会使用:Serial(Young区用) + Serial Old(Old区用) 的收集器组合

表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialGC

并行GC(ParNew)

并行收集器,使用多线程进行垃圾回收,在垃圾收集,会Stop-the-World暂停其他所有的工作线程直到它收集结束

image-20200325191328733

ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景时配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。

常见对应JVM参数:-XX:+UseParNewGC 启动ParNew收集器,只影响新生代的收集,不影响老年代

开启上述参数后,会使用:ParNew(Young区用) + Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParNewGC

但是会出现警告,即 ParNew 和 Serial Old 这样搭配,Java8已经不再被推荐

image-20200325194316660

备注: -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数

并行回收GC(Parallel)/ (Parallel Scavenge)

因为Serial 和 ParNew都不推荐使用了,因此现在新生代默认使用的是Parallel Scavenge,也就是新生代和老年代都是使用并行

image-20200325204437678

Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器一句话:串行收集器在新生代和老年代的并行化

它关注的重点是:

可控制的吞吐量(Thoughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) ),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%。高吞吐量意味着高效利用CPU时间,它多用于在后台运算而不需要太多交互的任务。

自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间( -XX:MaxGCPauseMills))或最大的吞吐量。

常用JVM参数:-XX:+UseParallelGC-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器

开启该参数后:新生代使用复制算法,老年代使用标记-整理算法

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParallelGC

老年代下的垃圾收集器

串行GC(Serial Old)

Serial Old是Serial垃圾收集器老年代版本,它同样是一个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的Java虚拟机中默认的老年代垃圾收集器

在Server模式下,主要有两个用途(了解,版本已经到8及以后)

  • 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用(Parallel Scavenge + Serial Old)
  • 作为老年代版中使用CMS收集器的后备垃圾收集方案。

配置方法:

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialOldlGC

该垃圾收集器,目前已经不推荐使用了

并行GC(Parallel Old)

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。

在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6以前(Parallel Scavenge + Serial Old)

Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以考虑新生代Parallel Scavenge和老年代Parallel Old 收集器的搭配策略。在JDK1.8及后(Parallel Scavenge + Parallel Old)

JVM常用参数:

-XX +UseParallelOldGC:使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代 Parallel Old

image-20200325211525152

使用老年代并行收集器:

-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParallelOldlGC

并发标记清除GC(CMS)

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以最短回收停顿时间为目标的收集器

适合应用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

CMS非常适合堆内存大,CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。

image-20200325212836441

Concurrent Mark Sweep:并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行

开启该收集器的JVM参数: -XX:+UseConcMarkSweepGC 开启该参数后,会自动将 -XX:+UseParNewGC打开,开启该参数后,使用ParNew(young 区用)+ CMS(Old 区用) + Serial Old 的收集器组合,⚠️Serial Old将作为CMS出错的后备收集器

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC

四个步骤

为了尽可能的缩短垃圾收集时用户线程的停顿时间,HotSpot推出了CMS收集器,它是第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。他是一款老年代的收集器,采用了标记-清除算法,并且也会STW,并且无法和新生代收集器Parallel Scavenge配合使用。他的收集过程主要分为初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段4个主要阶段:

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快
  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  • 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。 (❗️重新标记的是并发标记阶段所标记过的垃圾)
  • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

image-20200325215401981

优点:

  • 并发收集,低延迟
  • 尽管CMS收集器采用的是并发回收,但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

缺点:

  • 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发FullGC。
  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
  • 以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial old回收器作为补偿措施当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用serial old执行FullGC以达到对老年代内存的整理。
  • CMS也提供了参数 -XX:CMSFullGCSBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC

三色标记法

  • 三色标记算法在CMS/G1的并发标记阶段使用的:
  • CMS将对象标记为三种颜色:
    • 黑色:对象本身及所有的引用都被扫描过
    • 灰色:对象本身被扫描,至少还存在一个引用没被扫描
    • 白色:没有被GC扫描,扫描结束后白色的可以被回收。
  • 标记过程:
    • 一开始所有对象都是白色,然后将GCRoots直接关联的对象置为灰色
    • 遍历灰色对象的所有引用,完成后把自己置为黑色,引用置为灰色。重复此步骤知道没有灰色对象
    • 结束,黑色对象存活,白色对象回收
  • 该算法会产生漏标,在对象被标记为灰色后该对象被其他线程被置空。会导致产生浮动垃圾,直到下一轮GC才会被回收。可以通过「写屏障」来解决,只要在A写B的时候加入写屏障,记录下B被切断的记录,重新标记时可以再把他们标为白色即可。
    • 这里的写屏障指的是属性赋值的前后加入一些处理,类似于AOP。
  • 还有可能产生错标,对象被标记为白色后又建立了引用。可以用原始快照和增量更新来解决。
    • G1解决方案:原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。相当于无论引用关系是否删除,都会按照刚开始扫描时那一瞬间的对象图快照来扫描。
    • CMS解决方案:增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。

image-20221220210708403


为什么新生代采用复制算法,老年代采用标整算法

新生代使用复制算法

因为新生代对象的生存时间比较短,80%的都要回收的对象,采用标记-清除算法则内存碎片化比较严重,采用复制算法可以灵活高效,且便与整理空间。

老年代采用标记整理

标记整理算法主要是为了解决标记清除算法存在内存碎片的问题,又解决了复制算法两个Survivor区的问题,因为老年代的空间比较大,不可能采用复制算法,特别占用内存空间

垃圾收集器如何选择 🤔

组合的选择

  • 单CPU或者小内存,单机程序
    • -XX:+UseSerialGC
  • 多CPU,需要最大的吞吐量,如后台计算型应用
    • -XX:+UseParallelGC(这两个相互激活)
    • -XX:+UseParallelOldGC
  • 多CPU,追求低停顿时间,需要快速响应如互联网应用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC
参数 新生代垃圾收集器 新生代算法 老年代垃圾收集器 老年代算法
-XX:+UseSerialGC SerialGC 复制 SerialOldGC 标记整理
-XX:+UseParNewGC ParNew 复制 SerialOldGC 标记整理
-XX:+UseParallelGC Parallel [Scavenge] 复制 Parallel Old 标记整理
-XX:+UseConcMarkSweepGC ParNew 复制 CMS + Serial Old的收集器组合,Serial Old作为CMS出错的后备收集器 标记清除
-XX:+UseG1GC G1整体上采用标记整理算法 局部复制    

G1垃圾收集器

image-20200326115120405

开启G1垃圾收集器

-XX:+UseG1GC

以前收集器的特点

  • 年轻代和老年代是各自独立且连续的内存块
  • 年轻代收集使用单eden + S0 + S1 进行复制算法
  • 老年代收集必须扫描整个老年代区域
  • 都是以尽可能少而快速地执行GC为设计原则

G1是什么

Garbage-First 收集器,是一款面向服务端应用的收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求。另外,它还具有一下特征:

  • 像CMS收集器一样,能与应用程序并发执行
  • 整理空闲空间更快
  • 需要更多的时间来预测GC停顿时间
  • 不希望牺牲大量的吞吐量性能
  • 不需要更大的Java Heap

G1收集器设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换Java8以前的CMS收集器。

主要改变:Eden,Survivor和Tenured等内存区域不再是连续了,而是变成一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代,大对象H区。他会在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。由于这种方式的侧重点在于回收垃圾最大量的区间(Region)面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高。GC的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC(Young GC):应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
  • 老年代并发标记过程:当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
  • 混合回收(Mixed GC):标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

特点

优点:

  • 并行与并发

    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW

    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况

  • 分代收集

    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。

    • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。

    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代;

缺点:

  • 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
  • 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

底层原理

Region区域化垃圾收集器,化整为零,打破了原来新生区和老年区的壁垒,避免了全内存扫描,只需要按照区域来进行扫描即可。

区域化内存划片Region,整体遍为了一些列不连续的内存区域,避免了全内存区的GC操作。

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置子区域大小

在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048 = 64G内存

Region区域化垃圾收集器

Region区域化垃圾收集器

G1将新生代、老年代的物理空间划分取消了

image-20200326120105859

同时对内存进行了区域划分

image-20200326120130427

G1算法将堆划分为若干个区域(Reign),它仍然属于分代收集器。

这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间

这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片的问题存在了

在G1中,还有一种特殊的区域,叫做Humongous(巨大的)区域,如果一个对象占用了空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象,这些巨型对象默认直接分配在老年代,但是如果他是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H区来存储,为了能找到连续的H区,有时候不得不启动Full GC。

回收步骤

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内碎片

  • Eden区的数据移动到Survivor区,加入出现Survivor区空间不够,Eden区数据会晋升到Old区
  • Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行

image-20200326121409237

回收完成后

image-20200326121622208

小区域收集 + 形成连续的内存块,最后在收集完成后,就会形成连续的内存空间,这样就解决了内存碎片的问题

具体回收过程

G1垃圾回收过程的话,主要包括三个环节。首先当年轻代的Eden区用尽时,会开始年轻代GC,这个阶段是并行且独占的,他会暂停所有用户线程然后启动多个线程执行年轻代回收。然后当堆内存达到45%的时候,会开始老年代并发标记的过程,标记完成后就会开始混合回收的过程,混合回收既会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收,优先回收垃圾比例比较高的区域。如果上述方式不能正常工作就会产生full gc,比如堆内存太小导致复制时没有多余的内存分段可用,G1会停止应用程序的执行并使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

  • 年轻代GC的具体回收过程,根据GCRoot和Remember Set来标记存活对象,存活的对象会被复制到Survivor区中空的内存分段,另外如果Survivor区内存段中存活的对象如果年龄达到了阈值则会被复制到old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
  • 并发标记的过程则主要针对老年代,和CMS比较类似,首先也是初始标记阶段(stw),标记从根节点直接可达的对象,并且会触发一次年轻代GC。然后是根区域扫描阶段,年轻代GC后所有存活对象大多都移动到了survivor区,然后这个阶段会扫描survivor区直接可达的老年代区域对象并标记为存活对象(主要还是为了更新堆区域之间的相互引用Remember Set)。然后进入并发标记阶段,并且会计算每个区域的对象活性,即存活对象的比例。另外由于是和用户线程一起进行的,所以还需要stw并重新标记(stw)修正上一次标记的结果,G1的重新标记会用的初始快照算法SATB来进行,比CMS效率更高。然后就是独占清理阶段(stw),这个阶段会根据存活对象的比例对分区进行排序,识别可供混合回收的区域并更新了Remember Set。最后进入并发清除阶段,识别并清理完全空闲的区域。
  • ps:区域内用的是标记压缩,区域间用的是复制算法。

G1 GC的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)

(如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)

image-20221220211646673

顺时针,young gc->young gc+concurrent mark->Mixed GC顺序,进行垃圾回收。

  1. 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
  2. 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
  3. 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

🧐 举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。

Remembered Set(记忆集)避免全局扫描

一个对象被不同区域引用的问题

一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

解决方法:

  • 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
  • 每个Region都有一个对应的Remembered Set
  • 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
  • 如果不同,把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。

参数配置

开发人员仅仅需要申明以下参数即可

三步归纳:-XX:+UseG1GC -Xmx32G -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM尽可能停顿小于这个时间

G1和CMS比较

  • G1不会产生内碎片
  • 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

SpringBoot结合JVMGC

启动微服务时候,就可以带上JVM和GC的参数

  • IDEA开发完微服务工程
  • maven进行clean package
  • 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数
    • 我们就可以根据具体的业务配置我们启动的JVM参数

例如:

java -server -Xms1024m -Xmx1024 -XX:UseG1GC -jar  xxx.jar