JVM 知识点梳理

JDK 、JRE、JVM

  1. JDK( Java Development Kit ) Java开发工具包
    • JRE+ 开发命令工具(运行java.exe、编译javac.exe、javaw.exe)
  2. JRE( Java Runtime Environment )Java运行环境
    • JVM + Java核心类库(lang、io、util)
  3. JVM( Java Virtual Mechinal )Java虚拟机
    • 实现跨平台的核心(一次编写,到处运行)
    • 编译与解释
      • 计算机只认识低级语言(机器语言、汇编语言),而不认识高级语言(Java、C、Python)
      • 编译:通过编译器,将高级语言编译为低级语言,在Java语言中,编译又分为前端编译和后端编译。
      • 解释:通过解释器直接执行,不需要编译成机器语言。(HotSpot引入了JIT技术)
    • 编译阶段:
      • .java文件 编译 成.class javac 前端编译 编译期
      • .class文件 翻译成 机器指令 JVM 后端编译 运行期
    • Java编译性还是解析性?编译型+解释型
    • 反编译 .class ->.java :jd-gui ;javap(简易字节码)、jad(jad xxx.class)7、cfr(语法长)

对象创建流程

  1. 类加载检查
    当虚拟机接收到一条new指令( ?)时,先去检查这个指令的参数,是否能在常量池中定位到,这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,则执行类加载过程。
  2. 分配内存
    类加载检查通过后,虚拟机将为新生对象分配内存,相当于将一块同等大小的内存从Java堆中划分出来。
    • 对象一定会被分配到堆上吗?:如果JIT的逃逸分析后该对象没有逃逸,那么可能优化到栈上分配。
  3. 初始化
    内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
    • 零值: 基本数据类型会赋值为默认值(0 0l false),引用数据为赋值为null
  4. 设置对象头
    初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
    • 对象组成:对象头、实例数据、对齐填充
  5. 执行 <init>
    从虚拟机的角度已经创建完成一个新的对象,但从Java程序代码来看,对象创建才刚刚开始,需要执行<init>方法,按照程序中设定的初始化去操作初始化 ,这时候一个真正的程序对象就生成了
    <init>() :收集类中所有实例变量的赋值动作、实例代码块和构造函数合并产生的

内存分配方式

  1. 指针碰撞:默认 内存规整

    • 指针加法:新对象分配内存,指针向"未用"区域移动,移动大小为新对象的占用内存大小
    • 回收算法:标记复制,标记整理
  2. 空闲列表: 内存不规整

    • 列表: 已使用和未使用的内存在堆中相互交错,记录那些内存块可用,分配的时候从空余的内存块中分配一块和新对象的占用内存大小的给新对象
    • 回收算法:标记清除
  3. 并发情况:CAS+TLAB

    由于堆是全局共享的,如果使用指针碰撞的方式,当多线程分配对象时,指针会成为热点资源 ,效率变低

    首先在TLAB分配,当对象大于TLAB可用内存时候,再使用CAS+失败重试策略

  • TLAB 本地线程分配缓冲
    • 每个线程预先划分一块区域,每个线程只需要在自己的区域内进行内存分配即可,不需要争抢热点指针,如果分配的区域内存使用完了,在重新申请即可。
    • 仅在分配时候是独占的,读取依然是共享的
    • 默认开启,缺省 Eden 的1%:-XX:UserTLAB;-XX:TLABWasteTarget'Percent

TLAB问题:

  1. 空间浪费-内存孔隙:线程C,剩余空间1格,分配新对象需要2格
    • 直接在堆分配:后续仍然大于1格,后续需要分配的大多数对象都需要在堆内存直接分配
    • 废弃TLAB,使用新的TLAB:频繁废弃TLAB,频繁申请TLAB,并发问题
  2. JVM选择策略:最大浪费空间 refill_waste 运行时不断调整,使系统的运行状态达到最优
    • 最大浪费空间:允许浪费多少空间,不允许就是要保留,使用堆分配;允许浪费,就新建一个TLAB。一旦使用最大浪费空间,说明了原有的TLAB已经放不下对象了。
    • 当请求对象大于refill_waste时,会选择在堆中分配
    • 反之,会废弃当前TLAB,新建TLAB来分配新对象
    • refill_waste:1.5格;新对象:2格
  3. 浪费后,仍然会产生孔隙;当发生 GC 的时候,TLAB 没有用完,没有分配的内存也会成为碎片
  • CAS+重试
    • 第一个线程抢占到了分配空间,第二个线程没有抢占到就重试抢占后面一块内存空间,保证更新操作的原子性对分配空间同步处理
    • 谁抢上谁用,抢不上的继续抢,保持同步

对象组成

mark world:

  1. 对象头

    • mark world:存储对象自身的运行时数据
      • hashcode哈希码、GC分代年龄15(1111)
      • 轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳
    • 指针类型:指向对象的类元数据地址\Class对象,即对象代表哪个类(加载)
    • 记录数组长度:如果是数组对象,则对象头中还有一部分用来记录数组长度,该数据在32位和64位JVM中长度都是32bit
  2. 实例数据

    • 用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
    • 在Java代码中能看到的属性和他们的值
  3. 对齐填充字节

一个空的Object对象占16个字节

  1. 在64位系统中,指针压缩(前提)
    • 开启:markword占8个字节+类元指针占4个字节+padding填充4个字节=16个字节;
    • 不开启:markword占8个字节+类元指针占8个字节=16个字节;
  2. 启用指针压缩
    • -XX:+UseCompressedOops(1.6默认开启),禁止指针压缩:-XX:-UseCompressedOops
  3. 如果不开启这个指针压缩,都是用8个字节来存储这些对象的内存地址,这些信息放到堆里面,无形的就会增大很多空间,导致堆的压力很大。很容易触发gc

对象创建方式

  1. new创建新对象
    • new Student()
  2. 通过反射机制
    • Student.class.newInstance()\Student.class.getConstructor().newInstance();
    • String str = (String)Class.forName("java.lang.String").newInstance();
  3. 采用clone机制
java 复制代码
//1.实现Cloneable接口并复写Object的clone方法  
public class MyClass implements Cloneable {
    private int value;

    public MyClass(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    @Override
    public MyClass clone() throws CloneNotSupportedException {
        return (MyClass) super.clone();
    }
}
//2.new MyClass().clone()
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        MyClass obj1 = new MyClass(10);
        MyClass obj2 = obj1.clone();

        System.out.println(obj1.getValue()); // 输出:10
        System.out.println(obj2.getValue()); // 输出:10
    }
}
  1. 通过反序列化机制
    • 调用 ObjectInputStream 类的 readObject() 方法
      序列化:指把 Java 对象转换为字节序列的过程;
      反序列化:指把字节序列恢复为 Java 对象的过程;

类加载机制

类加载过程

类加载子系统在运行第一次遇到一个class文件时就去加载、链接、初始化class文件

  1. 加载 Loading : 将类的.class文件加载到JVM中
    类加载器 通过"包名 + 类名",找到Class字节码文件,创建一个java.lang.Class对象的实例来表示这个类,用,在方法区中存储类的元数据,Class对象作为程序中每个类的数据访问入口。
  2. 链接 Linking
    • 验证 Verify : 确保class文件字节流中的信息符合虚拟机规范,有没有安全隐患
      主要体现:文件格式、元数据、字节码、符号引用
    • 准备 Prepare :为类的"静态变量"分配内存,并设置默认值/零值,初始化阶段会显式赋值(将值直接赋上)
      不包含final修饰的,因为final修饰的在编译时分配内存赋默认值了
      例子:public static int value=123;
      在准备阶段之后赋为零值(int为0)-分配空间;在初始化阶段才会真正赋为123-真正赋值
      例子:public static final int value=123;
      在准备阶段直接赋为123(基本类型以及字符串常量,对象还是在初始化阶段赋值)
    • 解析 Resolve:将常量池中的符号引用->直接引用
      本类中如果用到了其他类,需要找到对应类,即将常量池中的符号引用->直接引用
      主要体现:字段、接口、方法
  3. 初始化 Initilization : 执行类构造器方法()的过程,主要完成"静态变量赋值"以及"静态代码块执行"
    • 产生方式:类存在 static修饰的变量 或者静态代码块 时,编译器自动生成
    • 什么时候会触发类的初始化? 触发时机:主动调用 懒加载
      • 创建类的实例 new
      • Class.forName("") 反射类
      • 首次访问这个类的静态变量或静态方法时
      • 子类初始化,如果父类还没初始化,会引发父类初始化;子类访问父类的静态变量,只会触发父类的初始化;
      • JVM启动时会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等
  4. 类的生命周期:
    • 加载:
    • 使用:
    • 卸载:Java自带的类是不会被回收掉的,只有自定义类加载器一些场景的类会被回收掉,如tomcat,SPI,JSP等临时类,是存活不久的,所以需要来不断回收
      • 该类所有的实例都已被GC回收
      • 该类的ClassLoader已经被GC回收
      • 类对应的Class对象没有被引用

类加载机制

双亲委派机制\父委派模型

当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由子类加载器来负责类的加载

类加载器

通过一个类全限定名称来获取其二进制文件(.class)流的工具,被称为类加载器。

或者说:找.class文件的工具,用于"加载"到JVM中

  1. Java语言系统中支持以下4种类加载器:各类加载器是组合关系 来复用父加载器的代码,不是继承关系
    • 启动类加载器 Bootstrap ClassLoader JAVA_HOME\lib rt.jar resources.jar
      用于加载 Java 的核心类,由底层的 C++ 实现,并不是一个 Java 类,无法被 Java 程序直接引用
    • 扩展类加载器 Extention ClassLoader JAVA_HOME\lib\ext
      用来加载java的扩展库,开发者可以直接使用这个类加载器
    • 应用\系统类加载器 Application ClassLoader 用户路径(classpath)上的类库
      用于加载程序员自己编写的类。系统默认的类加载器
    • 用户自定义类加载器 User ClassLoader
      自定义类加载器时,需要继承 java.lang.ClassLoader 类 或 URLClassLoader,并至少重写其中的findClass(Stringname)方法,若想打破双亲委托机制,需要重写loadClass()方法
java 复制代码
public class MyClassLoader extends ClassLoader{
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
    }
        @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        return super.loadClass(name, resolve);
    }
}

双亲委派流程

  1. 先检查类是否已经被加载过,如果已经被加载,就不需要加载 缓存
  2. 若没有加载,则调用父加载器的 loadClass() 方法进行加载
  3. 若父加载器为空,则默认使用启动类加载器作为父加载器
  4. 如果父类加载失败,抛出 ClassNotFoundException 异常后,调用自己的 findClass() 方法进行加载

ClassLoader 方法:

  • loadClass():如果父类加载器加载失败,则会调用自己的findClass方法完成加载,默认的双亲委派机制在此方法中实现,保证了双亲委派规则。(类加载过程是线程安全的)
  • findClass():根据名称或位置加载 .class 字节码
  • definclass():把 .class 字节码转化为 Class 对象
java 复制代码
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//private final ClassLoader parent;
                    // 2. 若没有加载,则调用父加载器的 loadClass() 方法进行加载
                    c = parent.loadClass(name, false);
                } else {
                    // 3.若父加载器为空,则默认使用启动类加载器作为父加载器
                    BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //父类无法完成加载请求,抛出 ClassNotFoundException 异常
            }
            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 父类无法完成加载请求,抛出异常后,再调用自己的 findClass() 进行加载
                c = findClass(name);
                // 5. 记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

补充:ClassNotFoundException和NoClassDefFoundError区别

  • ClassNotFoundException:运行期异常exception
    在类加载阶段尝试加载类的过程中,找不到类的定义时触发
    原因:Class.forName("")、类加载器loadClass、findSystemClass(),路径没有找到指定名称的类,抛出该异常
  • NoClassDefFoundError:编译期通过,运行时报错error
    表示运行时尝试加载一个类的定义时,虽然找到了类文件,但在加载、解析或链接类的过程中发生了问题
    原因:依赖问题或类定义文件(.class文件)损坏导致的
    依赖问题-interface模块:不放逻辑,模块中间调用接口,转换类(逻辑类,异常类)
    A、B类,B类引用A,后续A被删除
  • NoSuchMethodError:编译期通过,运行时报错error
    表示方法找不到
    原因:jar包冲突

双亲委派机制作用

  1. 避免类的重复加载
    当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类
  2. 保护程序安全,防止核心API被随意篡改
    • 沙箱安全机制

      在classpath下,要加载一个 java.lang.Integer 类的请求,通过双亲委派机制委派的启动类加载器,发现存在Integer类直接返回,不会重新加载传递的过来的Integer类,只会加载JAVA_HOME中的jar包里面的类,可以防止核心API被随意篡改。

    • 打破双亲委派的情况下,可以替换java. 包的类吗?不可以

      =>无法替换 java. 包的类,即使打破双亲委派,依然需要调用父类中的 defineClass()方法 来把字节流转换为一个JVM识别的 Class 对象,而 defineClass()方法 通过 preDefineClass()方法 限制类全限定名不能以 java. 开头。

    • 如何判断JVM中类和其他类是不是同一个类?

      取决于类加载器:每一个类加载器,都拥有一个独立的类名称空间

      比较两个类是否"相等",只有在同一个类加载器下才有比较意义。即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

java 复制代码
//将字节流转换成jvm可识别的java类
  protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);//检查类全限定名是否有效
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);//调用本地方法,执行字节流转JVM类的逻辑。
        postDefineClass(c, protectionDomain);
        return c;
    }
//检查类名的有效性
private ProtectionDomain preDefineClass(String name,ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);
        if ((name != null) && name.startsWith("java.")) { //禁止替换以java.开头的类文件
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }

        if (name != null) checkCerts(name, pd.getCodeSource());

        return pd;
    }

打破双亲委派

不委派、向下委派:

某些情况下父类加载器需要委托子类加载器去加载class文件,受到加载范围的限制,父类加载器无法加载到需要的文件
https://blog.csdn.net/laodanqiu/article/details/138817475

  1. 打破双亲委派方式
    • 重写 loadClass() 方法
    • 利用线程上下文加载器
  2. 例子:JDBC注册数据源驱动 4.0之后
    =>对于DriverManager类由jdk提供,位于rt.jar,被启动类加载器加载,而mysql的驱动jar包是有应用类加载器加载,当启动类加载器加载完DriverManager类后,又将DriverManager委派给应用程序类加载器去加载mysql的驱动jar包,这里需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派
java 复制代码
// 1.加载数据访问驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.连接到数据"库"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
//省略 Class.forName() 注册驱动这一步,在JDBC4.0后,支持SPI的形式注册Driver数据源
//当我们导入mysql-connector-java依赖jar包后,会生成 META-INF/services/java.sql.Driver ,java.sql.Driver中内容是"com.mysql.cj.jdbc.Drive"
java 复制代码
public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        //省略代码
        //使用ServiceLoader.load()加载配置文件中指定的实现
        //Driver.class => java.sql.Driver -> 具体实现com.mysql.cj.jdbc.Drive
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
             while(driversIterator.hasNext()) {
                driversIterator.next();
             }
        } catch(Throwable t) {
                // Do nothing
        }
        //省略代码
    }
}
//获取"线程上下文类加载器",即应用类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取"线程上下文类加载器",类似于 ThreadLocal 将变量传递到整个线程的生命周期
    //这个值如果没有特定设置,一般默认使用的是应用程序类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
//在DriverManager的静态代码块要执行的loadInitialDrivers(),中 driversIterator.next();
//具体实现:在ServiceLoader中的next()方法,
public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            //会返回一个nextService(); 
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //重点在于这里:
        //cn是刚才通过spi获取的Driver具体实现类:com.mysql.cj.jdbc.Drive
        //loader是刚才获取的应用类加载器
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

运行时数据区

内存结构

问题:共享状态作用存储内容扩容问题版本变化

(1)线程独占

  1. 虚拟机栈
    用于存储栈帧,当方法被调用时会创建一个栈帧入栈

    存储:局部变量表(存储本地局部变量)、操作数栈(进行运算)、帧数据(方法返回以及异常派发)

    在深度溢出或扩展失败的时,会分别抛出 StackOverflowError 和 OutOfMemoryError 异常

  2. 本地方法栈

    存储native本地方法信息,在Execution Engine执行时加载本地方法库

    HotSpot VM将本地方法栈和虚拟机栈合并了,本地方法栈也会在虚拟机栈的深度溢出或扩展失败的时候会分别抛出StackOverflowError 和 OutOfMemoryError 异常。

  3. 程序计数器 /pc寄存器

    存储当前执行的指令的地址,执行后指向下一条指令地址

    如果正在执行的是一个Natvie(本地方法),那么这个计数器的值则为空

(2)线程共享


  1. 存储所有对象及其实例变量和数组的信息,是垃圾回收的主要区域 OutOfMemoryError
  2. 方法区
    存储与类相关的一切信息
    存储:运行时常量池、字段数据、方法数据、方法代码,是堆的逻辑组成部分,垃圾回收是可选的
    • 具体实现:
      1.7及之前实现是永久代,JDK1.8及之后是"元数据区"元空间
      JDK6->JDK7:static变量、字符串常量池移到堆里
      永久代,垃圾回收不容易被触发,尤其像字符串对象很可能是随时变成垃圾
    • 元空间与永久代最大的区别在于:空间
      元空间并不在虚拟机中,而是使用本地内存\堆外内存,元空间的内存使用量受限于操作系统对本地内存的限制,更加灵活,有效地避免了永久代的内存溢出问题,并且可以减少垃圾回收的压力
      =》OOM:Metaspace
    • 方法区与堆:类是模板,对象是实体
      类的结构信息(包括实例变量的定义)存储在方法区,而实例变量的具体值存储在堆内存中
      这种分离使得类的结构信息在程序运行时是共享的,而实例变量的具体值则是每个对象独有的。

堆和栈 区别

  1. 功能
    堆:对象存储单位,代表着数据,所在区域不连续,会有碎片
    栈:方法运行时单位,代表着逻辑,所在区域连续,没有碎片
  2. 共享性
    堆:线程共享
    栈:线程独占
  3. 大小及分配方式
    堆:程序员自己申请 大,速度比较慢,易产生内存碎片
    栈:系统自动分配 小,速度较快,程序员是无法控制的
  4. 异常
    堆:堆空间不足 java.lang.OutOfMemoryError
    栈:栈溢出 java.lang.StackOverFlowError;栈扩展失败 OutOfMemoryError
    (1)虚拟机中的堆一定是线程共享的吗
    不一定 ,为了保证对象的内存分配过程中的线程安全性,引入了TLAB
    TLAB 在内存分配上是线程独占的,在读取数据上是线程共享的

常量池

常量池为了避免频繁的创建和销毁影响系统性能,实现了对象共享。
例如字符串常量池,在编译阶段就把所有字符串放到一个常量池中,节省内存空间,节省运行时间。

  1. Class文件常量池\类常量池 Constant Pool Table
    Class文件常量池:Class文件中的资源仓库,在编译后产生,每个Class字节码文件都有一个Class文件常量池,JVM类加载.class,将类的元数据置于方法区中(在运行期加载到方法区中去)
    加载:1.class文件信息Class对象加载到方法区;2.Class文件常量池会随着加载到运行常量池中
    Class文件:描述信息(类的版本、字段、方法、接口等)+常量池(存放编译期生成的各种字面量和符号引用)
    • 字面量:双引号字符串和常量:文本字符串,被声明为final的常量值 private int value = 1;
    • 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 com.a.Test、value、main()
      动态链接:符号引用只有到运行时被加载到内存后,这些符号才有对应的内存地址信息。这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时,会被转变为被加载到内存区域的代码的直接引用
json 复制代码
//.class字节码文件内容:
cafe babe       0000      0033       0010            0a00 0300 0d07  
魔数          次版本号    主版本号    常量池计数器       常量池            方法...
查看字节码文件:javap -verbose Test.class ,其中constant pool table代表class常量池
魔数(用来确定一个文件能否被JVM接受)、版本号、常量池、类、父类、和接口数组、字段、方法等信息
  1. 运行时常量池 Runtime Constant Pool
    运行时常量池:每个已加载的类都会有一个对应的运行时常量池,用于存储常量、符号引用(包括对应的直接引用)和一些编译期已知的常量数据
    • 运行时常量池具有动态性:Class文件常量池+运行时产生的常量(1.7前字符常量池)
      • 编译期的常量池的内容可以进入运行时常量池(ldc) 懒加载
      • 运行时产生的常量也可以放入池中String.intern()
    • JDK 1.7,虽然两者位置不同,但是根据虚拟机规范,字符串常量,需要放在运行时常量池中
      • 字符串池就是运行时常量池的一个逻辑子区域。即字符串池是运行时常量池的分池
json 复制代码
String s = "黄河之水天上来";
编译期-class常量池:字面量   "黄河之水天上来"
jvm-运行期->运行时常量池 "黄河之水天上来"   不是对象,只是字面量-常量
--->
1.程序执行到这一行,根据字面量去 字符常量池 中,查找对应字面量的"引用"  懒加载
ldc(JavaThread* thread, bool wide)) :将int、float或String型常量从常量池推送至栈顶
2.没有,在 字符常量池 创建String对象,并返回引用;

版本变化: 运行时常量池在方法区中,1.7及之前实现是永久代,JDK1.8及之后是"元数据区"元空间

  1. 字符串常量池 String Pool
    字符串常量池:来保存字符串的常量池,
    在HotSpot虚拟机中,实现为StringTable,底层是一个c++的hashtable <字面量,引用>
    String Pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的
    版本变化: JDK6->JDK7:static变量、字符串常量池移到堆里
    垃圾回收:字符串常量池本身不会被GC,如果一直不回收处于总是"只进不出"的状态,很可能会导致内存泄露
    Full GC时,对StringTable<>进行可达性分析,引用对象不可达,移除这个引用,并且销毁执行的String对象
    组成部分:ldc 、String.intern()
json 复制代码
intern()
1.JDK1.6:如果存在,则返回常量池中的引用;不存在,复制一个副本放到常量池
2.JDK1.7&JDK1.7+:如果存在,则返回字符串常量池中的引用;不存在,将在堆上的地址引用复制到字符串常量池

// 语句1
String s3 = new String("a") + new String("b");

// 语句2
s3.intern();

// 语句3
String s4 = "ab";

// 语句4
System.out.println(s3 == s4);


问题1:语句4结果?
如果是JDK1.6及以前的版本,结果就是false;而如果是JDK1.7开始的版本,结果就是true
问题2:语句1有几个对象?
"a" 、"b"、 new String("a")、new String("b")、 
new StringBuilder、new StringBuilder("a").append("b").toString()
  1. 基本类型包装类对象常量池
    除了Float和Double,其他类型都存在常量池,当然这些常量池是有缓存范围 - 享元模式
    对应值在缓存范围内,可以使用对象池,超出范围需要创建对象

垃圾回收

判断对象是否存活

  1. 引用计数法

    给对象添加引用计数器,有引用就+1,引用失效就-1。任何时刻,引用为0,即判断对象死亡

    优点:实现简单,效率高

    缺点:在主流的Java虚拟机中不被使用,无法解决对象相互循环引用的问题

    循环引用:a引用b,b又引用a,但是ab没有被其他引用,应该被回收(对象)

  2. 可达性分析算法

    从根引用(GCRoots)进行"引用链"遍历扫描,如果可达则对象存活,如果不可达则对象已成为垃圾

    缺点:STW时间长(解决三色标记法);内存消耗(需要存储大量的对象数量和引用关系)

    如果要使用可达性分析来判断是否可以回收,需要在一个一致性快照中进行STW

    • GC Roots 当前一定不能回收的对象
      • 虚拟机栈中引用的对象:正在运行的线程\方法,不能回收
      • 本地方法栈中引用的对象:本地方法native
      • 方法区中类静态属性引用的对象:static 对象,随着类的存在而存在
      • 方法区中的常量引用的对象: static final ; public static final A ACONSTANT = new A()
    • 对象不可达,一定会被垃圾收集器回收吗: finalize() "自我拯救一次" 官方不推荐使用
      • 第一次标记和筛选
        • 标记:通过可达性分析算法,将不可达对象标记为白色,筛选:是否要执行 finalize() 方法
        • 筛选:对象没有覆盖 finalize() 方法 或者 finalize() 方法已经被虚拟机调用过
      • 第二次标记
        • 如果对象有必要执行finalize() 方法 (finalize() 方法被覆盖并且没有执行过),将对象放到 F-Queue 的队列中排队,稍后由一条虚拟机自动建立的、低优先级的 Finalizer线程 去触发方法,稍后GC将对F-Queue中的对象进行第二次小规模标记。如果在队列中的对象连接上GC Roots引用链,那么在第二次标记时,将其移除出"即将回收"的集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收

  3. 三色标记法

    属于可达性分析的一种,可以大大的降低STW的时长

  • STW、 safe point 、Safe Region
    • STW Stop-The-World 全局停顿
      • 执行任何垃圾收集算法时,Java应用程序的其他所有线程都被挂起,且不能彻底避免的,只能尽量降低STW的时长(所有Java代码停止,native代码可以执行,但不能与JVM交互)
    • safe point 安全点
      • 当线程执行到这个位置的时候,可以被认为处于"安全状态",如果有需要,可以在这里暂停,当JVM需要对线程进行挂起的时候,会等到安全点在执行
      • 线程挂起场景: STW、获取Dump、死锁检测
    • Safe Region 安全区域
      • 用于处理无法立即响应到达安全点请求的线程
      • 例如:长时间的计算操作,是不会与GC操作冲突,不会改变对象的引用关系,这种代码区域就可以称之为安全区域。
      • 当线程运行到安全的代码\安全区域时,JVM认为线程虽然没在安全点,但是因为处于安全区域,也可以进行正常的GC操作,当这段代码执行完了,要退出安全区域的时候,就需要检查一下,自己能不能退出去,比如看看GC是否在运行。
    • https://blog.csdn.net/WZH577/article/details/109782827

三色标记法

可以减少JVM在GC过程中的STW时长。 CMS、G1等主要使用的标记算法

(1)引用计数法、可达性分析算法问题

循环引用;SWT时间过长

(2)三色标记法将对象分为三种状态:白色、灰色和黑色。

白色:未标记

灰色:已标记,引用对象(相当于)未标记完

黑色:已标记,引用对象已标记完

(3)标记过程的三个阶段

  1. 初始标记 STW
    遍历所有GC Roots,将 GC Roots 和 GC Roots的下一级的对象标记为灰色
    只会扫描被直接或者间接引用的对象,而不会扫描整个堆,因此这个过程其实很快
  2. 并发标记 没有STW
    遍历GC Roots的下游,从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色,重复此步骤直到灰色对象集合为空。
    在并发标记阶段采用多线程,用户线程与标记线程并发执行,没有STW,降低GC停顿时长,但应用程序线程可能会修改对象图,因此垃圾回收器需要使用写屏障技术来保证并发标记的正确性。耗时最久
  3. 重新标记 STW
    再标记一次,标记在并发标记阶段中被修改的对象以及未被遍历到的对象。
    STW原因:重新遍历灰色对象,如果不停顿,用户线程还是继续执行,那么这个GC永远可能也做不完了
  4. 垃圾回收器会执行清除操作,清除未被标记对象
    垃圾回收器会将所有未被标记的对象标记为白色
    (4)三色标记算法的漏洞:并发标记过程,会导致出现多标,漏标问题
  • 多标:多余标记,应该回收的对象让它存活了,会产生浮动垃圾
    • 可以容忍,下次垃圾回收周期会把它们清除掉

在并发标记阶段,D->E,E为灰色,如果存在用户线程执行D.E = null,则D->E引用链断开,但由于E已经被置为灰色,导致存活,进而导致F、G存活,多标问题

  • 漏标:遗漏/忘记标记,应该存活的对象,被回收了,导致新引用被标记不可达
    • 严重问题,一个存活对象被回收掉

在并发标记阶段,D->E,E为灰色,如果存在用户线程执行D.G->G,E.G->null ,此时E为灰色,G仍然为白色,即使E->G存在引用链,但是它不会被扫描和被标记为灰-黑色,标记环节结束时,会把对象G当做垃圾清除掉

  • 破坏漏标发生的充要条件
    • 黑色对象D新增了对白色对象G的引用 (满足条件1):增量更新
      • 至少有一个黑色对象被标记后,又存在一个白色对象的引用
    • 灰色对象E指向白色对象G的引用被断开了 (满足条件2): 原始快照
      • 所有的灰色对象在引用扫描完成之前删除了对白色对象的引用
  • 增量更新:实时记录变化,确保每一次变化都会被重新检查
    • 解决方案:
    • 如果有黑色对象被标记后,又存在一个白色对象的引用,记录黑色对象的引用,在重新标记阶段以黑色引用为根,重新扫描
    • 缺点:浪费时间(重新扫描黑色链,会浪费时间,但漏标情况较少,可以接受)
    • 回收器: CMS垃圾收集器使用增量更新方案
  • 原始快照:基于GC开始时的状态做决策,忽略之后的变化,确保GC的稳定性和一致性
    • 解决方案:
    • 如果灰色对象在扫描完成前删除了对白色对象的引用,在灰色对象取消引用之前,先将灰色对象引用的白色对象记录下来,在重新标记阶段以白色对象为根,重新扫描
    • 缺点:
    • 多标问题(D->G正常;断开);需要更多内存
    • 回收器: G1垃圾收集器使用原始快照方案
      (5)读写屏障
      可以理解成就是在写操作前后插入一段代码,用于记录一些信息、保存某些数据等,概念类似于AOP
  • 增量更新:针对新增引用的情况下,就是在赋值操作之前添加一个写屏障,在写屏障中记录新增的引用(将黑色存为灰色,在重新标记阶段,重新遍历链路)
  • 原始快照:针对引用减少的情况下,就是在赋值操作(置空)执行之前添加一个写屏障,在写屏障中记录被置空的对象引用

跨代引用

(1)跨代引用问题:

跨代引用:指在Java堆内存的不同代之间存在引用关系,导致对象在不同代之间的引用

比如:新生代到老年代的引用,老年代到新生代的引用等

假如我们进行YoungGC,从GC Roots进行可达性分析,如果一个对象被老年代对象引用,为了判断新生代中某个对象是否存活,需要额外遍历整个老年代来确保可达性分析的正确性,反过来也是一样。这种方案成本太高

注:不仅新生代、老年代存在跨代引用,G1的rigen之间也存在跨代引用,即所有涉及到部分区域收集的收集器都面临这样的问题。

简单思想:标记一下有没有额外的引用,于是定义了一个全局的数据结构------Remembered Set

(2)Remembered Set 记忆集

用于记录从非收集区域指向收集区域的指针集合的抽象思想,卡表是记忆集的一种实现方式

例如:在分代GC中,通常只能单独收集的只有Yong gen,那记忆集记录的就是Old gen指向Young gen的跨代指针,那就不需要遍历整个老年代了,只需扫描Remembered Set中的条目,从而减少了扫描的开销

(3)Card Table 卡表

HotSpot虚拟机中,卡表最简单的形式可以只是一个字节数组,采用空间换时间思想,不需要扫描整个老年代空间

HotSpot:

CARD_TABLE[this.address>>9]=0;

字节数组每一个元素都对应一块固定大小的内存块,称为"卡页" Card Page

一个卡页大小为2(9)(512字节),从CARD_TABLE[0]开始对应第一块。一个卡页通常包含不止一个对象,只要其中有一个\多个对象存在跨代引用指针,那就将对应卡表的数组元素的值标识为1,使这个元素变脏(Dirty),没有则标识为0

在垃圾收集发生时,只要筛选出卡表中变脏的元素(包含跨代指针),把他们加入GC Roots中一并扫描

注意:新生代对象引用老年代对象时,老年代对象所在的区域不会被标记为脏页,即只有老年代引用新生代才会处理卡页

(4)Logging Write Barrier 写屏障

写屏障是为了维护卡表:例如他们何时变脏、谁把他们变脏等->只要引用字段赋完值进行记录到卡表中

垃圾回收算法

对象无法存活,判定为垃圾,如何回收?垃圾回收算法

标记清除法、复制算法、标记整理法、分代收集算法

  1. 标记-清除算法: 老年代(CMS,为了降低STW的时长 )
    • 标记阶段:利用可达性分析算法遍历内存,标记所有的活动对象
    • 清除阶段:再遍历一遍内存,将未被标记的对象清除

优点:速度快,因为不需要移动和复制对象

缺点:导致空间不连续,产生比较多的内存碎片,可能导致后续没有连续空间,需要进行一次GC

  1. 复制算法:年轻代
    • 首先将内存划分成两个区域,每次只使用一块
    • 当快满的时候,将标记出来的存活的对象复制到另一块内存区域中,然后对整个之前的空间进行垃圾回收,将未复制的垃圾对象清理掉。

优点:

内存空间是连续的,不会产生内存碎片

不需要标记,直接对存活对象(对象的指针是否被引用)进行复制;只处理存活对象,不处理垃圾对象

缺点:浪费了一半的内存,复制对象会造成性能和时间上的消耗

  1. 标记-整理算法:老年代
    • 标记阶段:利用可达性分析算法遍历内存,标记所有的活动对象
    • 整理阶段:移动所有存活对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收

特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;

优点:不会产生内存碎片;不会浪费内存空间

缺点:太耗时间(性能低)

  1. 分代收集算法:总纲
    根据内存对象的存活周期不同,把Java堆分为新生代和老年代
    通过将不同时期的对象存储在不同的内存池中,就可以节省宝贵的时间和空间,从而改善系统的性能。
  1. 新生代 Young: 占总空间的 1/3
    • Eden 8 : Survivor From 1: Survivor To 1
    • 复制算法:有大量对象死去和少量对象存活,付出少量存活对象的复制成本就可以完成收集
  2. 老年代:Old :占总空间的 2/3
    • 标记清理算法、标记整理算法:对象的存活率极高,没有额外的空间对他进行分配担保
  3. 为什么要分代?为什么年轻代要分3份?
    • 分代:老年代,长期存活,空间大;年轻代:朝生夕死,不确定是否存活
    • 年轻代要分3份:朝生夕死,复制算法

(1)Java代码如何调用垃圾回收

通过使用系统类的方法:System.gc();

通过使用运行时类方法:Runtime.getRuntime().gc();

这两个方法用来提示 JVM 要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于 JVM 的。

垃圾回收过程

垃圾回收对象转化流程

  1. 对象创建先分配在Eden区
    • 对象最开始分配在 Eden区 ,如果Eden区没有足够的空间时,触发 yonggc
    • 把yonggc后仍然存活的对象移动到 Survivor From区,Eden区 清空,然后再次分配新对象到 Eden区
    • 如果再触发gc,采用复制算法,把 Eden区存活的和 Survivor From区存活的对象转移到另一块空着的Survivor To区
    • 每移动\gc一次,对象年龄+1,对象年龄到达15次进入老年代
  2. 大对象直接进入老年代
    大对象是指需要大量连续内存空间的对象,比如:字符串、数组
    这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝
  3. 长期存活的对象进入老年代
    虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阈值(15)对象进入老年区。
  4. 动态年龄分配
    如果在Survivor空间中小于等于某个年龄的所有对象大小的总和大于Survivor空间的一半时,那么就把大于等于这个年龄的对象都晋升到老年代。

从年龄小的对象开始,不断地累加对象的大小,当年龄达到N时,刚好达到TargetSurvivorRatio这个阈值,那么就把所有年龄大于等于N的对象全部晋升到老年代去

  • 对象太多:都达到一半Survivor,数量太多,每次yonggc都需要移动很多对象
  • 从N年龄后的对象,大概率已经经过了多次回收,希望那些可能是长期存活的对象,尽早进入老年代
  1. 空间分配担保
    每次进行Yong GC之前,会进行空间分配担保。

对象什么时候进入老年代

  1. 15次
    在对象头中,分代年龄占4bit,即2(4)-1 (1111)
    设置年龄: -XX:MaxTenuringThreshold
  2. 大对象
    设置阈值:-XX:PretenureSizeThreshold
  3. 动态年龄分配
  4. 空间担保机制

空间分配担保原则

(1)问题

如果Survivor区域的空间不够,就要分配给老年代。但是,老年代也是可能空间不足的。

所以,在这个过程中就需要做一次空间分配担保(CMS),来保证对象能够进入老年代

(2)空间分配担保机制

  1. 在进行Minor GC之前,JVM首先会检查【老年代最大连续空闲空间】是否大于【当前新生代所有对象占用的总空间】
  2. 如果大于,那么说明此次的Minor GC是安全的,可以放心的进行Minor GC
  3. 如果小于,则JVM会去查看HandlePromotionFailure参数的值是否为true(表示是否允许担保失败,1.7就不在支持了,直接到第5步)
  4. 如果允许担保失败,则此时JVM会去检查【老年代最大连续空闲空间】是否大于【历次晋升到老年代的对象的平均大小】
  5. 如果不允许担保失败,则此时就会进行一次Full GC 以腾出老年代更多的空间
  6. 如果小于,则JVM此时会进行一次Full GC,以便于腾出更多的老年代空间
  7. 如果大于,则JVM会冒险进行一次Minor GC
    1、存活对象<survivor,存活对象进入survivor区中
    2、存活对象>survivor,存活对象<老年代可用空间,直接进入老年代
    3、存活对象>survivor,存活对象>老年代可用空间,就触发了 Full GC
    如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了

Yong GC 、 Old Gc 、Full GC

Yong GC:主要用于对新生代垃圾回收 Minor GC

  1. Eden区满了之后就会触发minor GC清除年轻代中的垃圾
  2. Parallel Scavenge垃圾回收器,Full GC 前先执行一下 Yong GC
    Old GC:主要用于对老年代垃圾回收 Major GC
  • 老年代内存不足:当老年代空间不足以存放新的晋升对象或存活对象时,就会触发Major GC
  • 老年代使用率达到阈值:一些JVM实现中,当老年代的内存使用率达到一定阈值时,也可能触发Major GC
    Full GC:全堆回收:新生代、老年代、永久代
  1. 老年代内存不足:当老年代空间不足以存放晋升对象时,可能触发Full GC
  2. 空间担保失败
  3. 永久代空间不够或者是超过了临界值,会触发完全垃圾回收
  4. 执行System.gc()、jmap -dump 等命令

垃圾回收器

1)常见的垃圾回收器

(1)串行垃圾回收器:Serial, Serial Old

(2)并行垃圾回收器:Parallel Scavenge,Parallel Old,ParNew

(3)并发标记清除垃圾回收器:CMS

(4)G1垃圾回收器(JDK 7中推出,JDK 9中设置为默认)

(5)ZGC垃圾回收器(JDK 11 推出,JDK 17默认)

新生代收集器有Serial、ParNew、Parallel Scavenge

老年代收集器有Serial Old、Parallel Old、CMS

整堆收集器有G1、ZGC

(2)默认垃圾收集器

JDK1.8:Parallel Scavenge(新生代)+Parallel Old(老年代)

JDK1.9:G1

CMS与G1

CMS 并发标记清除垃圾回收器

以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

"并发" "低停顿"

(1)回收过程

  1. 初始标记:
    标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
  2. 并发标记:
    gcroot的下游
    根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有STW。
  3. 重新标记:
    顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
  4. 并发清理:
    GC线程清除不可达的对象,并回收它们占用的内存空间。这个阶段与应用线程并发执行,不需要STW。
    (2)CMS的问题是什么
  5. 并发回收导致CPU资源紧张:
    并发阶段,虽然不会导致用户线程停顿,但因为占用一部分线程而导致应用程序变慢,降低程序总吞吐量
    CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
  6. 无法处理浮动垃圾:
    在并发清理节点,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,只有等到下一次GC时,才能被清理掉,这部分垃圾叫浮动垃圾。
  7. 并发失败
    CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年代直接分配(大对象),但是老年代没有足够的空间来分配,所以导致concurrent mode failure, 一旦出现此错误时,便会 STW 切换到SerialOld收集方式,这样一来停顿时间就很长了
    默认情况下:+XX:CMSInitiatingOccupancyFraction 老年代使用 92%空间,会触发 CMS 垃圾回收
  8. 内存碎片整理
    CMS使用"标记-清理"会产生大量的空间碎片,会给大对象分配带来麻烦,会出现老年代还有很多剩余空间,但无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
    解决:在 Full GC 时开启内存碎片的合并整理过程:-XX:UseCMSCompactAtFullCollection 默认开启0
    由于这个内存整理必须移动存活对象,是无法并发的,虽然空间碎片没有了但是停顿时间变长了

G1 Garbage First

https://www.jianshu.com/p/477a0f2b2164

G1 收集器不采用传统的新生代和老年代物理隔离的布局方式,仅在逻辑上划分新生代和老年代,将整个堆内存划分为2048个大小相等的独立内存块Region,每个Region是逻辑连续的一段内存,具体大小根据堆的实际大小而定,整体被控制在 1M - 32M 之间,且为2的N次幂(1M、2M、4M、8M、16M和32M),并使用不同的Region来表示新生代和老年代,G1不再要求相同类型的 Region 在物理内存上相邻,而是通过Region的动态分配方式实现逻辑上的连续。

G1收集器通过跟踪Region中的垃圾堆积情况,每次根据设置的垃圾回收时间,回收优先级最高的区域,避免整个新生代或整个老年代的垃圾回收,使得stop the world的时间更短、更可控,同时在有限的时间内可以获得最高的回收效率。

通过区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。

G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。

(1)分区Region:

G1 垃圾收集器将堆内存划分为若干个 Region,每个 Region 分区只能是一种角色 E S O H,空白区域代表的是未分配的内存。

H:存放巨型对象,对象的大小超过Region容量的50%以上,对于其他回收器这个对象默认会被分配在老年代,但可能是个短期存活的巨型对象,可能导老年代频繁GC,G1划分了一个H区专门存放巨型对象,如果寻找不到连续的H区的话,就会 Full GC

(2)Remembered Set RSet

为了避免整堆扫描,为每个分区各自分配了一个 RSet,记录了其它 Region 对当前 Region 的引用情况。

当回收某个Region时,只需扫描它的 RSet 就可以找到外部引用,来确定引用本分区内的对象是否存活,

进而确定本分区内的对象存活情况

注意:只记录老年代到新生代的引用,且不是同一分区的

如果引用源在本分区,不需要记录; G1 每次 GC 时,所有的新生代都会被扫描,引用源是年轻代的对象也不需要记录,只需要记录老年代到新生代之间的引用

(3)Card Table:

RSet是一个概念模型。实际上,Card Table 是 RS 的一种实现方式。类似于跨代引用那里的介绍

G1对内存的使用以分区(Region)为单位,而对象的分配则以卡片(Card)为单位。

(4)回收过程

  1. 初始标记(会STW)
    标记 GC Roots 根及其根下一级,个阶段需要停顿线程,但耗时很短
  2. 并发标记
    标记 GC Roots 引用链,耗时较长,但可与用户程序并发执行
  3. 最终标记(会STW)
    对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象
  4. 筛选回收(会STW)
    对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
    这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的
    CMS 与 G1 区别
  5. 使用范围及回收算法
    CMS:老年代;标记-清除算法,容易产生内存碎片
    G1:整堆;标记-复制算法回收年轻代、标记-整理算法回收老年代,没有内存空间碎片
  6. STW时间
    CMS:以最小的停顿时间为目标,无法预测停顿时间
    G1:可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
  7. 垃圾回收过程
    CMS:初始标记,并发标记,重新标记,并发清理
    G1:初始标记,并发标记,最终标记,筛选回收
相关推荐
BD_Marathon2 小时前
【Flink】部署模式
java·数据库·flink
鼠鼠我捏,要死了捏5 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw5 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友5 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls5 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh5 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫6 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong6 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊7 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉7 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源