JVM原理

1 Java虚拟机概述

1.1 跨平台性

在Java虚拟机中执行的指令,我们称之为Java字节码指令。
同一个Java程序,被编译成一组Java字节码的集合之后,可以通过Java虚拟机运行与不同的操作系统上,不同的操作系统使用不同的Java虚拟机,实现了跨平台的特性。

1.2 JVM基本结构

image.png

1.3 JVM类加载流程和内存结构总览

image.png
加载(排队买票)-验证(检票)-准备(上车)-解析(解析票寻找座位)-初始化(开车)

1.4 类加载

1.4.1 加载

通过类的全路径名称,读取类的二进制数据流。解析类的二进制数据流,转化为方法区(永久代or元空间)内部的数据结构。创建java.lang.Class类的实例对象,表示该类型。

1.4.2 验证

它的目的是保证第一步中加载的字节码是合法且符合规范的。
格式检查:检查魔数、版本、长度等等。
语义检查:抽象方法是否有实现类、是否集成了final类等等编码语义上的错误检查。
字节码验证:跳转指令是否指向正确的位置,操作数类型是否合理等。
符号引用验证:符号引用的直接引用是否存在。

1.4.3 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即:在方法区中分配这些变量所使用的内存空间。
注意这里所说的初始值概念,比如一个类变量定义为:public static int v = 8080;实际上v在准备阶段后的初始值为0而不是8080,将v赋值为8080的put static指令是程序被编译后,存放于内构造器方法之中。
但是注意,如果声明为:public static final int v = 8080;在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

1.4.4 解析

解析阶段是指虚拟机将运行时常量池中的符号引用替换为直接引用的过程。
符号引用就是class文件中的:CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的变量。

1.4.5 初始化

到达这个阶段,类就可以顺利加载导系统中。此时,类才会开始执行Java字节码。初始化阶段是执行类构造器方法的过程
方法是由编译器自动收集类中的类变量的赋值操作静态语句块中的语句合并而成的,虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法

2 JVM虚拟机内存模型

2.1 程序计数器(线程私有)

是当前线程所执行的字节码的行号指示器,指向虚拟机字节码指令的位置。
被分配了一块较小的内存空间。
针对于非Native方法:是当前线程执行的字节码的行号指示器。
针对于Native方法:则为undefined。
每个线程都有自己独立的程序计数器,所以,该内存是线程私有的。
这块区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

2.2 虚拟机栈&本地方法栈(线程私有)

虚拟机栈为执行Java方法服务的,是描述方法执行的内存模型。
栈是线程私有的内存空间。
每次函数调用的数据都是通过栈传递的。
在栈中保存的主要内容为栈帧。它的数据结构就是先进后出。每当函数被调用,该函数就会被入栈,每当函数执行完毕,就会执行出栈操作。而当前栈顶,即为正在执行的函数。
每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、帧数据区等信息。
本地方法栈是为native方法服务的。
image.png

2.3 堆

运行时数据区,几乎所有的对象都保存在java堆中。
Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示地释放。
堆是垃圾收集器进行GC的最重要的内存区域。
Java堆可以分为:新生代(Eden区、S0区、S1区)和老年代。
在绝大多数情况下,对象首先分配在eden区,在一次新生代GC回收后,如果对象还存活,则会进入S0或S1,之后,每经历过一次新生代回收,对象如果存活,它的年龄就会加一。当对象的年龄达到一定条件后,就会被认为是老年代对象,从而进入老年代。

2.4 方法区

逻辑上的东西,是JVM的规范,所有虚拟机必须遵守的。
是JVM所有线程共享的、用于存储类信息,例如字段、方法数据、常量池等。
方法区的大小决定了系统可以保存多少个类
JDK8之前–永久代
JDK8之后–元空间

2.4.1 永久代

指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类,从而造成内存溢出。所以,设置合适的永久代大小,对于系统的稳定性是至关重要的。
-XX:PermSize
设置初始永久代大小。例如:-XX:PermSize=5m
-XX:MaxPermSize
设置最大永久代大小,默认情况下为64MB。例如:-XX:MaxPermSize=5m

2.4.2 元空间

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用堆外的直接内存。
因此,与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存
-XX:MaxMetaspaceSize
设置元空间默认初始大小。例如:-XX:MetaspaceSize=40m
设置最大元数据空间。例如:-XX:MaxMetaspaceSize=40m
为什么使用永久代替换元空间?
不用进行空间设置,用的是堆外内存。

3 JVM垃圾回收算法

3.1 什么事垃圾回收

GC:垃圾回收,即:Garbage Collection。
垃圾:特指存在于内存中的、不会再被使用的对象。
回收:清除内存中的“垃圾”对象。
image.png

3.2 可触及性

什么是可触及性?
就是GC时,是根据它来确定对象是否可被回收的
也就是说,从根节点开始是否可以访问到某个对象,也说明这个对象是否被使用。
可触及性分为三种状态:
① 可触及:从根节点开始,可以到达某个对象。
② 可复活:对象引用被释放,但是可能在finalize()函数中被初始化复活。
③ 不可触及:由于finalize()只会执行一次,所以,错过这一次复活机会的对象,则为不可触及状态。

public class DieAliveObject {
    private static DieAliveObject dieAliveObject;

    public static void main(String[] args) {
        dieAliveObject = new DieAliveObject();

        int i = 0;
        while (i < 2) {
            System.out.println(String.format("----------GC nums=%d----------", i++));
            dieAliveObject = null; // 将dieAliveObject对象置为"垃圾对象"

            System.gc(); // 通知JVM可以执行GC了
            try {
                Thread.sleep(100); // 等待GC执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (dieAliveObject == null)
                System.out.println("dieAliveObject is null");
            else
                System.out.println("dieAliveObject is not null");
        }
    }

    /**
     * finalize只会被调用一次,给对象唯一一次重生的机会
     */
    @Override
    protected void finalize() {
        System.out.println("finalize is called!");
        dieAliveObject = this; // 使对象复生,添加引用
    }
}

3.3 四种引用级别

强引用
就是一般程序中的引用,例如 Student student = new Student();
软引用(java.lang.ref.SoftReferenct)
当堆空间不足时,才会被回收。因此,软引用对象不会引起内存溢出。

public class SoftReferenceDemo {
    public static void main(String[] args) throws Throwable{
        /** 查看空余内存 */
        System.out.println("---------Free " + Runtime.getRuntime().freeMemory() / 1000000 + "M----------");

        /** 创建Teacher对象的软引用 */
        Teacher teacher = new Teacher("aaa", 15);
        SoftReference softReference = new SoftReference(teacher);
        System.out.println("softReference=" + softReference.get());

        /** 使得teacher失去引用,可被GC回收 */
        teacher = null;

        /** 执行第一次GC后,软引用并未被回收 */
        System.gc();
        System.out.println("---------First GC----------");
        System.out.println("softReference=" + softReference.get());

        /** 可以通过对数组大小数值调整,来造成内存资源紧张 */
        byte[] bytes = new byte[7*972*1024];
        System.out.println("---------Assign Big Object----------");

        /** 执行第二次GC,由于堆空间不足,所以软引用已经被回收 */
        System.gc();
        System.out.println("---------Second GC----------");
        Thread.sleep(100); // 睡眠0.1秒钟,保证GC已经执行完毕
        System.out.println("softReference=" + softReference.get());
    }
}

弱引用(java.lang.ref.WeakReferenct)
当GC的时候,只要发现存在弱引用,无论系统堆空间是否不足,均会将其回收。

public class WeakReferenceDemo {
    public static void main(String[] args) throws Throwable{
        /** 创建Teacher对象的弱引用 */
        Teacher teacher = new Teacher("aaa", 15); // teacher的强引用

        /** teacher的弱引用 */
        WeakReference<Object> weakReference = new WeakReference<>(teacher);

        /** 使得teacher失去引用,可被GC回收 */
        teacher = null; // help GC

        /** 执行GC前,查看弱引用并未被回收 */
        System.out.println("---------Before GC----------");
        System.out.println("weakReference=" + weakReference.get());

        /** 执行GC,所以弱引用已经被回收 */
        System.gc();
        System.out.println("---------After GC----------");
        Thread.sleep(1000); // 睡眠1秒钟,保证GC已经执行完毕
        System.out.println("weakReference=" + weakReference.get());
    }
}

虚引用(java.lang.ref.PhantomReferenct)
如果对象持有虚引用,其实与没有引用是一样的。虚引用必须和引用队列在一起使用,它的作用是用于跟踪GC回收过程,所以可以将一些资源释放操作放置在虚引用中执行和记录。

/**
 * 虚引用Demo
 * 虚引用必须和引用队列一起使用
 *
 **/
public class PhantomReferenceDemo {
    private static PhantomReferenceDemo obj;

    public static void main(String[] args) {
        /** 创建引用队列 */
        ReferenceQueue<PhantomReferenceDemo> phantomRefQueue = new ReferenceQueue<>();

        /** 创建虚引用 */
        obj = new PhantomReferenceDemo();
        PhantomReference<PhantomReferenceDemo> phantomReference = new PhantomReference<>(obj, phantomRefQueue);
        System.out.println("phantomReference = " + phantomReference.get()); // 总会返回null

        /** 创建后台线程 */
        Thread thread = new CheckRefQueueThread(phantomRefQueue);
        thread.setDaemon(true);
        thread.start();

        /** 执行两次GC,一次被finalize复活,一次真正被回收 */
        for (int i = 1; i <=2 ; i++) {
            gc(i);
        }
    }

    public String print() {
        return "这是一个打印方法";
    }

    private static void gc(int nums) {
        obj = null;
        System.gc();
        System.out.println("---------第" + nums + "次GC----------");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (obj == null) {
            System.out.println("obj is null");
        } else {
            System.out.println("obj is not null");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize() is called!");
        obj = this; // 复活对象
    }
}

/**
 * 从引用队列中获得被回收的对象
 */
class CheckRefQueueThread extends Thread {
    private ReferenceQueue<PhantomReferenceDemo> phantomRefQueue;

    public CheckRefQueueThread(ReferenceQueue<PhantomReferenceDemo> phantomRefQueue) {
        this.phantomRefQueue = phantomRefQueue;
    }

    @Override
    public void run() {
        while (true) {
            if (phantomRefQueue != null) {
                PhantomReference<PhantomReferenceDemo> phantomReference = null;
                try {
                    /**
                     * 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入到引用队列,以通知应用程序对象
                     * 的回收情况
                     */
                    phantomReference = (PhantomReference<PhantomReferenceDemo>) phantomRefQueue.remove();
                } catch (Throwable e) {
                    e.printStackTrace();
                }
                if (phantomReference != null) {
                    System.out.println("Object="+ phantomReference +" is delete by GC");
                }
            }
        }
    }
}

3.4 槽位复用

局部变量表中的槽位是可以服用的,如果有一个局部变量超过了其作用域,则在其作用域之后的局部变量就有可能服用该变量的槽位,这样能够起到节省资源的目的。

3.5 对象分配总览

image.png

3.5 栈上分配

栈上分配是JVM提供的一项优化技术。基本思想如下所示:
① 对于那些线程私有的对象(即:不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。
② 分配在栈上的好处是可以再函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
③ 对于大量的零散小对象,栈上分配提供了一种很好的对象分配优化策略,栈上分配速度快,并且可以有效避免GC带来的负面影响,但是由于和堆空间相比,栈空间较小,因此对于大对象无法也不适合在栈上分配。
栈上分配的技术基础,两者必须都开启:
① 逃逸分析:逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。
② 标量替换:允许将对象打散分配在栈上。比如:若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。

3.5.1 逃逸分析

对于线程私有的对象,可以分配在栈上,而不是分配在堆上。好处是方法执行完,对象自行销毁,不续约gc的介入。可以提高性能。而栈上分配的一个技术基础(如果关闭逃逸分析或关闭标量替换,那么无法将对象分配在栈上)就是逃逸分析。
逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。如下所示:

Student student; // 属于逃逸了
public void say1() {
    student = new Student();
}

public void say2() {
    Student student = new Student();// 没有逃逸
}

3.5.2 标量替换

标量
不可被进一步分解的量,Java的基本数据类型就是标量。
聚合量
标量的对立就是可以被进一步分解的量,Java中对象就是可以被进一步分解的聚合量。
替换过程
① 通过逃逸分析确定该对象不会被外部访问。
② 对象可以被进一步分解,即:聚合量。其中,JVM不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。

/**
 * 栈上分配(默认开启)
 * 逃逸分析:DoEscapeAnalysis
 * 标量替换:EliminateAllocations
 * TLAB:UseTLAB
 * 【栈上分配】-Xmx50m -Xms50m -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:-UseTLAB  // 4毫秒
 * 【关闭栈上分配】-Xmx50m -Xms50m -XX:+PrintFlagsFinal -XX:+PrintGCDetails -XX:-UseTLAB -XX:-DoEscapeAnalysis // 1530毫秒
 *  注意:-XX:+PrintFlagsFinal只是为了查看参数设置情况,可以去掉。
 *
 **/
public class AssignOnStack {
    public static void main(String[] args) {
        sizeOfStudent();
        StopWatch stopWatch = StopWatch.createStarted();
        // 制造将近7.5个G左右的对象
        for (int i=0; i< 100000000; i++) {
            initStudent();
        }
        stopWatch.stop();
        System.out.println("========执行一共耗时:" + stopWatch.getTime(TimeUnit.MILLISECONDS) + "毫秒");
    }

    /**
     * student所占用空间为72bytes
     */
    public static void sizeOfStudent() {
        Student student = new Student();
        student.setName("muse");
        System.out.println("========student大小为:" + ObjectSizeCalculator.getObjectSize(student));
        System.out.println("========student大小为:" + RamUsageEstimator.humanSizeOf(student));
    }

    public static void initStudent() {
        Student student = new Student();
        student.setName("muse");
    }
}

3.5.3 TLAB分配

TLAB分配的全称是Thread Local Allocation Buffer,即:线程本地分配缓存区,这是一个线程专用的内存分配区域。
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次空间分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。
TLAB本身占用的Eden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占整个Eden空间的1%,当然可以通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。由于TLAB空间一般不会很大,因此大对象无法再TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。

/**
 * -XX:+UseTLAB -XX:+PrintTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis // 98ms
 * -XX:-UseTLAB -XX:+PrintTLAB -Xcomp -XX:-BackgroundCompilation -XX:-DoEscapeAnalysis // 189ms
 * @description
 **/
public class TLabDemo {
    public static void alloc() {
        byte[] b = new byte[2];
        b[0] = 1;
    }

    public static void main(String[] args) {
        long b = System.currentTimeMillis();
        for (int i=0; i<10000000;i++) {
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println("------------TLabDemo耗时:" + (e-b) + "------------");
    }
}

3.6 主要的垃圾回收算法

3.6.1 引用技术法

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就+1;当引用失效时,引用计数器就-1;只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
image.png
但是引用计数器有两个严重的问题:
① 无法处理循环引用的情况。
② 引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加减法操作,对系统性能会有一定的影响。
image.png
因此:JVM并未选择使用此算法作为垃圾回收算法。

3.6.2 标记清除法

内存碎片多,对于大对象的内存分配,不连续的内存空间分配效率低于连续空间。
标记清除算法是现代垃圾回收算法的思想基础。分为两个阶段:标记阶段和清除阶段。标记清除算法产生最大的问题就是清除之后的空间碎片
image.png

3.6.3 复制算法

为了解决标记清除算法效率低的问题。该算法效率高,并且没有内存碎片,但是只能使用一半的内存系统,适用于新生代。
将原有的内存空间分为两块每次只使用其中一块内存,例如:A内存,GC时将存活的对象复制到B内存中。然后清除掉A内存中所有对象。开始使用B内存。复制算法没有内存碎片,并且如果垃圾对象很多,那么这种算法效率很高。但是他的缺点是系统内存只能使用1/2
image.png
复制算法在JVM中的使用
因为新生代大多数对象都是“朝不保夕”,所以在新生代串行GC中,使用了复制算法。
① 设置Eden区与Survivior区比例的JVM参数:-XX:SurvivorRatio。
② 设置BigObject的JVM参数:-XX:PretenureSizeThreshold。
③ 设置OldObject的JVM参数:-XX:MaxTenuringThreshold。
下图解释,Eden是对象产生的地方,初次执行垃圾回收时,会讲将Eden中活跃对象放到S1中,清空Eden区,第二次执行垃圾回收时,会将Eden产生的新的对象和S1中的对象放入S2中,清空Eden区和S1区,以此交替执行。GC执行了15次都没有被回收的对象会在下一次GC时移到老年代中。
image.png

3.6.4 标记压缩法

为了解决复制算法只能使用1/2内存的问题。适用于垃圾对象多的情况,适用于老年代。
标记压缩算法是一种老年代的回收算法。
垃圾回收步骤:
① 标记存活对象。
② 将所有存活的对象压缩到内存的一端。
③ 清理所有存活对象之外的空间。
该算法不会产生内存碎片,并且也不用将内存一分为二。因此其性价比较高。
image.png

3.6.5 分代算法

将内存区间根据对象的生命周期分为两块,每块特点不同,使用回收算法也不同,从而提升回收效率。
将堆空间分为新生代老年代,根据它们只记得的不同特点,执行不同的回收算法,提升回收率。当前JVM的垃圾回收,都是采用分代收集算法的,针对新生代和老年代,它们对应的垃圾回收算法如下所示:
① 【新生代】由于大量对象消亡,少数存量对象,只需要复制少量对象,就可以完全清除S0/S1的垃圾对象空间,所以采用“复制算法”更为合适;
② 【老年代】对象存活率高,每次GC只清除少部分对象,所以采用“标记-清除”或“标记-压缩”算法来回收。
image.png

3.6.6 分区算法

将这个堆空间划分成连续不同的小区间,每个区间都单独使用独立回收。避免GC时间过长造成系统停顿。
将堆空间划分成连续的不同小区间,每个区间独立使用、回收。由于当堆空间大时,一次GC的时间会非常耗时,那么可以控制每次回收多少个小区间,而不是整个堆空间,从而减少一次GC锁产生的停顿。
image.png

4 JVM垃圾回收器

image.png

4.1 串行回收器-Serial

串行回收器也叫Serial回收器,是最古老收集器。他在JDK1.3之前是虚拟机新生代收集器的唯一选择。
它是单线程执行回收操作的。它的特点就是,在单核或内核少的计算机来说,有更好的性能表现。他的有点就是简单高效。
image.png
配置JVM参数启动制定的垃圾收集器
使用-XX:+UseSerialGC可以指定新生代和老年代都是Serial收集器。
使用-XX:+PrintCommandLineFlags可以打印虚拟机参数。
image.png

4.2 并行回收器概述

将串行回收器并行化,与串行回收器有相同的回收策略、算法、参数。
image.png

4.3 并行回收器-ParNew回收器

ParNew是一个新生代的回收器,也是一个独占式的回收器,他与串行回收器唯一不同的,就是它采取并发方式执行GC。一定要注意一点,就是在CPU核数少的机器,他的性能很可能比串行回收器差。
-XX:ParallelGCThreads指定并行GC的线程个数,最好与CPU个数一致,否则会影响垃圾回收性能。
默认情况下,当CPU数量<=8个的时候,并行线程数为7个。如果CPU数量>8个,并行线程数量为3+(5*cpu_nums/8)。

4.4 并行回收器-ParallelGC回收器

ParallelGC也是新生代的回收器,也采用复制算法执行GC回收任务。他与ParNew有一个不同点就是,它提供了一些设置系统吞吐量的参数来控制GC行为。
-XX:MaxGCPauseMillis:最大的垃圾收集暂停时间,它是一个大于0的整数,ParallelGC会根据设置的值来调整堆的大小和其他JVM参数,使其把GC停顿时间控制在MaxGCPauseMillis之内,但是大家要注意,如果将值设置很小,虽然停顿时间小了,却造成初始化的堆也变小了,垃圾回收会变得很频繁。
-XX:GCTimeRatio:设置吞吐量大小,可设置0~100之间的整数。就是说它影响的是垃圾回收的时间,通过1(1+m)来计算,假如n=99,那么1/(1+99)=1%,也就是说,系统会花费小于1%的时间用于垃圾回收。
-XX:+UseAdaptiveSizePolicy:如果你不倾向手动设置上面的参数,可以采用把参数调整交由虚拟机自动设置。

4.5 并行回收器-ParallelOldGC回收器

他跟ParallelGC相似,也是关注于吞吐量的收集器。是一个应用于老年代的回收器。
可以与ParallelGC搭配使用,即ParallelGC + ParallelOldGC。
采用标记压缩算法进行GC操作。也可以使用-XX:ParallelGCThreads来制定并行GC的线程个数。
image.png
查询是否使用了该回收器
image.png

4.6 并行回收器-CMS

CMS全称为Concurrent Mark Sweep,即:并发标记清除。它采用的是标记清除算法。也是多线程并发执行器。分为如下6个步骤:
① 初始标记(STW):标记根对象
② 并发标记:标记所有对象
③ 预清理:清理前的准备以及控制停顿时间。(可以采用-XX:CMSPrecleaningEnabled关闭,不进行预清理)
④ 重新标记(STW):修正并发标记数据。
⑤ 并发清理:清理垃圾(真正的执行垃圾回收)。
⑥ 并发重置:重置状态等待下次CMS的触发。
为什么要有预清理?
因为第四步重新标记是独占CPU的,如果YoungGC发生后,立即触发一次重新标记(新生代使用ParNew回收器),那么一次停顿时间可能很长,为了避免这种情况,预处理时,会刻意等待一次新生代GC的发生,然后根据历史数据预测下一次YoungGC的时间,在当前时间和预测时间取中间时刻执行重新标记操作,目的就是尽量避免YoungGC与重新标记重叠执行,从而减少一次停顿时间。
image.png
并行回收器JVM参数:
image.png

4.7 G1垃圾回收器

全称Garbage First Garbage Collector。优先回收垃圾比例最高的区域。
G1收集器将堆划分为多个区域,每次收集部分区域来减少GC产生的停顿时间。
第一阶段:新生代GC
新生代GC的主要工作就是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。回收后,所有的eden区都应该被清空,而survivor区会被收集一部分数据,但是应该至少仍然存在一个survivor区。
image.png
第二阶段:并发标记周期
① 初始标记(STW):标记从根节点直接可到达的对象。这一阶段会伴随一次YoungGC,会产生STW(全局停顿),应用程序会停止执行。
② 根区域扫描:由于YoungGC的发生,所以初始标记后,eden会被清空,存活对象放入Survivor区。然后本阶段则扫描survivor区,标记可直达老年代的对象。本阶段应用程序可以并行执行。但是,根区域扫描不能和YoungGC同时执行(因为跟区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),因此如果恰巧在此时需要进行YoungGC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次YoungGC的时间就会延长。
③ 并发标记:用来扫描整个堆的存活对象,并做好标记。与CMS相似,该阶段可以被一次YoungGC打断。
④ 重新标记(STW):本阶段也会发生STW,应用程序会停止执行。由于并发标记阶段中,应用程序也是并发执行的,所以本阶段,对标记结果进行最后的修正处理。
⑤ 独占清理(STW):本阶段也会发生STW,应用程序就会停止执行。它用来计算各个区域的存活对象和GC回收比例,然后进行排序,从而识别出可以用来混合收集的区域。该阶段给出了需要被混合回收的区域并进行了标记,那么混合收集阶段,是需要这些信息的。
⑥ 并发清理:本阶段会去识别并清理那些完全空闲的区域。
image.png
第三阶段:混合收集
在第二步的并发标记周期过程中,虽然有部分对象被回收,但是总体回收比例还是比较低的。由于G1已经明确知道哪些区域还有比较多的垃圾比例,所以就可以针对比较高的区域进行回收操作。
image.png

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值