侧边栏壁纸
博主头像
敢敢雷博主等级

永言配命,自求多福

  • 累计撰写 57 篇文章
  • 累计创建 0 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

备战春招---JVM

敢敢雷
2019-12-28 / 0 评论 / 0 点赞 / 54 阅读 / 8,442 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我删除。

学习来源—深入理解Java虚拟机(周志明)
面经来源—牛客2020秋招面经大汇总!

JVM

Java程序开发相对于C和C+ + 程序开发,在内存管理领域,记得一个比喻,Java是自动挡,C和C+ + 是手动挡。Java在虚拟机自动内存管理机制帮助下,不需要为每一个new操作去写内存释放代码,不容易出现内存泄漏和内存溢出问题。同时,java一次编写多处运行的特性也离不开JVM的支持。可见,JVM在Java中的重要性。

JVM数据区域

JVM所管理的内存区域包括下图的几个区域
image.png

程序计数器

程序计数器是一块内存较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在java程序运行时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
在Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以,每条线程都有一个独立的程序计数器,各条线程之间的计数器互不影响,独立储存,所以程序计数器的线程私有的内存。
程序计数器是唯一一个在Java虚拟机规范没有规定任何内存溢出情况的区域。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈桢,栈帧是用来存储局部变量表、操作数栈、动态链接、方法出入口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
虚拟机栈主要是存储Java方法执行的。所以虚拟机栈也是线程私有区域。
虚拟机栈有两种异常状况

  1. 如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,即栈溢出。
  2. 如果虚拟机栈在扩展时,无法申请到足够的内存,将会抛出OutOfMemoryError异常,即内存溢出。

本地方法栈

本地方法栈和虚拟机栈所发挥的作业非常相似,它们的区别就是本地方法栈中执行的是Native方法。
和虚拟机栈一样,它是线程私有的区域。它也会抛出StackOverflowError异常和OutOfMemoryError异常。

Java堆

Java堆是Java虚拟机所管理的内存中最大的一块。此区域是唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆内存区域是GC的主要管理区域,所以堆还可以细分为新生代和老年代。
因为Java堆中的内容均为对象实例,所以Java堆内存是内存共享的区域。正因为如此,如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量等。因为它存放了静态变量,所以方法区也被叫做永久代。
和堆一样,这个区域也会发生GC,但是没有堆内存那么频繁。方法区也是内存共享区域,它在无法满足内存分配需求时,也会抛出OutOfMemoryError异常。

运行时常量池

特别的运行时常量池,就是我们的String中的字符串常量池,它在JDK1.6,运行时常量池在方法区中,在JDK1.7后,运行时常量池被移到到了堆内存中,故而,运行时常量池也受到内存到限制,在常量池无法再申请到内存时,将会抛出OutOfMemoryError异常。

对象创建的过程

Java中创建对象的方式有很多种,比如关键词new,反射的newInstance(),clone方法和反序列化。在Java虚拟机中,看看是怎么创建对象的吧。

比如new关键字,当虚拟机遇到一条new指令时,首先回去检查这个指令当参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

分配内存空间

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。再分配内存过程中,会通过空闲列表或者发生指针碰撞(想象两个区域,空闲的区域是一边,使用过的区域是一边,中间一个指针,然后指针向空闲区域挪动一个分配空间的大小,就是指针碰撞)分配内存。
但是在分配内存这步在并发情况下不是安全的,解决方法有两个

  1. 对分配内存空间对动作进行同步处理,实际上采用的就是CAS失败重试来保证原子性。
  2. 使用TLAB,即本地线程分配缓存。把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,就是使用TLAB。

初始化对象

内存分配完成后,虚拟机将分配到的内存空间都初始化为零值。接下来虚拟机要对对象进行必要的设置,例如这个对象的哈希玛,对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。这些工作完成之后,从虚拟机的视角来看,一个对象已经产生,但是Java程序的角度来看,对象的创建才刚刚开始。目前所有的字段都还为零,所以,接着把对象按照程序员的意愿初始化,即赋值。这样,一个真正可用的对象才算完全产生出来。

将内存空间的地址赋值给对应的引用

初始化对象后,需要使用这个对象,目前这个对象还在堆中,如果需要使用这个对象,需要在栈中引用堆的对象,即将内存空间的地址赋值给对应的引用。现在,对象才能够被使用。

其中初始化对象和将内存地址赋值给对应的引用可能发生指令重排序

类的生命周期

类从被加载到虚拟机内存中开始,到卸载除内存为止,它到整个生命周期其实包括七个阶段,分别如下:

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

image.png

其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的类的加载过程必须按照这种顺序按部就班地开始,而解析阶段就不一定了。它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

类的初始化顺序

直接看这张图把,代码找不到了,哈哈
image.png

双亲委派模型

双亲委派机制是在JDK 1.2期间被引用并被广泛应用于之后几乎所有的Java程序中。它是Java设计者推荐给开发者的一种类加载器实现方式。
双亲委派机制的工作过程就是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

类加载器

类加载器有四种,由高到低的加载器分别为:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application ClassLoader)
  • 自定义类加载器(User ClassLoader)

启动类加载器

启动类加载器负责加载存放在JAVA_HOME的lib目录中的.jar文件。启动类加载器无法被Java程序直接引用。

扩展类加载器

扩展类加载器负责加载JAVA_HOME /lib/ext目录下的所有类库,开发者可以直接使用扩展类加载器

应用程序类加载器

应用程序类加载器负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。

自定义类加载器

自定义类加载器使用场景有下面几个

  1. 想加载非ClassPath随意路径中的类文件
  2. 都是通过接口来实现,希望解耦时,常用于框架设计
  3. 这些类希望予以隔离,不同应用的同类名都可以被加载,不受冲突,比如Tomcat

使用步骤如下

  • 继承ClassLoader父类
  • 遵从双亲委派机制,重写findClass方法
  • 读取文件字节码
  • 调用父类的defineClass来加载类
  • 使用者调用该类加载器的loadClass方法

打破双亲委派模型

双亲委派模型并不是一个强制性的约束模型,双亲委派模型的好处是在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
可以看出双亲委派模型解决了各个类加载器的基础类的统一问题(越基础的类越往上层的类加载器进行加载),但是如果这些基础类又要调用用户代码怎么办。那么现在就要打破双亲委派机制了。这里举个实际例子,比如JDBC
先分析下:

  1. 使用JDBC第一步是加载驱动Class.forName(“xx.xx.DriverA”)来加载实现类
  2. Class.forName()方法默认使用当前类的ClassLoader,JDBC是在DriverManager类里调用Driver的,当前类也就是DriverManager,它的加载器是BootstrapClassLoader
  3. 用BootstrapClassLoader去加载非rt.jar包里的类xx.xx.DriverA,就会找不到
  4. 要加载xx.xx.DriverA需要用到AppClassLoader或其他自定义ClassLoader

那么问题就出现了,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类。如何在父加载器加载的类中,去调用子加载器去加载类。现在,我们就要打破双亲委派模型。
Java引入了一个设计:线程上下文类加载器。这个类加载器就是通过java.lang.Thread类的setContextClassLoaser()方法设置。如果创建线程时未设置,它会从父线程中继承一个,如果全局都未设置,那么这个类加载器就是应用程序类加载器。所以,引出了Java的一个高级特性—SPI,它就是通过父类加载器请求子类加载器去完成类加载的动作。这种行为实际上就是打破了双亲委派模型来逆向使用类加载器。

SPI

要使用Java SPI,需要遵循如下约定:

  1. 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  2. 接口实现类所在的jar包放在主程序的classpath中;
  3. 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  4. SPI的实现类必须携带一个不带参数的构造方法;

JDBC

先看MySQL驱动包
image.png
image.png
现在看下JDBC是怎么启动:

  1. 先加载启动Class.forName(“com.mysql.cj.jdbc.Driver”)。
  2. 创建数据库的连接使用DriverManager的getConnectin()方法。

那现在进入DriverManager类

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
      
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

 public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

通过线程上下文加载器,在加载Driver时,通过ServiceLoader.load(Driver.class)方法,获得了启动类加载器,再使用应用程序类加载器完成加载驱动。显然,这里打破了双亲委派模型,将原本应该使用启动类加载器加载的Driver用应用程序类加载器加载。解决了要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类。

其实打破双亲委派模型在很多地方都有,例如Spring也是通过SPI来解耦或者是在第三次打破双亲委派模型来解决的热部署问题。

一个SPI的小Demo,可以看见Spring的一些端倪吧。
首先先明白Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
先创建接口,Animal

public interface Animal {
    void say();
}

然后两个实现类

public class Cat implements Animal {
    @Override
    public void say() {
        System.out.println("我是小猫");
    }
}

public class Dog implements Animal {
    @Override
    public void say() {
        System.out.println("我是小狗");
    }
}

安装约定,创建配置文件
image.png
主启动类

public class main {
    public static void main(String[] args) {
        ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
        for (Animal animal : animals){
            animal.say();
        }
    }
}

输出结果如下
image.png

例如SpringBoot的自动装配,就和SPI有关系,不过他是对SPI的扩展,在SpringBoot的AutoConfigure就有一个特别的文件
image.png
image.png

spring.factories文件,是用来记录项目包外需要注册的bean类名。

GC

现在在JVM中,对象的创建过程大概清楚了。在复习下关于对象的清理,即垃圾回收。

需要清理的区域

根据JVM内存模型区域,其中虚拟机栈、本地方法栈和程序计数器,这三个区域是线程私有的区域,它们随着线程而生,随线程而灭。所以,这部分区域并不是垃圾回收区域。
而Java堆和方法区不一样,它们是主要存储对象的区域,这部分的内存分配和回收都是动态的,所以垃圾回收器主要关注的就是这部分内存。

需要清理的对象

我们知道堆中存放的是Java几乎所有的实例对象,所以在垃圾回收器在对堆回收前,第一件事就是判断哪些对象还存活,哪些对象已经死去。
有两种判断对象是否存活的算法,分别是引用计数算法和可达性分析算法。

引用计数算法

简单的说,引用计数算法就是给对象中添加一个引用计数器,每当又一个地方引用它时,计数器的值加一,当引用失效时,计数器值就减一。任何时刻计数器为0的对象就是不可能再被使用的。
所以,引用计数器的实现简单,判断效率也高。但是Java并没有使用引用计数算法,因为它很难解决对象之间相互循环引用的问题。
比如A引用B,同时B也引用A,这样就导致它们的引用计数器永远不可能等于0,于是引用计数算法无法通知GC收集器回收它们。

可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots
没有任何引用链相连时,则证明此对象是不可用的。

在Java中,可以作为GC Roots的对象包括以下几种

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象。
  • 本地方法栈中Native方法引用的对象

这样记就行,首先能作为GC Roots对象的,肯定是存活时间最长或者最后回收的对象。
所以在虚拟机栈中,引用的对象,这些肯定是正在使用的对象,所以它可以作为GC Roots。
然后,方法区也被叫做永久代,在这个里面的大多数对象都是最后被GC或者不能GC的,所以静态实现和常量引用对象可以作为GC Roots对象。
Native方法是Java本地方法,所以它引用的对象也可以作为GC Roots对象。

用什么方法回收

垃圾收集算法有四种分别是标记-清除算法,复制算法,标记-整理算法,分代收集算法。

标记-清除算法

它是最基础的收集算法,如它的名字引用,算法分为“标记”和“清除”两个阶段。
首先标记出所需要回收的对象,在标记完成后统一回收所有被标记的对象。它的不足主要有两个:

  1. 效率问题。标记和清除两个过程的效率都不高。
  2. 空间问题。标记清除后会产生大量不连续的内存碎片,空间碎片太多导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

为了解决效率问题,复制算法出现了。它就是将内存按容量划分为两块大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活着的对象复制到另一块上面,再把已使用过的内存空间一次性清理掉。
这种算法的代价是将内存缩小为了原来的一半,使能使用的空间也缩小了一半。但是复制算法一般使用在新生代,因为对象都是朝生夕灭的。所以在新生代,使用复制算法完美的应用了其复制算法的特性。

标记-整理算法

复制算法会浪费一半的空间,标记-清除算法会产生大量的不连续碎片。所以,针对前两种算法的缺陷,有第三种垃圾回收算法—标记-整理算法。
标记-整理算法的标记过程和标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉边界以外的内存。
简单的说,就是先标记需要回收的对象,然后把存活的对象进行整理,再清理掉除存活对象以外的内存。这样做的目的是减少了内存碎片,因为它的整理步骤,将存活的对象移到了一端。

分代收集算法

为了更好的回收垃圾对象,在JVM中,将堆分为新生代和老年代。
在新生代中,每次垃圾收集时都会有大批对象死去。只有少量存活,同时新生代的GC很频繁,那就可以使用复制算法。
而老年代中,因为对象存活率高、没有额外空间对它进行分配担保,同时GC发生频率低,就可以使用标记-清理或者标记-整理算法。

垃圾收集器

垃圾回收算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java中根据不同的场景提供了三个种类收集器,分别是串行的、吞吐量优先的和用户响应时间优先的。

串行

根据它的名字,可以看出这个种类的收集器在回收时是串行单线程的,它的具体收集器就是Serial收集器和Serial Old收集器。

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,它是一个单线程的收集器,但这个“单线程”的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作。单线程是指它在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
Serial收集器也是作用于新生代的一款收集器。它收集起来简单高效,对于单核CPU的环境来说,Serial收集器没有线程交互的开销,所以它适用于CPU核数少的运行环境。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,他和Serial收集器同样是一个单线程收集器。在老年代使用的是“标记-整理算法”。同时,它还有一个用途,就是作为CMS收集器的后备预案。
同时,因为他在老年代使用的是“标记-整理算法”。根据这个特性,它更加适合于在堆内存较小的运行环境中。

所以关于串行这一类收集器,它们是一款单线程的收集器,根据他们的特性,他们适用于在CPU核数少和堆内存较小的环境下使用。

吞吐量优先

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

如果虚拟机总共运行了100分钟,其中垃圾收集花费掉了1分钟,那吞吐量就是99%。
故而,这类收集器会使用多线程来并行回收垃圾。
主要实现的收集器有Parallel Scavenge收集器和Parallel Old收集器

Parallel Scavenge收集器

Parallel Scavenge收集器它是一款新生代收集器,采用的复制算法并且是并行多线程收集器。使用这款收集器,就不需要手工指定新生代的大小、Eden与Survivor的比例、晋升老年代对象大小等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量。
所以它适用于多核CPU和堆内存较大的运行环境。

Parallel Old收集器

它是Parallel Scavenge收集器的老年代版本,使用的是多线程和“标记-整理”算法。如果在新生代使用Parallel Scavenge收集,那老年代会自动使用Parallel Old收集,无需再继续手动设置,它也和Parallel Scavenge收集器一样的注重吞吐量以及CPU资源敏感的场合。

用户响应时间优先

为了提高用户响应,毫无疑问,它也是用的多线程并行收集垃圾对象。相对于吞吐量,它更注重的是让单个STW时间最短,而吞吐量优先是尽可能让单位时间内STW时间更短。这两个是不同的概念。
实现它的收集器有ParNew收集器和CMS收集器

ParNew收集器

ParNew收集器实际上就是Serial收集器的多线程版本,它也是作用于新生代的收集器,一样的采用的复制算法进行垃圾收集。所以它也和Serial一样简单粗暴,唯一不同的是采用多线程并行来收集垃圾。所以如果在单CPU环境下,ParNew收集器的效果肯定没有Serial收集器效果好。
它一般与CMS收集器一起使用。

CMS收集器

CMS收集器是HotSpot虚拟机中第一款真正意义上的并发收集器。CMS收集器是使用的“标记-清除”算法作用于老年代的一款收集器。它的运行过程相比于其他几种收集器更复杂一些,整个步骤分为4个。

  1. 初始标记:主要做两件事情一是遍历GCRoot可直达的老年代对象;二是遍历新生代直达的老年代对象。这里的直达是指直接关联到GCRoot的一级对象。初始标记阶段是完全STW的,引用程序会暂停。
  2. 并发标记:并发标记阶段是与应用程序一起执行的,这个阶段主要做两件事
    一是对初始标记中标记的存活对象进行trace,标记这些对象为可达对象,例如A->B,A在初始标记被识别,而B就是在并发标记阶段被识别;二是将在并发阶段新生代晋升到老年代的对象、直接在老年代分配的对象以及老年代引用关系发生变化的对象所在的card标记为dirty,避免在重新标记阶段扫描整个老年代。
  3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  4. 并发清除

其中初始标记、重新标记这两个步骤仍需要STW。

因为CMS是一款基于”标记-清除“算法实现的,所以它在清除后会产生大量空间碎片。所以Serial Old收集器因为它是单线程采用”标记-整理“算法实现的老年代收集器可以作为CMS的后备预案。

其他垃圾收集器

随着JDK版本的不断提升,在后续又出现了G1垃圾收集器和ZGC垃圾收集器

G1垃圾收集器

在JDK1.7时,就有一款新的垃圾收集器诞生,它就是G1垃圾收集器。
与其他GC收集器相比,G1具备如下特点。

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW停顿时间。同时G1收集器在Java线程执行GC时,仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:和其他收集器一样,分代的概念还是在G1中保留。但是它分代的方式与其他收集器不同的是,它不在按照传统的概念将堆分为Eden区,From Survivor区、To Survivor区和老年代,它管理的模式是管理整个Java堆,根据不同的存活时间、熬过多少次GC的旧对象来获得更好的收集效果。即将整个堆分为若干个Region空间,和棋盘一样。
  • 空间整合:G1收集器整体来看还是基于“标记-整理”算法实现的收集器,但是在两个Region之间又是基于复制算法实现的,但是无论如何,G1收集器都不会产生内存碎片。
  • 可预测的停顿:G1收集器可以建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

image.png
H是以往算法中没有的,它代表Humongous,表示这些Region存储的是巨型对象,当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。

G1收集器运作大致可以划分为以下几个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

可以发现G1收集器前面两个步骤的操作过程和CMS有很多相似的地方。
初始标记仅仅只是标记一下GC Roots能够直接关联到的对象,这阶段需要停顿线程,但耗时很短。
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活对象,这阶段耗时较长,但是可以并发执行。
最终标记阶段则是为了修正在标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在Remembered Set Logs里面。最好将数据合并到Remembered Set中,这阶段需要停顿线程,但是可以并发执行。
最后在筛选回收,对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。

ZGC收集器

Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器。它是在JDK 11提出的,目前还是半开源的收集器。它有以下特性

  • 停顿时间不会超过10ms
  • 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下)
  • 可支持几百M,甚至几T的堆大小(最大支持4T)

ZGC目前只在Linux/x64上可用,如果有足够的需求,将来可能会增加对其他平台的支持。目前只支持64位的linux系统

JDK可组合的收集器版本

连线的都是可组合使用的版本
image.png

对应JVM参数如下

新生代(别名) 老年代 JVM参数
Serial (DefNew) Serial Old(PSOldGen) -XX:+UseSerialGC
Parallel Scavenge (PSYoungGen) Serial Old(PSOldGen) -XX:+UseParallelGC
Parallel Scavenge (PSYoungGen) Parallel Old (ParOldGen) -XX:+UseParallelOldGC
ParNew (ParNew) Serial Old(PSOldGen) -XX:-UseParNewGC
ParNew (ParNew) CMS+Serial Old(PSOldGen) -XX:+UseConcMarkSweepGC
G1 G1 -XX:+UseG1GC
0

评论区