Java虚拟机原理剖析

文章目录

一、JVM基础

在这里插入图片描述

1.1 Java虚拟机简介

1、是什么:

  • 是一种抽象化的计算机,有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统
  • 是一种规范,可用不同的方式加以实现,对应不同的虚拟机

2、内容:一个指令集、堆栈、 “垃圾堆”和一个方法区

3、Java语言跨平台

  • Sun以及其提供商提供了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(.class文件)

  • .Java文件可以被被编译成.class

  • 一旦一个JVM在给定的平台上运行,此.java文件都能在这个平台上运行,如图2所示
    在这里插入图片描述

  • 特点:JVM仅与字节码.class文件有关,与任何语言无关

4、Java和Jvm的关系:

  • Java发布之初,就考虑不止让Java也要让其他语言运行在JVM上(时至今日,可以在JVM上执行的常见语言有:Python、Groovy、JS、PHP、Lua)
  • java规范分为java语言规范和JVM规范。

5、常见的Jvm实现

  • JRockit:BEA公司的虚拟机,全部代码都靠即时编译器编译执行由于在GC方面的优异表现,被Oracle收购

  • HotSpot: 由于LongView技术公司设计,后来因为其在JIT编译上的优异表现被Sun收购,06年ORacle收购了Sun

    现在我们使用的HotSpot = 原HotSpot(取JIT) + JRockit(取GC)

  • Zing:Azul公司的虚拟机,是真正的高性能虚拟机。一个VM实例可以管理至少数十个CPU+几百G内存资源,实现可控的GC时间

1.2 指令

1、定义

指令 = 操作码 + 操作数

​ getstatic #2 :表示去.class文件常量池中[02]处,获取静态字段的值

1.3 执行引擎

混合模式:解释器和JIT即时编译器的共存,使得HotSpot执行引擎能够平衡启动速度和运行效率

1、解释器解释执行

特点:逐条解释字节码指令进行执行

2、JIT及时编译器编译执行

特点:一整段执行

3、HotSpot执行引擎执行过程

1)每当执行完一条指令(操作码 + 操作数)后,PC寄存器会更新下一条需要被执行的指定的地址

2)解释执行阶段

  • 当Java程序启动时,HotSpot执行引擎首先采用解释器,逐条解释执行字节码指令

    即读取字节码指令,将其翻译成对应的机器码,并直接由硬件执行

  • 优势

    可以快速启动程序(不需要等待编译器编译整个程序)、节省内存(只需要加载和执行当前需要的代码)

3)编译执行阶段

  • 热点代码探测:在执行过程中,HotSpot执行引擎会收集代码的执行情况,特别是那些执行频率高的代码块(称为“热点代码”)。热点代码探测通常基于计数器(如方法调用计数器)来实现
  • 即时编译
    • 一旦热点代码被探测到,HotSpot执行引擎就会将这些代码提交给JIT即时编译器进行编译
    • JIT编译器会将热点代码编译成与本地平台相关的机器码,并进行优化以提高执行效率
    • 编译后的代码会被缓存起来,以便后续再次执行时直接使用。这样,热点代码的执行速度就会大大提高

1.4 Jvm基于栈的指令集架构

源于: Java语言的跨平台特性

优点

  • 可移植性好:基于栈的指令集架构不依赖于特定的硬件寄存器,因此可以更容易地实现跨平台的操作
  • 实现简单:基于栈的指令集架构在设计和实现上相对简单

缺点

  • 执行性能:与基于寄存器的指令集架构相比,执行相同操作时需要更多的指令数量,影响执行性能

二 、class文件

2.1 字节码简介

1、是什么:二进制01文件

2、如何生成: .class文件即字节码,是.java文件被编译后(javac)生成的文件,如图3所示
在这里插入图片描述

3、使用IDEA查看:

  • IDEA安装jclassLib Bytecode插件
  • IDEA View栏下使用Show Bytecode with jclassLib ,如图5所示
    在这里插入图片描述

2.2 class文件内容

大纲不展示问题

2.2.1 一般信息

如图6所示
在这里插入图片描述

  • 主版本号:52-即Java8
  • 访问标志

类是public的、final等修饰的

  • 本类索引:#9即在常量池[09]中:全类名
  • 字段计数:成员变量个数
  • 方法计数:方法的个数

2.2.2 常量池

1、是什么:class文件的资源库

2、内容:14种不同结构的表数据

如:符号引用和字面量等

2.2.2.1 符号引用

class_info

类的全限定名:com/mjp/类名

Fieldref_info

  • 字段所述的类|接口:字段所在类信息
  • 字段名(name)和字段描述符(java/lang/String)

methodRef_info
在这里插入图片描述

名字和描述符:无参构造方法,在常量池[33]处有描述符

  • 名字<<init>>:即构造方法
  • 描述符()V:方法入参为空()、方法返回值为V即void

2.2.2.2 字面量

string_info

private String name = "mjp";

字符串字面量:cp_info #29 <mjp>

Integer_info

private final int a = 10;

Integer:10

2.2.2.3 字面量和符号引用

1、字面量

1)类型:string_info、Integer_info等

2)分类

  • 文本字符串:String name = “mjp”; cp_info # 4
  • 常量:final int a = 1;

二者均存在于常量池中,一个是[13]CONSTANT_Integer_info处、一个是[29]CONSTANT_utf8_info字符串常量池中

2、符号引用

1)类型:class_info、Fieldref_info、methodRef_info等

2)分类

  • class_info:类、接口全限定名,cp_info # 1
  • Fieldref_info:字段名称 和 字段描述符,cp_info # 2
  • methodRef_info:方法名、方法入参、方法返回值类型,cp_info # 3

3、符号引用的作用

  • 符号引用,保存了方法(所属类、描述符)的信息
  • 在类加载过程时,可以通过符号引用,动态链接到直接内存地址引用

这样使得Xxx.class文件更小


2.2.3 字段

private String name;
  • 名称:name
  • 描述符:String
  • 访问标志:public

2.2.4 方法

1、默认的无参构造方法

  • 名称:<<init>>
  • 描述符:<()V>表示方法的返回值类型是void、方法的入参为空
  • 访问标志:public

2、自定义方法

public void func(Integer i, String s) {
     this.map.put(s, i);
}
  • 名称:<func>
  • 描述符:(String;Integer;)V表示方法的返回值类型是void、方法的入参为String 和 Integer类型
  • 访问标志:public
3、方法调用字节码指令invoke
public class Demo{
	private void func(Animal animal, Map<String, Integer> map) {
        // invokevirtual,调用的多态方法
        animal.eat();
        
    	// invokeinterface,调用的接口方法
        map.put("mjp", 18);
        
        // invokestatic,调用的类方法
        f1();
        
        // invokespecial,调用的对象私有方法
        f2();
        
        // invokevirtual,调用的实力方法
        f3();
    }

    // invokestatic
    public static void f1() {

    }
    
	// invokespecial
    private void f2() {

    }
    
	// invokevirtual
    public void f3() {
    }
}
  • invokevirutal: 当你调用一个对象的非静态、非私有方法,并且这个方法在编译时不能确定具体调用哪个类的方法时(即存在继承和多态的情况) 或者 实力方法(非静态、非私有)

    上述animal.eat()即多态在编译时期无法确定调用是父类还是子类的eat方法、对象本身的实力方法-f3()

  • invokespecial:调用对象的private私有方法、构造方法、父类继承方法-f2

  • invokeinterface:调用接口方法,上述map.put即调用的Map接口的put方法

  • invokestatic:调用的类方法-f1()

  • invkoedynamic:使用了lambda表达式

三、类加载

3.1 类加载过程

分为三个阶段:加载、链接、初始化

1、加载(Loading)

  • 通过类的全限定名,获取此类的二进制字节流class文件
  • 将class文件存储的静态结构,转换为方法区的运行时数据结构
  • 并在堆中生成一个代表此类的Class对象,作为这个类在方法区中各种字段、方法的访问入口

2、链接

1)验证(Verification)

确保Class文件的字节流中包含的信息符合JVM规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证内容

2)准备(Preparation)

  • 在方法区中为静态变量分配内存、并设置默认值
public static int value = 1;

在准备阶段过后,value的值为0,而不是1。赋值1的动作在初始化阶段进行

3)解析(Resolution)

动态链接:将常量池中的符号引用转化为直接引用的过程。

3、初始化(Initialization):<clinit>()

此时Java程序代码才开始真正执行

  • 为类的静态变量赋予初始值

    • 正常情况下:在编译时就完成了赋值
    public static final int a = 1;
    
    • 特殊情况:对于复杂表达式,即使是static final修饰,也不在编译时完成赋值,而是在此处初始化阶段
    public static final int a = new Random().next(10);
    
  • 执行 static {} ,仅在此执行一次

  • 执行其父类的<clinit>()

4、使用

除了上述1-3类的加载过程外,类的生命周期还包括4-5

  • new 类

5、卸载

当堆中代表类的Class对象不再被引用,即不可达时,表示类的生命周期结束

  • GC会回收这部分堆内存
  • 同时卸载类在方法区的数据(方法、字段)

JVM自带的类加载器所加载的类(String类等核心类),在虚拟机的生命周期中通常不会被卸载


3.2 类加载时机

JVM规定了五种情况,需立即对类进行初始化(自然的,加载和链接则需要在此之前开始)

1、如果类没有进行过初始化,此时遇到newgetstaticputstaticinvokestatic这四条字节码指令时,则触发此类的初始化

2、首次访问(读取或者设置)这个类的静态变量(被final修饰、已在编译期把结果放入常量池的静态字段除外)和调用静态方法的时候。

3、如果类型没有进行过初始化,使用了反射获取此类的class对象,则触发此类的初始化

A.class;
Class.forName(xxx.xxx.A);

4、当类进行初始化的时,如果其父类还未初始化过,则对其父类进行初始化

5、当虚拟机启动时,会先初始化main方法所在的主类


3.3 类加载器的层次结构

Java的类加载器采用了双亲委派模型(Parents Delegation Model)。这个模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。

1、启动类加载器(Bootstrap ClassLoader)

  • 根类加载器负责加载Java核心库,如rt.jar(javax、lang、util、io等)
  • 由JVM使用C++实现的

2、扩展类加载器(Extension ClassLoader)

  • 负责加载JRE的扩展目录(lib/ext)中的jar包和类库
  • Java实现,由根类加载器完成加载

3、系统类加载器(System ClassLoader):应用类加载器(Application ClassLoader),加载类路径CLASSPATH下的类。一般来说,用户编写的类都是由它来完成加载的

4、自定义类加载器

1)自定义

  • 如果不打破双亲委派机制-参考:ApClassLoader(本质继承ClassLoader,重写findClass方法)

  • 如果打破双亲委派机制-参考:WebappClassLoader

2)场景

  • 用于加载非标准路径下的类,如网络上的类文件
  • 隔离记载类,尤其是不同版本的相同类

5、上下文类加载器

1)定义

  • 是Thread类的一个属性
  • 每个线程都可以有自己的上下文类加载器,在运行时动态加载与当前线程上下文相关的类
  • 在JDBC -SPI机制中,使用当前线程的上下文类加载器来加载JDBC驱动-con.mysql.driver.Driver

2)显示设置

// 1.显示设置-自定义类加载器为当前线程的上下文加载器
MyClassLoader myClassLoader = new MyClassLoader();
Thread.currentThread().setContextClassLoader(myClassLoader);

// 2.显示设置-系统类加载器为当前线程的上下文加载器
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());

3)默认为系统类加载器关系

  • 如果没有显式地为当前线程设置上下文类加载器,则默认继承自其父线程-通常是main线程
  • 如果main线程也未显式设置上下文类加载器,则使用系统类加载器作为上下文类加载器

3.4 类型安全机制

Java的类型安全机制依赖于

  • 类的完全限定名(包括包名和类名)和类加载器
  • 两个类如果由不同的类加载器加载,即使它们的完全限定名相同,它们也被视为不同的类型
  • 这意味着,如果一个接口是由一个类加载器加载的,而其实现类是由另一个类加载器加载的,那么这些类之间就无法正确地实现多态和类型转换,因为JVM会认为它们是两种完全不同的类型
  • 所以,通常情况下接口和实现类应由相同的类加载器加载

3.5 双亲委派机制

3.5.1 流程

当需要加载一个类时

  • 首先委托给父类加载器去加载
  • 如果父类加载器能够加载该类,就成功返回
  • 如果父类加载器加载不到,子类加载器才会尝试自己去加载

3.5.2 api

  • Class<?> loadClass(String name) :加载name类,返回此类的class对象

  • Class<?> findLoadedClass(String name):查找name类是否被加载,若为null则说明未被加载。如果已被加载则返回class对象

  • Class<?> findClass(String name):调用子类加载器去加载name类

    • 若子类加载器加载范围包括name类,则可以加载,并返回class对象
    • 否则返回null
    • 具体实现,可以参考ApClassLoader,底层调用defineClass
      • 根据类的全路径名称,找到.class文件
      • 将.class文件转换为运行时方法区的动态数据结构
      • 返回此类在堆中的class对象,作为此类在方法区字段、方法的访问入口
  • Class<?> defineClass(String name, byte[] b, int off, int len):将.class(二进制字节数组) --> .java(class对象)

  • ClassLoader getClassLoader():使用class对象调用此方法,返回加载此类的类加载器

  • getSystemClassLoader():获取系统类加载器


3.5.3 双亲委派机制源码-loadClass

protected Class<?> loadClass(String name, boolean resolve){
    synchronized (getClassLoadingLock(name)) {
    // 1.校验下这个类是否已经被加载过了
    Class<?> c = findLoadedClass(name);
    // 2.没有被加载过
    if (c == null) {
        // 2.向上委派
        // 2.1如果当前类加载器不是启动类根类加载器,则向上委派给其父类加载器去加载
		if (parent != null) {
            // 递归-委派
            c = parent.loadClass(name, false);
         } else {
            // 2.2如果当前类加载器已经是根类加载器了,则让其尝试去加载
            c = findBootstrapClassOrNull(name);
         }
		
        // 3.向下尝试
        // 如果向上委派直到根类加载器都没能加载,则不断执行此句,尝试子类加载器加载
        if (c == null) {
            // 自定义类加载器需要重写此方法,当父类加载器无法完成加载时,需自定义加载逻辑去自己加载
            c = findClass(name);
        }
    }
    return c;
}

3.5.4 双亲委派机制的作用

1)安全性

防止用户自定义的类(可能含有恶意代码)覆盖掉Java核心库中的类

2)唯一性

loadClass时,一旦发现c != null,则直接返回class对象,保证了一个类在Jvm中只会被加载一次

3)可扩展性

支持自定义类加载器,通过继承ClassLoader,重写findClass方法,实现自定义加载逻辑


3.5.5 打破双亲委派机制

场景一:SPI

1、缺陷:无法直接加载第三方类

2、缺陷描述

  • JDBC规范中的Driver接口,是在java.sql下的即rt.jar中的sql包下的,故通过启动类加载器去加载
  • 而JDBC驱动的实现类如MySQL-com.mysql.jdbc包下的具体实现Driver类
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.6</version>
</dependency>
public class Driver  implements java.sql.Driver{

}

鉴于类型安全机制,正常情况下实现类应该和接口一样由启动类加载器去加载,但是实现类在类路径下,无法被根类加载器直接加载

  • 导致了在JDBC 4.0之前,需要通过Class.forName("com.mysql.jdbc.Driver")方法显式的使用系统类加载器加载驱动

3、解决-SPI机制

JDBC 4.0后,支持SPI方式来注册数据库驱动,无需再通过Class.forName("com.mysql.jdbc.Driver")方法显式加载驱动

1)代码实现

String url = "jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false";  

//一、获取mysql的数据库连接对象
Connection conn = DriverManager.getConnection(url,"root", "941123");
        
//二、获取SQL语句执行对象
Statement statement = conn.createStatement();
        
//三、执行SQL语句
ResultSet set = statement.executeQuery("select  * from tb_user");

2)SPI实现机制原理

SPI

3)打破双亲委派机制

  • 在加载Driver驱动时,使用的是SPI服务发现机制
private static void loadInitialDrivers() {
        // 一、load
		ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        // 二、hasNext
         while(driversIterator.hasNext()) {
             // 三、next
             driversIterator.next();
         }
}
  • load:使用上下文类加载器去加载驱动Driver类
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
  • 因为上下文类加载器没有显示指定,所以默认为系统类加载器,所以本质就是使用系统类加载器去加载的类路径下的Driver实现类
Class<?> c = Class.forName("com.mysql.driver.Driver", false, loader);

场景二:Tomcat

1、背景

  • 在容器化环境,如Docker中,每个容器运行自己的Web服务和Jvm实例

  • 传统的应用服务器,如Tomcat中,多个Web服务,是共享一个Jvm实例的

    • 则两个不同Web服务(如web1和web2)中分别调用 ClassLoader.getSystemClassLoader() 时,返回的是相同的系统类加载器实例
    • 因为系统类加载器是JVM级别的,而非应用程序级别的

2、Tomcat中可能存在的缺陷

如图8,Web1和Web2共享一个Jvm实例时
在这里插入图片描述

  • web1服务使用系统类加载器去加载Spring4中的某个类A,并获取aClass对象
  • 当web2服务使用到类A时,发现全限定名称 和 类加载器(系统类加载器)都相同,则不会再去加载Spring5中的类A,而是直接返回aClass对象

3、 解决-自定义类加载器

Tomcat为每个Web应用程序创建了一个独立的WebAppClassLoader类加载器 ,并打破双亲委派机制

如图9所示
在这里插入图片描述

1)打破双亲委派机制

  • 当要加载一个类时,tomcat不会按照双亲委派机制,优先让父类加载器去加载,而是优先使用自定义WebAppClassLoader类加载器去优先加载类路径下的全限定类名的类
  • 当自定义加载器无法加载到时,才会走双亲委派

具体代码为:WebappClassLoader --继承> WebappClassLoaderBase --继承> URLClassLoader#findClass

2)自定义类加载器加载过程

  • web1服务使用自定义类加载器WebApp1加载器去加载Spring4中的某个类A,并获取aClass对象
  • 当web2服务使用到类A时,使用自定义类加载器WebApp2加载器去加载Spring5中的类A

3.6 Launcher

1、内容

  • static class ExtClassLoader:扩展类加载器
  • static class AppClassLoader:系统类加载器

2、构造器

public Launcher() {
	// 1.创建扩展类加载器
    ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader();
	// 2.创建系统类加载器
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
	// 3.设置上下文类加载器为:系统类加载器
    Thread.currentThread().setContextClassLoader(this.loader); 
}

四、运行时数据区-栈

4.1 PC寄存器

1、特点

一个线程对应一个栈,当然也对应一个PC寄存器。Java中唯一一块无OOM的内存区域

2、内容

下一条该执行的指令地址

3、作用

当CPU在多个线程之间切换时,PC寄存器能够确保每个线程都能从上次中断的位置继续执行

4.2 本地方法栈

1、作用

管理Java中native方法的调用

  • JNI:是Java调用本地方法的一种机制
  • 通过JNI,Java程序可以加载并执行用其他语言(如C或C++)编写的库
  • 本地方法栈保存JNI调用过程中的状态信息,确保JNI调用的正确性和稳定性

2、特点

  • 可能OOM
  • 也可以StackOverFlow

4.3 Java虚拟机栈

4.3.1 Java栈

1、定义

  • Java栈是线程私有的,它的生命周期与线程相同,在创建线程时,会创建一个Java栈用于存放该线程执行方法的所有栈帧。

2、特点

  • 无GC、但是可能有OOM和StackOverFlow

4.3.2 栈帧

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。这个引用用于支持当前方法的代码能够实现动态链接

1、定义

  • 是一种数据结构,是Java栈的基本单位

2、作用

  • 一个线程对应一个Java栈,一个线程可能有多个执行方法,每个方法对应一个栈帧

  • 栈帧中存储了方法的

    • 局部变量表
    • 操作数栈
    • 动态链接
    • 方法返回地址

    等信息

  • 栈帧中存储的方法信息,用于支持Jvm对方法的调用和执行

3、局部变量表

1)作用

  • 存储方法的参数
  • 存储方法内部定义的局部变量

2)存储基本单元:slot- 变量槽

  • 存储内容
    • 8中基本类型的值(long、double占用2个slot来存储值)
    • 引用类型的堆内存地址值

3)局部变量名

  • 局部变量名主要是在.java源代码中,用于帮助开发者理解和编写代码的,是编译时概念
  • 在.class文件中,局部变量名会被转换成JVM内部使用的索引index
// 其中d = 1.0、s = "m" 
public void testMethod(double d, String s) {  
    int i = 5;  
    User user = new User();
    // 其他代码  
}  

slot和局部变量的关系如图10所示
在这里插入图片描述

其中index即.java源码中的局部变量名

  • index = 11表示d变量名称
  • slot1和slot2两个槽用于存储double类型的值,即存储的d变量的值
  • double d = 10,即上述的index = 11指向槽,这样就可以通过index访问槽中的值了

一般this的index = 0,即当前对象的堆地址值,在index = 0 指向的slot中

4)局部变量是否线程安全

  • 线程安全
    public String func1() {
        StringBuilder sb = new StringBuilder();
        sb.append("a");
        String s = sb.toString();
        return s;
    }

sb在方法结束后,栈帧随着方法的结束,生命周期也结束了

  • 线程不安全-逃逸
    public StringBuilder func2() {
        StringBuilder sb = new StringBuilder();
        sb.append("a");
        return sb;
    }

sb是堆中的对象,非线程安全


4、操作数栈

1)作用

保存计算过程中临时变量

2)特点

FIFO

3)实战

public int add() {
    int a = 8;
    int b = 15;
    int c = a + b;
    return c;
}

.class文件中的方法-code字节码信息如图11所示
在这里插入图片描述

0- bipush:

  • PC寄存器,去指令地址0,获取字节码指令: bipush
  • 执行字节码指令bipush 8 : 将8压至操作数栈顶

2- istore_1:

  • PC寄存器,去指令地址2,获取字节码指令: istore_1
  • 执行字节码指令istore_1 : 将操作数栈顶的int类型值(8)存储到局部变量表中索引index = 1处
    • 弹出操作数栈栈顶int值8
    • 将int值8,存入局部变量表中的slot槽中(值为8),指向此槽的index = 1(即局部变量名a)
  • 此时局部变量表中内容为,如图12所示
    在这里插入图片描述

3-bipush:同上0,操作数栈,操作值15

5- istore_2:同上2,局部变量表,slot存储值15,index = 2指向此slot

6、7:iload_1和iload_2

  • 获取index = 1对应的slot中的值:8,存入操作数栈
  • 获取index = 2对应的slot中的值:15,存入操作数栈

8-iadd:iadd 由于JIT即时编译器负责执行,从操作数栈中弹出两个int类型的值进行相加,并将结果压回操作数栈

  • 从操作数栈中拿出元素:8
  • 从操作数栈中拿出元素:15
  • 8 + 15 = 23
  • 23压入操作数栈中

9-istore_3:同上2,局部变量表,slot存储值23,index = 3指向此slot

10-iload_3:获取index = 3对应的slot中的值:23,存入操作数栈

11-ireturn:从操作数栈中拿出元素:23

  • 将返回值23压入调用者func的操作数栈
public void func() {
    // pc = n
	add();
	// other,pc n + 1
}

public int add() {
    int a = 8;
    int b = 15;
    int c = a + b;
    return c;
}
  • 并将方法返回地址(PC = n + 1)加载到调用者func方法的PC寄存器中
  • 然后退出方法

5、动态链接

5.1 定义

在运行时解析方法的引用

5.2 作用

多态:运行时确定调用哪个类的方法

5.3 实现过程

将符号引用 -> 直接引用

public void func() {
    Map<String, Integer> map = new HashMap<>();
    map.put("mjp",18);
}
 0 new #4 <java/util/HashMap>
 3 dup
 4 invokespecial #5 <java/util/HashMap.<init> : ()V>
 7 astore_1
 8 aload_1
 9 ldc #2 <mjp>
11 bipush 18
13 invokestatic #6 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
16 invokeinterface #7 <java/util/Map.put : (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;> count 3
21 pop
22 return

1)0 - new #4 <java/util/HashMap>

  • 动态链接:符号引用new #4,转为直接引用时,可以获取到HashMap类地址
  • 对象类型检查:在执行invokeinterface 之前HashMap的实例已被推送到了操作数栈上

2)16- invokeinterface #7 <java/util/Map.put

  • 背景
    • invokeinterface:在编译时无法确定具体调用哪个实现类的方法,所以此处只能先用符号引用#7表示
    • #7 : .class文件常量池中的InterfaceMethodRef_info符号引用
<java/util/Map>
<put : (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;>
  • 执行过程

    HashMap在运行时常量池中,如图7所示:class对象地址为0x03,put方法的引用地址为0x13

在这里插入图片描述

  • 当执行invokeinterface时,JVM会检查这个对象引用的实际类型 ,即HashMap:0x03
  • 查找方法实现:
    • Jvm会查找HashMap类的vtable虚拟方法表(该表包含了HashMap中所有实例方法的直接地址 )
  • 去匹配和Map.put方法签名相匹配的方法(方法名、方法参数个数和类型、返回值类型)地址0x13

6、方法返回地址
    public int add() {
        // pc = 20
        int a = 1;
        // pc = 21
        int b = func();
        // pc = 22
        return a + b;
    }

1)定义

func方法栈帧的方法返回值地址,存储的是调用者add的PC寄存器的值

2)内容

值 = 调用者add方法,调用func方法后的下一条指令的地址 22

五、堆

5.1 参数

模块参数大小
初始化堆内存-Xms物理内存大小 * 1/64
堆最大内存-Xmx内存大小 * 1/4
新生代内存-Xmn-XX:newRatio = 2,新生代1/3,老年代2/3

1、ms和mx

建议ms = mx,防止频繁GC以及从ms <–> mx扩容缩容(但是过大也可能会造成STW变长)

2、totalMemory 和 maxMemory

Runtime runtime = Runtime.getRuntime();
long l = runtime.maxMemory();
long l2 = runtime.totalMemory();
  • max:Jvm可用的最大堆内存
  • total,Jvm从启动到运行到此刻,总共分配的内存大小(可能未完全使用)。随着程序不断运行,total会增加

3、新生代

在这里插入图片描述

-XX:SurvivorRatio=n :设置两个Survivor区(From和To)与Eden区的比例(默认8:1:1)

5.2 创建对象方式

1)new

2)反射

  • newInstance
  • 构造器

3)clone克隆

4)反序列化


5.3 对象存放位置

堆中

5.3.1 新生代

几乎所有的对象都在新生代Eden区创建的


5.3.2 老年代

  • 场景1:MinorGC(YoungGC),GC了15次,仍存活的对象

  • 场景2:动态判断

一次Minor GC后,Survivor区中一批对象的总大小 > 50%Survivor区,则这批对象中年龄最大的被直接晋升到老年代


5.3.3 TLAB

正常来说,堆中数据,是线程共享的

但是堆中有一块很小的区域TLAB,本地线程分配缓存

1)定义:线程私有的堆空间

2)作用:当分配的对象很小时,则直接放入TLAB

  • 私有,则无竞争,空间换时间

TLAB放不下,才放入新生代的eden区

5.4 对象结构

结构大小(字节)
对象头markword-8、KclassPoint-4
实例数据4
对齐填充数据8的倍数,是对象大小最终为8的整数倍

5.4.1 对象头

5.4.1.1 markword

如图15所示
在这里插入图片描述

  • 分代年龄4bit,max = 1111即15,表示minorGC15次仍存活,则年->老年代
  • Epoch:此pid获取到偏向锁的次数
  • +XX:BiasedLockingStartupDelay=n 表示系统启动 n 毫秒之后,启用偏向锁
  • +XX:PreInflateSpin=n 表示自旋 n 次之后,轻量锁升级为重量级锁

5.4.1.2 klassPointer指针
class对象和klassPointer指针
class对象klassPointer指针
定义类加载完成后返回的方法区的访问入口。反射获取的class对象指针
作用程序员可以通过反射可以获取运行时类的信息此指针指向对象类型信息-InstanceKlass对象
存储位置堆内对象头

InstanceKlass对象

1、定义

  • 是JVM中用于表示Java类的一种数据结构
  • 类被加载到内存中时,都会创建一个对应的InstanceKlass对象来表示这个类

2、作用

  • 主要存储类的元数据( 类的名称、父类、接口、字段、方法等 )信息
  • 也包含了类的静态字段和方法代码等详细信息
  • 支持类的实例化、方法调用等运行时操作

Class对象,是InstanceKlass对象的“镜像”或“代理”(Class对象对InstanceKlass对象进行了封装和抽象,隐藏了JVM内部的实现细节,为Java代码提供了一个更加高级和易于使用的接口)

  • 程序员通过Class对象可以获取类的运行时数据
  • Jvm通过InstanceKlass对象管理类的元数据

对象访问定位

如图16所示,动态链接的完整版
在这里插入图片描述


5.4.2 实例数据

类的实例变量(即非static的字段)的值 或 地址引用

  • 字段的元数据(修饰符、类型、名称)是存在于方法区的
  • 字段的值,是同对象一起,在对象结构-实例数据中的(具体值或地址引用)
<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   version>0.17</version>
</dependency>
public class Demo{
    private int a1;
    private int a2 = 10;

    private String s = "mjp";

    private Object obj;
    private double d1;
    private Double d2;

    public static void main(String[] args) {
        Demo obj = new Demo();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable()
        );
    }
}
  • 基本类型,除了double和long外,其余都占4字节,存的是值

  • 引用类型,占4个字节,存的是地址引用

    正常情况下,64位Jvm中对象的引用大小是8B,但是因为 -XX:+UseCompressedOops 压缩成4B

    压缩参数,如图41所示
    在这里插入图片描述

    • s:指向堆中字符串常量池对象
    • obj
    • d2

5.4.3 对齐填充数据

  • 对象的大小必须为8的整数倍(有助于提升内存访问的效率)

5.4.4 Object类对象占用16字节

1、pom

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   version>0.17</version>
</dependency>

2、code

Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable()

3、结果

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

4、解析对象内部结构object internals

1)对象头(Object Header)

  • Mark Word(标记字)

    • 从偏移量0开始,占用8个字节
    • 0x0000000000000001对象地址
    • non-biasable是一个非偏向锁的对象
    • age 0 :GC年龄为0
  • Class Pointer(类指针)

    • 从偏移量8开始,占用4个字节,但在64位JVM上

    • 0xf80001e5是这个类指针的值,它指向 Object 类的元数据instancekclass

2)对齐间隙(Alignment Gap)

  • 偏移量12开始占4个字节(确保对象的大小是8的倍数, 12 + 4 = 16 % 8 == 0)

3)实例变量(这里Object类无成员变量)

实例大小(Instance Size):16


5.5 new过程字节码指令分析

  • 方法调用
public User func() {
    User u = new User("wxx");
    return u;
}
  • 类定义
public class User{
	private String name;
	private User(String name) {
		this.name= name;
	}
}
  • .class文件-方法code
 0 new #2 <com/mjp/lean/User>
 3 dup
 4 ldc #6 <wxx>
 6 invokespecial #4 <com/mjp/lean/User.<init> : (Ljava/lang/String;)V>
 9 astore_1
10 areturn

1、类加载过程

new触发类的首次初始化,完成User类的加载、链接、初始化。然后使用new

1)方法区内容

如上图16所示,此时方法区已经有了表示类的instanceKClass对象。即已经有了类的元数据、方法详情Vtab等信息

2)实例初始化

执行构造函数之前,先执行实例初始化(如果有的话)

public class User{
	private Integer age;
	{
        age = 10;
        System.out.println("实例初始化");
    }
}

2、执行 new字节码指令

0 - new

  • 为对象分配内存

内存分配的方式主要有两种:指针碰撞(Pointer Bumping)和空闲列表,期间还可能设置Jvm的CAS和TLAB本地线程分配缓存

  • 设置user对象的内存地址为0x01
  • 设置对象结构(此时堆中已有对象结构)
    • 对象头
    • 实例数据:将User类的普通(非静态)成员变量String name跟随类进入0x01地址处,属性默认值为null
    • 对齐填充数据
  • 将user对象内存地址引用0x01,存入操作数栈顶

3 - dup

  • 复制操作数栈顶的引用 0x01

  • 将复制的0x01也一同压入操作数栈

4- ldc #6 <wxx>

从运行时常量池中加载常量值"wxx"的引用地址,压入操作数栈顶 。如图14所示
在这里插入图片描述

6- invokespecial #4 <com/mjp/lean/User.<init> : (Ljava/lang/String;)V>

  • 从操作数栈中,弹出

    • wxx常量值的引用地址,指向的值为字符串wxx
    • user对象引用地址0x01
  • 根据0x01,匹配User类的有参构造方法-User.<init> : (Ljava/lang/String;)V>

    • 找到堆中user对象的地址
    • 再根据对象头中的kclasspoint指针找到方法区表示User类的instancekclass对象
    • 根据instancekclass对象中的元数据信息(类名User) 以及 方法详情信息(Vtab虚拟方法表中的有参构造),匹配到User#init地址
  • 执行初始化

    完成普通成员变量name的初始化

9- astore_1

  • 将操作数栈顶剩余的0x01地址引用,弹出,存入局部变量表中
    • 用一个slot槽存储0x01
    • index = 1,指向引用地址0x01

10 - areturn

  • 从局部变量表中获取局部变量user引用index = 1指向的引用地址0x01返回
  • 并将方法返回地址值(调用者下一步应该执行的PC寄存器值)一同返回

5.6 字符串常量池

参考:十五、字符串

1、背景

jdk7之后,字符串常量池从方法区移到堆中了

2、原因

方法区内存管理

  • 字符串大部分朝生夕死,容易称为垃圾,容易触发方法区的GC(法区GC困难)

3、打印字符串常量池信息

-XX:+PrintStringTableStatistics

优化思路:避免创建相同的字符串

  • 使用StringBuilder
  • 使用intern字符串常量池

六、方法区

6.1 方法区、永久代、元空间

6.1.1 方法区

1)定义

用于存储每个类的

  • 元数据

    • 类元数据
    • 方法元数据
    • 字段元数据(修饰符、类型、名称)
  • 构造函数和普通方法详情

  • 运行时常量池

    • 存储了与类运行时相关的常量信息
    • 便于高效的访问它们
  • 异常表

  • JIT即时编译器执行的缓存热点代码

  • 加载此类的类加载的对象引用

等数据的内存区域

2)特点

类似接口,是Jvm规范,不同jdk版本有不同的实现


6.1.2 永久代

1)定义

jdk8之前,方法区的具体实现

2)特点

  • "代"即年轻代和老年代,所以可以通过FullGC间接回收方法区垃圾

6.1.3 元空间

1)背景

  • jdk8之前Jvm虚拟机主要是HotSpot实现
  • jdk8,HotSpot = 原HotSpot + JRocket

2)定义

使用本地内存作为方法区, 不再像永久代那样使用JVM的内存 (可以有效防止OOM)

3)大小

  • 大小是动态变化的 (初始值 <-> 最大值)
大小初始大小最大大小
默认值接近21M本地内存大小
参数设置-XX:MetaspaceSize=N-XX:MaxMetaspaceSize=N
作用GC阈值OOM阈值
  • 查看进程元空间大小
jps 获取进程id
jinfo -flag MetaspaceSize <PID> 

4)使用元空间原因

  • 永久代,容易频繁GC。元空间大小动态变化,不易频繁GC
  • GC相对复杂
    • 方法区一般存储类信息
    • 堆中只要GCRoot不可达即为垃圾
    • 方法区中类称为垃圾,需满足
      • 自身类型的所有实例均被GC
      • 加载自身的类加载器被GC(一般很难)
      • class对象无引用

既然永久代容易GC而且GC过程又复杂,所以换成元空间

6.2 变量和常量

1、局部变量|常量

2、实例变量|常量

局部变量|常量实例变量|常量
名存储位置栈帧局部变量表中-index索引即等效名称方法区字段元数据
值存储位置栈帧局部变量表slot中堆中对象结构实例数据中
值内容具体值、堆中对象引用地址具体值、堆中对象引用地址

3、类变量|常量

public class Demo{
    private static final int a1 = 1;
    private static int a2 = 2;
    private static final ZoneId a3 = ZoneId.systemDefault();
    private static ZoneId a4 = ZoneId.systemDefault();
}

1)名:都是存储在方法区中,字段元数据(修饰符、类型、名)

2)值

  • a1的值1
    • 编译时就已经确定,无需加载到运行时方法区,故不在栈、堆、方法区
    • 直接在编译时内嵌入到类的二进制中
  • a2-a4:都存在元空间的静态字段区域中 ,引用的对象存在于堆中

七、GC

7.1 如何定位垃圾

7.1.1 垃圾定义

没有任何引用指向此对象,即对象已"死亡"


7.1.2 可达性分析

1、目的

判断对象是否还存活,即判断对象是否已为垃圾

2、定义

  • GCRoots对象作为起点
  • 从这些点向下搜索
  • 当一个对象到GCRoot没有任何路径相连,则认为此对象为垃圾。即GCRoot不可达

3、GCRoot节点

1)栈

  • 栈帧

  • 本地方法栈-通过JNI引用的对象

  • RSet

2)方法区-类属性引用的对象

private static User u = new User();

u本身存在元空间的静态字段区域中 ,引用的对象存在于堆中

3)堆

  • 字符串常量池对象

  • 被同步锁(synchronized)持有的对象

  • Jvm内部-如类加载器也是GCRoot点,被它引用的对象也认为是存活对象

4、STW

GCRoot可达性分析过程中,所有用户线程都会被stop。

防止分析过程中,对象的存活状态又变化了

  • 原本GCRoot可达的,变为不可达了
  • 原本不可达的,产生新的引用,可达了

7.1.3 对象真正的死亡

当对象GCRoot不可达时,则认为此对象不再被任何引用,认为是垃圾,可以回收了。

但是在回收前,垃圾还有一次机会可以复活

1、对象复活

参考:finalize

2、守护线程-finalize

2.1、用户线程

main以及main线程汇总创建的线程

2.2、守护线程

Thread daemonThread = new Thread(() -> {  
    // 线程任务  
});  
daemonThread.setDaemon(true); // 设置为守护线程  
daemonThread.start();

1)定义:后台线程

2)作用:为用户线程提供服务

  • 垃圾收集Finilize线程
  • 性能监控线程

3)特点:

  • 当用户线程数 >= 1时,Jvm不会终止
  • Jvm无需等待所有守护线程结束,才终止,所以守护线程不能持有需要持续执行的任务,因为JVM可能随时停止它们

7.2 垃圾收集算法

7.2.1 标记清除(mark-sweep)

1、过程

1)标记

标记出所有被GCRoot可达的对象

2)清除

  • 遍历运行时内存
  • 将GCRoot不可达的对象回收,释放地址

过程如图17所示
在这里插入图片描述

2、缺点

  • 内存碎片:清除后可用空间不是连续的,需要维护一个可用空间列表记录哪些空间可用
  • 可能会stw

3、适合场景:老年代内存回收


7.2.2 复制(copying)

1、定义

将内存可用地址一分为二(From和To),每次使用其中的一块

2、过程

加入正在使用的From块,要发生GC了

1)标记

在From块中,标记出GCRoot可达对象

2)复制

  • GC时,直接将From块中这些GCRoot可达对象实体,完整的复制到To块中
  • 更新对象引用地址,使其指向To区中的新对象

3)清除

将From块中全部内容都清除

4)角色转换

现在To变成原本的From,用于给对象分配内存

如果18所示
在这里插入图片描述

3、缺点

优点是解决了标记-清楚的缺点

  • 内存的可用大小降低一半
  • 需要维护引用关系

原本对象在From块中的地址引用为0x01,当copy过程中,将对象实体copy到To块中了,在To块中对象地址为0x02,

所以对象的引用地址需要更新为0x02

4、解决缺点

不是严格按照一分为二,新生代而是按照,8:1:1的分配方式,如上图13所示
在这里插入图片描述

  • 当eden + from survior作为可用内存,存储对象。当发生GC时,将存活的对象copy到to survior区域
  • 此时,可用区域为eden全部区域 + to survior中未被使用区域
  • 保证了 > 80%的内存可用

5、使用场景

新生代区域适合此算法

  • 对象朝生夕死,快速的完成copy
  • 而如果是老年代,对象本身基本上都是"老不死"对象,会出现大量无意义的copy

7.2.3 标记-整理(mark-compact)

1、背景

优化标记-清除的碎片化问题

2、过程

1)标记

标记出GCRoot可达的对象

2)整理

  • 将所有存活的对象移动到内存的一端(保持存活对象之间的相对位置不变)
  • 移动过程中,更新指向对象的地址引用
  • 移动完成后,将未标记的垃圾对象所占用的内存空间释放回收

如图19所示
在这里插入图片描述

3、优点

  • 减少空间碎片
  • 在将存活对象移动到内存的另一端过程中,保持了对象之间的相对位置不变,减少了对象引用地址的变更

4、缺点

  • 当内存较大、存活对象较多时,整理过程比较耗时

5、适用场景

老年代


7.2.4 分代算法

1、定义

不同于上述三种垃圾回收算法,分代算法是一种思想。

即将内存分为年轻代(存储朝生夕死的对象)和老年代("老不死"的对象)。根据年轻代和老年代对象特点,选取合适的垃圾回收算法。eg:年轻代使用复制算法


7.3 垃圾回收相关概念

7.3.1 MinorGC

1.定义:YoungGC

2.触发条件

新生代中的Eden区空间不足时

3.回收对象

回收Eden区和Survivor区中的垃圾对象,并将存活的对象复制到另一个Survivor区或晋升到老年代

4.特点

  • Minor GC通常比较频繁,因为新生代中的对象生命周期较短
  • 执行速度快,因为新生代中的对象大多数很快变为垃圾

7.3.2 MajorGC

1.定义:OldGC

2.触发条件

  • 当老年代内存空间不足时

3.回收对象:主要回收老年代中的垃圾对象

4.特点

  • 标记-整理算法,执行速度相对较慢

7.3.3 MixedGC

1.定义:G1独有混合GC

2.触发条件

  • 当新生代 region不足。
  • 当老年代内存占据到一定比例(如45%)时

3.回收对象:同时回收新生代和老年代中的垃圾对象


7.3.4 FullGC

1.定义:对整个堆(新生代和老年代)、元空间进行垃圾回收

2.触发条件

  • 调用System.gc()时

  • 老年代空间不足时

  • 元空间不足时

  • 老年代内存担保机制

    1)目的

    防止老年代内存溢出,影响程序稳定

    2)过程

    在MinorGC之前,Jvm会判断

    • 如果此时新生代中所有对象的总大小 < 老年代可用空间大小,则允许MinorGC

    • 如果此时新生代中所有对象的总大小 > 老年代可用空间大小

      && 历史MinorGC晋升至老年代的总对象的平均大小 < 老年代可用空间大小 ,也允许MinorGC(但是存在风险,因为本次的MinorGC大部分对象需要晋升到老年代)

    • 如果二者都 > 老年代可用空间大小,则本次不允许本次MinorGC执行,因为可能会触发FullGC 或者直接导致老年代OOM等异常。此时直接进行FullGC,回收老年代垃圾

3.特点

  • 是最耗时的垃圾回收过程,因为它需要扫描整个堆、元空间
  • STW

7.3.5 安全点|区域

1、安全点

1)定义

即在程序运行过程中,可以全部正确的枚举出所有根的状态

  • 这里的根即GCRoot,有了GCRoot就不会漏掉存活的对象
  • 全部正确,要想实现这一点,就不能在枚举过程中改变根,所以就需要暂停所有Java程序
    • 不是随时随地就可以暂停的,在暂停线程之前,必须将属于自己的根放到 GC 能看见的位置。以免GC无法找到所有的根

2)过程

  • 在GC发生时,JVM会对所有用户线程,发出中断请求

  • 线程接收到中断请求后,如果自己在安全点,则将自己中断挂起

  • 如果自己不再安全点,则继续执行到安全点,再将自己挂起

  • 如果所有线程都已到达安全点,JVM将执行GC操作

3)作用

保证所有存活的数据不会被漏掉

4)安全点选择

  • 方法中执行时间较长的指令(如循环结束、方法调用前后、异常跳转等)

2、安全区域

1)背景

当Jvm要GC时,有些线程并未运行到安全点,且这些线程被Block住了,无法继续执行指令到安全点

2)定义

  • 如果一段代码内,对象的引用关系不会发生改变,即为安全区域

3)过程

  • 当线程进入安全区域时,它会标识自己已经进入了安全区域
  • 当JvmGC时,有些阻断的线程无法响应,但是Jvm获知他们已经达到安全区域,此时Jvm仍可以安全地进行GC
  • 当线程获取到执行时间片,准备离开安全区域时,它会检查GC是否完成
    • 若GC已经完成,线程将继续执行
    • 否则,等待可以离开安全区域继续执行的信号

7.4 并行和并发

7.4.1 并发- Concurrency

1)过程

单处理器中,同一时间段,用户线程和GC线程,交替获取cpu时间片执行

2)优缺点

  • stw很短,用户几乎无感,对用户体验和程序响应友好
  • 用户线程和GC线程交替执行,则二者需要交互(比如安全点),这样才能够安全GC

7.4.2 并行- Parallel

1)过程

  • 垃圾收集器会将GC任务分解成多个子任务,分配给不同的CPU核心上的线程并行执行
  • 同一时刻,多个GC线程, 在不同的处理器或处理器核心上,独立的执行

2)优缺点

  • 用户线程完全STW
  • 但可以减少垃圾收集总时间,吞吐量高

7.5 G1前置

7.5.1 指标

1、吞吐量
用户线程执行时间 ÷ (用户线程执行时间 + G C 线程执行时间) 用户线程执行时间 ÷ (用户线程执行时间 + GC线程执行时间) 用户线程执行时间÷(用户线程执行时间+GC线程执行时间)
吞吐量越高,用户线程执行时间越长,尽可能快的完成任务

2、单次STW时长

3、吞吐量和STW与内存大小关系

    • 提高吞吐量(够用,很少GC)
    • 暂停时间长(一旦GC,则STW时间就长)
  • 小年轻代

    • 暂停时间短

    • 牺牲吞吐量


7.5.2 垃圾收集器历史版本

并发Concurrency收集器减少暂停时间,并行Parallel收集器提高吞吐量

1、Jdk8垃圾收集器

  • -XX:+UseParallelGC

即Parallel Scavenge(新生代-复制算法) + Parallel Old(老生代-标记整理算法) ,吞吐量大,但是STW也长

  • 吞吐量是首要任务
  • 对STW无感,可以接受一秒或更长

2、G1

  • Java9的默认垃圾收集器G1
  • 一直到24年6月的jdk22,默认都是G1

3、CMS

  • jdk9标记为废弃(不建议使用,推荐G1)
  • jdk14移除,即从jdk15开始无法再使用cms

4、ZGC

  • jdk13发布
  • jdk17基本可稳定使用
  • STW < 1ms

7.5.3 G1特点

1、适用于大的堆内存 > 6G

2、目标

G1在暂停时间可控的情况下,获得最大的吞吐量

3、能力

1)期望暂停时间可控(软实时性)

这个能力即G-First,垃圾优先回收的名称由来

  • -XX:MaxGCPauseMillis= 200
  • 暂停时间只是一个目标,并不能保证总能达到

避免使用-Xmn、-XX:NewRatio 等,这些设置会将年轻一代大小限制在特定值,导致可控延时功能失效

所以,G1只能在有限时间内,选取更有价值的region进行回收

2)region内无内存碎片

在转移时,region内不会出现碎片化

  • 即将转移后的对象从内存的另外一侧开始存放

但是会出现以region为单位 (整个堆)的碎片化,和普通的复制算法相比,算是一缺点

2)可预测性

预测下次GC暂停时长的功能

根据预测结果,G1使用延迟GC、拆分GC目标等手段,来达到期望暂定时间可控的功能终极目标

4、参数调整

  • 如果想提升吞吐量,则将XX:MaxGCPauseMillis调大
  • 如果想降低STW,则将XX:MaxGCPauseMillis调小

7.5.4 region

1、背景

G1,在保留了新生代和老年代的概念下,将堆分割成等大的region区域

所以新生代和老年代在G1中是虚拟的

  • 年轻代 动态 占比-XX:G1NewSizePercent = 5- Max60%,是一个动态的(因为年轻region可能变为old region)

如图25所示
在这里插入图片描述

实际内存中,是排成一排的空间。上图二维只是便于展示

2、特点

大约 2000 个区域,大小从 1 到 32Mb 不等

  • 新生代和老年代表示的region区 并不要求连续

    此region本次放新生代对象,GC一次后可能放老年代对象

  • 默认region大小为堆总大小 / 2048

个数多-单个小

  • 需要疏散的实时对象少,但是容易出现大对象

个数少-单个大

  • 处理跨区域引用的工作量相对减少、但需要疏散的实时对象越多

3、作用

为了期望暂定时间可控

4、Humongous

1)定义

专门用于存放大小为region 50%或更大的对象。

2)本质

也是region,只不过在old region中每个Humongous都是以连续region形式分配的

5、CollectedHeap

CollectedHeap的3个主要成员变量结构如图53所示
在这里插入图片描述

_hrs:通过数组维护所有HeapRegion

_young_list:新生代HeapRegion的链表

_free_region_list:空闲HeapRegion的链表

老年代的HeapRegion没有与任何东西连接在一起

6、HeapRegion类的5个成员变量结构如图54所示
在这里插入图片描述

_bottom :region头地址

_end:region头地址和尾地址

_top:region内空闲内存空间的头地址

_next_young_region:单向链表,指向下一个新生代region

_next_in_special_set:单向链表

  • 当前region在空闲region链表_free_region_list上时,它指向下一个空闲region
  • 当前region在回收集CSet链表上时,它指向下一个回收对象所在region

7.5.5 Vm Options

多个参数,使用空格隔开

1、查看默认的垃圾收集器

-XX:+PrintCommandLineFlags -version
  • java -XX:+PrintCommandLineFlags -version在服务器终端查看

2、查看运行时方法区总大小 和 使用率

-XX:+PrintGCDetails -version

3、使用G1垃圾收集器

-XX: +UseG1GC

4、G1目标暂停时间

-XX:MaxGCPauseMillis= 200
  • ms

5、设置每个region大小

-XX:G1HeapRegionSize=24M
  • 2的幂次方
  • 默认为堆大小 / 2048

6、触发MixedGC阈值

-XX:InitiatingHeapOccupancyPercent = 45
  • 默认堆占用45%触发MixedGC
  • 调小为40%,可能会降低FullGC的可能

6、吞吐量

-XX: GCTimeRatio = 99

用户线程执行时间占99%

7、新生代和老年代占比

XX:NewRatio=2 默认为2,即新生代1/3

8、 -XX:ParallelGCThreads = n

参与并行收集垃圾的线程数(过小无法并行处理,过多则线程切换开销)

  • 如果CPU核心数n <= 8,则就为n

  • 如果CPU核心数大于8,则为8 + ((N - 8) * 5/8)

9、-XX:G1MixedGCLiveThresholdPercent=85

  • 在MixedGC时,老年代region中,只有存活对象占比 < 85%的region才会加入CSet作为收集候选region
  • 某个old region LiveThresholdPercent值越小,说明存活对象越少,越具有回收价值

10、-XX:G1MixedGCCountTarget = 8

在一次混合垃圾收集周期中,G1会尝试最多执行8次的混合回收

  • 过大:GC总耗时变长(可通过此参数,降低Mixed时长)
  • 过小:频繁GC

八、纯G1GC

8.1 并发标记-ConcurrentMark

并发标记是G1GC最主要的两个过程(并发标记和转移evacuation)之一

8.1.0 前置

1、作用

提供evacuation转移过程中所需信息

  • 在标记完成后,需计算出每个region中存活对象的个数(在evacuation中要用)

2、 目标

  • 在尽量不暂停用户线程的情况下,标记处存活对象,即并发标记

3、步骤

并发标记过程包括以下 5 个步骤:

① 初始标记阶段 ② 并发标记阶段 ③ 最终标记阶段 ④ 存活对象计数 ⑤ 收尾工作

8.1.0.1 标记位图

标记位图是标记的载体,并发标记并不是直接在对象上添加GCRoot可达的标记,而是在标记位图上添加标记

结构如图33
在这里插入图片描述

1、黑、白、×

  • 黑色表示已标记,是存活对象
  • 白色表示未标记
  • 带有叉号的是死亡对象

2、next 、 prev

每个region都有

  • next 是本次标记的标记位图
  • prev 是上次标记的标记位图,保存了上次标记的结果

3、关联关系

标记位图中每个bit都对应着关联区域内的对象的开头部分

  • 假设 单个对象的大小都是 8 个字节
  • 则每 8 个字节就会对应标记位图中的 1 个比特
  • 黑色表示比特值为 1,白色表示比特值为 0

4、bottom 、top 、nextTAMS 、prevTAMS

  • bottom 表示区域内众多对象的末尾
  • top 表示开头
  • TAMS 是“Top At Marking Start”(标记开始时的 缩写)
  • nextTAMS ,本次标记开始时的 top所在的位置
  • prevTAMS ,上次标记开始时的 top所在的位置

8.1.0.2 三色标记算法

1、定义

G1的并发标记阶段,使用三色标记算法,进行可达性分析

白色:该对象没有被标记过(垃圾)

灰色:该对象已经被标记过了,但该对象下的所有子对象没有被标记完( -> )

黑色:该对象已经被标记过了,且该对象下的所有子对象也被标记完

2、并发过程

如图32所示
在这里插入图片描述

1)起始阶段:所有节点都是白色的

2)init mark-STW

  • A,直接被GCRoot指向,所以A本身被标记过了,且A下无子对象,故为黑色
  • D,直接被GCRoot指向,所以D本身被标记过了,但子对象(E)尚未标记,故为灰色

3)并发标记

  • A,本身和所有属性都标记完成了,A变为黑色
  • D,本身和所有属性都标记完成了,D变为黑色
  • E,本身和所有属性都标记完成了,E变为黑色
  • F,变为灰色

黑色和灰色,GCRoot可达,白色表示垃圾

3、优点

并发标记

4、问题

在并发标记期间,用户线程的活动导致了可达性关系发生了变化

4.1 问题1 -浮动垃圾

1)描述:D -> E 引用断了

  • 原本E被认为是存活的,现在实际是垃圾

2)解决:本次GC可以忽略,下一次GC处理

4.2 问题2 - 漏标(新增对象)

1)描述:新增 H

  • H可能是存活对象,本次要处理

2)解决漏标(新增对象):STAB

4.3 问题3 - 漏标(变更对象引用)

1)描述:

原本D-> E,现在D -> B,所以B肯定是存活对象了

  • 原本B被认为是垃圾,本次GC就要回收了,但现在是存活对象
  • 本次就要处理

2)解决漏标(变更对象引用):Write Barrier写屏障技术


8.1.0.3 SATB

1、定义

Snapshot-At-The-Beginning,保存并发标记开始时,对象间的引用关系,的逻辑快照

2、原理

  • 在并发标记过程中产生的新对象,会被看作"已完成扫描和标记",故在标记位图中是黑色的

如图35所示
在这里插入图片描述

  • J和K的引用关系在STAB快照中不存在 ,所以能够判断出他们是在并发标记过程中产生的新对象,故标记为黑色

8.1.0.4 Write Barrier写屏障

1、定义

捕获对象引用关系的变更,并记录这些变更

2、作用

确保能够准确地识别出存活对象

  • G1在并发标记阶段,用户线程和GC线程是并发执行的,可能导致:GC线程在标记存活对象时,用户线程改变了对应的引用关系,使得原本认为的垃圾变成存活对象(这样就可能漏标了)影响标记结果。

3、过程

在对象属性引用另一个对象时,写屏障会被触发,捕获这一引用变更

4、G1的具体实现- SATB专用写屏障技术

写屏障是用于SATB 的,因此称之为 SATB 专用写屏障

5、STAB专用写屏障原理

obj0.filed = obj2;

5.1 satb_write_barrier() 函数源码

def satb_write_barrier(field, newobj):
2: if $gc_phase == GC_CONCURRENT_MARK:
3: 		oldobj = *field
4: 		if oldobj != Null:
5: 			enqueue($current_thread.stab_local_queue, oldobj)
6:
7: 		*field = newobj

参数 field 表示被写入对象的域(属性),参数 newobj 表示被写入域(属性)的值

上述代码中即,obj0对象的域-filed,field的值obj2

  • 2 :

    的GC_CONCURRENT_MARK 表示并发标记阶段的标志位 (flag)

  • 4 :

    • 检查当前是否处于并发标记阶段且field 非 Null
    • 检查通过,执行 5 :将 oldobj 添加到 $current_thread.stab_local_queue 当前线程的本地STAB队列中
    • 最终,执行 7 :执行实际的写入操作

此函数不是由用户线程去执行,而是交由 GC 线程在并发标记过程中处理,防止影响用户线程

5.2 这里将oldobj加入queue的原因

1)首先不是newobj加入queue分析如下

  • 如果newobj是在并发标记过程中新创建的对象,STAB算法会将其标记为存活对象
  • 如果newobj也有别处引用,那么也会标记为存活对象
  • 如果newobj也有别处引用,但是也在并发标记过程中被别的引用替换了,satb_write_barrier函数也会将newobj(对于别人而言是oldobj)放入queue,再最终标记阶段再进行对象分析

所以,newobj一定不会被错误当做垃圾回收掉

2)将oldobj加入queue的原因

如果不将oldobj加入queue,将oldobj当做垃圾回收,会有什么影响?

  • 对于本次的filed而言,是没有影响,因为不再引用了
  • 但是对于并发标记阶段,其它filed,将oldobj作为newobj进行引用
    • 本次并发标记过程感知不到(因为oldobj不是新增对象,无法通过STAB快照标记为存活对象)
    • 这样直接将oldobj当做垃圾,就影响到其它filed了

所以,需要将oldobj加入queue,再最终标记阶段,再分析一次

5.3 数据结构:SATB 本地队列queue 和 队列集合

二者关系如图36所示
在这里插入图片描述

1)本地队列内容

存储并发标记阶段的待标记对象的引用

如图32中
在这里插入图片描述

原本A -> null ,写,A ->B

  • 因为oldobj == null
    • 不会添加到STAB本地队列
    • 但是会执行A ->B

原本D -> E,写, D->H

  • 因为oldobj是E != null
    • 添加到STAB本地队列
    • 执行D->H

2)队列集合内容

填充满了的本地队列

5.3 本地队列作用

是用户线程私有的,无线程安全问题

5.4 过程

  • 并发标记阶段,GC线程会定期检查队列集合
  • 如果发现队列集合中有本地队列,则会对本地队列中的全部对象引用,进行标记和扫描

8.1.1初始标记阶段

8.1.1.1 定义

STW,并行,标记GCRoot直接引用的对象

8.1.1.2 过程

1、为每个region创建标记位图next

nextTAMS大小 = (top-bottom)/8 字节
如图34所示
在这里插入图片描述

2、根扫描

1)定义

标记GCRoot直接引用的对象

2)特点

STW

  • 虽然,G1中采用写屏障技术可以获知对象的修改
  • 但是大多数Root不是对象,他们的修改无法被写屏障获知
  • 所以,为了防止漏标,跟扫描过程必须STW

3)过程

三色标记算法的init阶段

  • Root -> C,但是C的子对象(A和E)尚未被标记,所以C是灰色的

8.1.2 并发标记阶段

8.1.2.1 定义

并发,标记初始化标记结果对象所引用的对象

8.1.2.2 过程

1、并发标记

继续顺着GCRoot直接引用的对象(如初始化标记中被标出来的C对象)继续标记(子对象),即三色标记算法的并发标记阶段

并发标记结束后如图35所示
在这里插入图片描述

  • C:初始化标记时被标记的,当时因为子对象A和E尚未被标记,故为灰色。而经历完并发标记阶段后,子对象A和E也均被标记,故为黑色,表示存活对象
  • A:本身被标记,且无子对象,故为黑色
  • E:同理A,E对应了标记位图中多个位,但只对其起始标记位(mark bit)涂黑
  • J和K:在并发标记期间创建的对象,G1通过STAB算法也完成了二者的标记,故J和K也是黑色,也是存活对象

1)针对并发过程中,新增对象(J和K)

通过STAB算法完成标记

2)针对并发过程中,引用关系发生变化的对象

通过STAB写屏障技术,完成扫描和标记,保证了不漏标

2、优先级

前者:顺着GCRoot直接引用的对象,继续扫描和标记

后者:一旦STAB队列集合中有本地队列,也需要扫描本地队列中变更引用关系的对象

二者优先级 : STAB本地队列 > 前者

原因:

  • 虽然写屏障会不断地往 SATB 本地队列中添加对象
  • 但是对象间引用关系的变化并不会改变存活对象的触达链路(—>)的总条数

8.1.3 最终标记阶段

1、remark定义

STW,并行,扫描并发阶段没有标记的对象,此阶段完成后,堆内所有存活对象都会被标记。

此时不带标记的对象都可以判定为死亡对象

2、内容

  • 并发标记后,STACK队列集合中的本地STACK都被扫描了
  • 仍有未填满的本地SATB 队列,待扫描

3、STW的原因

因为尚未扫描的(未满)SATB 本地队列中的数据会被用户线程操作,所以必须STW


8.1.4 存活对象计数

1、定义

并发(用户线程和GC线程之间是并发交替执行的,但是GC任务本身是并行的),对每个region中被标记的对象进行计数

2、过程

如图37所示
在这里插入图片描述

  • 标记位图中本次nextTAMS 所有存活的对象:A、B、E 和 GH(STACK本地队列中的对象)
  • J、K不是,因为他们是在并发标记阶段新产生的对象(但他们是存活对象)
  • L、M不是,因为他们是在存活对象计数过程中产生的新对象
  • 故,本次标记出来的存活对象总共大小 8 + 8 + 16 + 8 + 8 + 8 = 56字节

3、特点

  • 转移处理可能在计数过程中启动,所以需要先暂停记忆集合(remembered set)线程,因为RSet线程也会使用这些待计算的对象

8.1.5 收尾工作

1、定义

STW,并行,对并发标记过程进行收尾工作,为下次并发标记做准备

2、STW的原因

收尾工作中操作的数据,有的 和 用户线程是共享的

3、过程

GC 线程会扫描每个region

1)当前 -> 上一个

如图38所示
在这里插入图片描述

  • 将标记位图 next 中的并发标记结果移动到标记位图 prev 中
  • 对并发标记中使用过的标记值进行重置,下次并发标记做好准备
    • prevTAMS 被移到了 原nextTAMS处
    • 原nextTAMS 会移动到 bottom处
    • 在下次并发标记开始时,nextTAMS 会重新移动到Top处

2)执行标记-清除中的清除动作

清除定义:释放那些没有存活对象的region(整个region全是死对象),即以region为单位进行整个region清除

一旦发现会立即进行回收

回收一些完全空闲的区域,可以避免许多不必要的垃圾回收,这样就可以不费吹灰之力释放大量空间

3)计算每个区域的转移效率

定义:region的转移效率 = 区域内死亡对象的字节数 ÷ 转移此region所需时间

并按照该效率对region进行降序排序

  • 死亡对象的字节数
  • 转移时间,即预测时间,可以根据过往的转移时间进行来计算

一般来说死亡对象越多,转移效率就越高(死的多,则分子大,活的就少,则分母小,结果值就大)

8.2 转移-evacuation

8.2.0 前置

8.2.0.1定义

转移对象 + 重置region

  • 将region中所有存活对象转移到空闲region
  • 被转移的region中就只剩下死亡对象了,就可以重置region
    故转移后的region如图39所示
    在这里插入图片描述

8.2.0.2 特点

1)STW

转移是STW

  • 期间即使有并发标记,也需要暂定并发标记,优先让转移先执行

2)依赖性

  • 并发标记和转移在处理上是相互独立的
  • 并发标记的结果信息对于转移来说并不是必须的

因此,转移处理可能发生在并发标记开始之前,也可能发生在并发标记的过程中

3)触发转移的时机

当新生代region数达到上限时,转移就会被执行

当申请内存时,发现没有足够的内存,就会尝试执行do_collection_pause回收一些内存。此时就会在安全点状态下执行转移操作。如果转移后,仍无法满足申请的内存大小,则进行FullGC或者OOM


8.2.0.3Card Table-卡表

1、数据结构

如图40所示
在这里插入图片描述

1)数据结构:

数组,元素大小为 1 B (Byte) 即 8(bit)

2)数组内容

  • 物理内容:1 和 0
    • 1:代表脏Dirty卡片,脏卡片是已经被转移专用写屏障添加到RSet日志中的卡片
    • 0:代表干净卡片
  • 逻辑内容:卡片
    • 堆中大小为512B的一段存储空间
    • 逻辑映射计算公式

堆中的对象所对应(所在的)的卡片,在卡表中的索引值,计算公式
( 对象的地址-堆的头部地址 ) / 512 (对象的地址 - 堆的头部地址)/512 (对象的地址-堆的头部地址)512

  • eg1:假如obj1在堆中的位置是0x258(十六进制),对应十进制600,根据公式(600 - 0) / 512 = 1

    所以obj1(0x258)所在的卡片,在卡表中的index值 为1

2、卡表大小

假如堆大小 = 1G,则卡片的个数 = 1G / 512B = 2^21 (即这么多连续的卡片,组成了堆)即Card Table数组的index个数,此值即Card Table的大小为2M


8.2.0.4 RSet-记忆集

remember set转移专用记忆集

1、定义

RSet是每个region都具有的,用于记录region和region之间的引用关系的一种map数据结构

  • key:region地址,引用本region的其他region的地址
  • val:数组[卡表index1,卡表index2],引用方的对象所对应的卡片索引

2、作用

空间换时间,即使不扫描堆内所有region内的对象,也可以确定待转移对象所在区域内的存活对象

3、具体实现

G1GC 是通过卡表(card table)来实现RSet的

4、原理

如图31所示
在这里插入图片描述

当遍历region2的RSet记忆集时

  • entry1,k1是region1的地址,v1是卡表index 666
  • 然后去卡表index = 666处查看,值 = 1是脏表
  • 遍历region1中,idnex为666的卡片(512B)中全部对象的引用
    • 遍历其中obj3对象时,从对象结构-实时数据中获取obj3的属性值即地址引用
    • 发现地址引用指向region2中的对象obj2
    • 则可以判断,obj2被引用,是存活对象(除非没有一个活对象引用obj2)

8.2.0.5 转移专用写屏障

1、背景

引用对象发生了变化

2、evacuation_write_barrier() 函数源码

def evacuation_write_barrier(obj, field, newobj):
2: 	check = obj ^ newobj
3: 	check = check >> LOG_OF_HEAP_REGION_SIZE
4: 	if newobj == Null:
5: 		check = 0
6: 	if check == 0:
7: 		return
8:
9: 	if not is_dirty_card(obj):
10: 	to_dirty(obj)
11: 	enqueue($current_thread.rs_log, obj)
12:
13: *field = newobj

2:

检测两个对象地址的高位部分是否相

本质是为了判断obj 和 newobj是否为同一块region

如果二者同处于同一块region,则check的值一定小于region大小

3:

当check值小于region大小,又执行check >> 右移region的大小(2^n),则3返回check的结果一定为0

6:

如果obj 和 newobj是为同一块region,则check值为0,所以满足if判断,直接return

含义就是,虽然引用发生了变化,但是变化后的引用对象,就在我本身的region内

4:

说明新的引用为null

obj0.filed = null;

9-11:

当对象 obj 的属性被修改时,写屏障就会获知,并会将对象obj 所对应的卡片索引,添加到RSet日志中

  • 9

(obj地址 - 0 ) / 512 = index,判断CartTable[index] == 1即判断obj对应的卡片是否为脏卡片:

已经被转移专用写屏障添加到RSet日志中的卡片,目的是为了防止添加重复卡片。

  • 10

如果不是脏卡片,则在第10行将卡片变成脏卡片

  • 11

加入RSet日志中

所以转移专属写屏障函数的根本作用,就是将脏卡片添加到RSet日志集中,而RSet日志集的根本作用就是维护RSet

3、使用RSet日志 维护Rset

过程如图43所示
在这里插入图片描述

  • 从日志中取出脏卡片
  • 将脏卡片置为干净卡片
  • 检查卡片中的所有对象的属性
  • 往属性中地址所指向的region的RSet中,添加卡片,即<region, [卡片index]>
    • 对于region-A而言,region-B-卡片索引2048中对象b,引用了我当中的对象
    • 对于region-C而言,region-B-卡片索引2048中对象c,引用了我当中的对象

8.2.0.6 用户线程和GC线程执行关系

如图52所示
在这里插入图片描述

  • GC线程(并发标记:初始、最终、收尾 ,整个转移)阶段,导致用户线程STW
  • 转移可能发生在并发标记中暂停处理以外的所有时刻(比如并发、计数)

8.2.1 选择回收集合CSet

g1_policy()->choose_collection_set(target_pause_time_ms);

1、定义

参考并发标记后,提供的信息(每个region的转移效率),来选择要转移的区域,即CSet(eden、survior、old region均有)

2、选择标准

1)转移效率高

2)转移的预测暂停时间在用户的容忍范围内(用户指定了期望暂定的时间)

当所有已选区域预测暂停时间总和,快要超过用户的期望暂定的时间时,不再选择


8.2.2 转移

8.2.2.1 转移对象的定义

CSet中转移对象总共二大类

1、根对象 以及其子子孙孙对象

  • 在根对象转移过程中,会尝试将其子对象加入转移队列
  • 一旦对象加入转移队列,则会被转移,在转移过程中,会尝试将其子对象加入转移队列 ----
  • 一旦 ----

2、根据RSet确定的对象以及其子子孙孙对象

  • 在转移根据RSet确定的对象时,会转移其子对象
  • 在转移其子对象的过程中,会尝试将其子对象加入转移队列
  • 一旦对象加入转移队列,则会被转移,在转移过程中,会尝试将其子对象加入转移队列 ----
  • 一旦 –

8.2.2.2 "根"对象

1、"根"对象的定义

"根"对象,包含以下二类对象:

① 由根直接引用的对象

  • 被根直接引用,却不在CSet内的对象会被直接忽略

② 并发标记处理中的对象

并发标记中

  • SATB 队列集合中的oldobj
  • SATB 本地队列的引用也会被转移

这部分可能不在①中。如果他们是存活的,也应该被转移


8.2.2.3 根据RSet确定的对象
obj1 -> obj2

1、定义

被CSet内其他region中存活的对象引用的对象(obj2)

2、分析

for遍历CSet中的所有region

  • for扫描region中的RSet(如region2)中的所有values即所有卡表索引值

    • for扫描卡表内存中的所有obj对象,即scan_card方法

      • 如果obj1引用了obj2,且obj1不是死亡对象,则转移此对象obj2

        • for遍历此对象的所有子对象

          如果子对象也在CSet中,则转移子对象

如图46所示
在这里插入图片描述

仅有region-B在CSet中

遍历region-B的RSet

  • k1-region-A,发现a虽然引用b,但是a是死亡对象,如果b仅被死亡对象引用,则不转移
  • k2-region-C,发现e引用d‘,而且e是存活对象(无需考虑region-C在不在CSet),所以d’要转移
    • 转移到目标region-D,需要更新region-D中的RSet

8.2.2.4 两大类对象的转移源码-evacuate_roots()
1: def evacuate_roots():
		// 一、"根"对象,直接调用evacuate_obj转移对象函数,完成对象转移,返回转移后的新地址
2: 		for r in $roots:
3: 			if is_into_collection_set(*r):
4: 				*r = evacuate_obj(r)
5:		// 二、根据RSet找到的对象
6: 		force_update_rs()
7: 			for region in $collection_set:
8: 				for card in region.rs_cards:
9: 					scan_card(card)
10:
11: def scan_card(card):
12: 	for obj in objects_in_card(card):
			// 忽略掉死亡对象,以及只被死亡对象引用的对象
13: 		if is_marked(obj):
14: 			for child in children(obj):
15: 				if is_into_collection_set(*child):
16: 					*child = evacuate_obj(child)

8.2.2.5 对象的转移-evacuate_obj

书接上回:evacuate_roots方法中

  • 第16行
 *child = evacuate_obj(child)
  • 第4行
*r = evacuate_obj(r)

底层都是对象的转移**evacuate_obj()**函数

1、转移对象的源码

def evacuate_obj(ref):
2: 		from = *ref
    	// 未被标记的对象,不会参与
3: 		if not is_marked(from):
4: 			return from
    
    	// 如果对象被转移过了,则直接返回对象转移后的新地址
5: 		if from.forwarded:
6: 			add_reference(ref, from.forwarded)
    		// 返回转移对象的新地址值,即to
7: 			return from.forwarding
    
8:		// 一、将对象复制到转移目标region
9: 		to = allocate($free_region, from.size)
10: 	copy_data(new, from, from.size)
    
11:		// 将对象转移后的地址存入 forwarding 指针中
12: 	from.forwarding = to
13: 	from.forwarded = True
    
14:		// 二、扫描已转移完成的对象的子对象
15: 	for child in children(to):
			// 2.1如果子对象也在CSet,则加入转移队列
16: 		if is_into_collection_set(*child):
17: 			enqueue($evacuate_queue, child)
18: 		else:
				// 2.2 如果不在CSet,则
				// 参数:子对象的引用方child 和子对象 *child
19: 			add_reference(child, *child)
20:		
		// 三、将待转移对象所对应的卡片,添加到转移目标region的RSet中(图45中⑤)
21: 	add_reference(ref, to)
    	// 七、返回对象转移后的新地址值
23: 	return to

这里为了便于理解参数含义,使用具体代码举例

d.field = a;
  • 参数 ref 是待转移对象的引用地址,即d.field的值

  • from 是待转移对象,a

  • from.forwarded:转移后的地址引用,这里简记为to,即转移对象的新地址值

  • add_reference (from,to): 获取 from 所对应的卡片,将其添加到to的region的RSet中

push(card(from), to_region.rs_cards)

如果from和to位于相同的region,就没要更新了

2、源码中,转移对象伴随的执行动作分析

其中a是转移对象

步骤一、

  • 将对象复制到转移目标region(copy_data物理转移对象)

步骤二、

2.1逻辑如图44所示

在这里插入图片描述

假如对象a为根转移对象,完成了转移到目标region后为a‘

添加子对象(a引用的对象)至转移队列:对象 a 引用的所有位于CSet内的对象(b),会被添加到转移队列中

后续动作

  • 后续会将b对象转移到目标region

    • 然后继续扫描b的子对象
  • 会将a.filed值改为新的b‘地址值

  • 更新RSet,对于b而言:

    • 谁引用了我:更新b’所在region的RSet
    • 我又引用了谁:更新原本被b引用的对象所在region的RSet

1)转移队列定义

用来保存待转移对象引用值

2)处理转移队列的源码- evacuate()

def evacuate():
2: while $evacuate_queue != Null:
3: 		ref = dequeue($evacuate_queue)
4: 		*ref = evacuate_obj(ref)
  • 从转移队列中取出对象
  • 然后执行evacuate_obj方法

2.2 逻辑,如图42所示
在这里插入图片描述

a 被转移到了 a’,所以有必要更新c-region-RSet中的key(a’所在region地址)以及val(a’的卡片在卡表中的index)

步骤三:

如图45所示
在这里插入图片描述

  • a’的RSet,需要添加d对应的卡片
  • d.field1 的地址被替换成了对象 a’

8.3 软实时性

8.3.1 GC暂停处理

1、暂停处理时刻的选择(起始和结束)

依据2个时间值

  • 用户期望GC 暂停时间上限

  • GC 单位时间

准则:

单位时间内,累计的总GC暂定处理时间,不超过用户期望暂停时间

(eg:500ms、3s:即在3s内暂停时间总和不超过500ms)

2、依赖数据

G1中有一个队列记录了之前的暂停处理,调度开始时间和调度处理结束时间。G1本次的调度(起始和结束时刻)会参考之前的

3、符合准则举例

如图48所示
在这里插入图片描述

在单位时间内(a、b、c,长度都是单位时间),总的暂定时间(a的暂停时间、b的暂定时间、c的暂定时间),都不超过期望暂停时间(ok),所以图中GC暂停处理时刻的选择,没问题,遵循

4、不符合准则的场景

  • 预测转移时间不准
  • 堆内可用空间不足
  • 并发标记阶段暂停时间就已超用户期望值

8.3.2 预测转移时间

1、定义

因为用户设置了期望暂停时间,所以再将region放入CSet时,就要预测转移CSet中的这些region中的对象,需要的时间,如果预测时间已经 > 用户期望的暂停时间了,就不要再放入CSet中了

2、CSet转移时间计算公式

如果47所示
在这里插入图片描述

基本大头2个

  • 扫描RSet中的卡片
  • 对象转移

九、分代G1GC

上述分析的都是纯G1模式,OpenJDK对外开放给用户的是分代G1模式

9.1 前置

9.1.1 和纯G1的区别

  • region分为年轻代和老年代
  • CSet的选择也是分代的

9.1.2 G1新生代GC 和 G1老年代GC

G1新生代GC:full_young_gc

  • 新生代GC:minor GC
  • G1新生代GC:完全新生代GC
    • 将所有新生代region加入CSet(符合软实时性准则的前提下)

G1老年代GC

  • 老年代GC:MajorGC
  • G1老年代GC:部分新生代GC,或MixedGC
    • 所有新生代region加入CSet(符合软实时性准则的前提下)
    • 以及部分老年代region加入CSet

9.1.3 新生代region分类

新生代区域又分为两类:创建region和存活region

1、创建region

定义:存放刚刚生成,一次都没有转移过的对象

2、存活region

非创建region

9.1.4 转移专用写屏障生|失效场景

1、生效场景

obj1 -> obj2

如果obj1是老年代region中对象,obj2是新生代中对象,则新生代region2的RSet中,记录有老年代region1的卡片信息

但:

  • 如果obj1是新生代region中对象,obj2是老年代region中对象,region2的RSet中不记录卡表

一句话概括:

只有老年代引用新生代,新生代region的RSet中才会记录卡表

如图49所示
在这里插入图片描述

2、失效原因

首先对于新生代的对象,在G1新生代GC时,所有新生代region都会加入加入CSet,然后去扫描

  • 图49中右,需要通过regionA的RSet中记录谁引用了我,然后通过这个谁,能够快速的定位出A中的存活对象,所以必要要有这个数据

  • 图49中左,即使regionB中RSet不记录任何数据,因为regionA一定被加入CSet,在遍历整个regionA的过程中,也可以发现regionA中有对象引用了b,也能够判断b是存活对象。所以没必要记录,否则就重复记录数据了

3、总结

每个region中都有RSet,但只有老引用新的时候,才会在新region的RSet记录数据


9.1.5 转移对象存放的目标region

存放位置条件
新生代-存活region对象年龄 < 阈值(15)
老年代region(对象晋升到老年代)对象年龄 >= 阈值(15)
  • 如果目标region满了,则会选择一个空闲的区域,把它修改成存活region或old region来使用
  • 如果对象本身就在老年代,那么其年龄肯定 > 15了,所以下一次转移肯定还是在老年代(这个不算晋升)

9.1.6 不同代GC时的软实时性考量

1、背景

因为G1GC,无论是G1新生代GC,还是G1老年代GC,CSet中都含有全部的年轻代region。

因为G1认为年轻代中的对象是朝生夕死的,所以回收年轻代,性价比很高。

但是,如果新生代总region很大,就有可能违背软实时性准则,即:

GC单位时间内,累计总GC暂停处理时间 < 用户期望暂停时间

2、G1新生代GC

在遵循软实时性准则前提下,尽可能多的添加新生代region(优先选择价值高的region)

3、G1老年代GC

在遵循软实时性准则前提下,尽可能少的添加新生代region,然后添加部分老年代region

在并发标记结束之后设置个数

9.2 G1GC过程

9.2.1 G1新生代GC过程

如图50所示
在这里插入图片描述

将所有年轻代region(创建region 和 存活region)中存活对象,转移到目标region

  • 如果有对象晋升成功,则存入old region
  • 其它的正常存入新生代-存活region

9.2.2 G1老年代GC过程-MixedGC

1、过程

如图51所示
在这里插入图片描述

将所有年轻代region(创建region 和 存活region)中,以及部分老年代region中存活对象,转移到目标region

  • 所有新生代(创建region、存活region)中存活对象
    • 如果有对象晋升成功,则存入old region
    • 其它的正常存入新生代-存活region
  • 部分老年代中存活对象,存入old region

2、触发时机

“-XX:InitiatingHeapOccupancyPercent”,其默认值是45%,当老年代占据堆内存的45%时,触发MixedGC

9.2.3 二者选择

1、结论

垃圾回收器通常会选择G1老年代GC即MixedGC,只有在认为使用G1新生代GC效率更高时,才会切换成此

切换的时机:也只是在并发标记结束之后。

2、评估过程

  • 参考并发标记中标记出的死亡对象个数,预测出下次部分新生代 GC 的转移效率
  • 根据过去的完全新生代 GC 的转移效率,预测出下次完全新生代 GC 的转移效率
  • 如果预测出完全新生代 GC 的转移效率更高,则切换为完全新生代 GC

9.2.4 并发标记触发时机

当转移完成并通过以下 4 项检查之后,会开始执行并发标记。

① 不在并发标记执行过程中 (避免重复并发标记)

② 并发标记的结果已被上次转移使用完

③ 已经使用了一定量的堆内存(默认是全部堆内存的 45% 以上)

④ 相比上次转移完成之后,堆内存的使用量有所增加

9.2.5 FullGC

在执行Full GC时,G1可能会退化为使用串行收集器,能会导致较长的STW时间

1、容易引发场景

1)如果分配了许多巨大的对象,就更有可能出现满 GC 的情况

Humongous ,必须为它们找到一组连续的region,因此在所有 Java 堆内存耗尽之前就可能发生 Full GC

  • 减少大对象

    • 将大对象分割成小对象,如日志打印分割
  • 增加region大小,让大对象 相对region"变成"小对象

    -XX:G1HeapRegionSize

2)并发标记无法及时完成,无法启动空间回收阶段

  • 提高并发标记线程数-XX:ConcGCThreads

3) System.gc()

2、执行过程

标记存活对象:从GC Roots开始遍历堆中的所有对象,标记出所有存活的对象

整理内存空间:将存活对象移动到堆的一端,并清理掉未使用的内存空间

十、JVM调优

jvm中与性能有关的组件,如图24所示
在这里插入图片描述

  • JIT即时编译器
  • 堆(内存泄露、内存溢出)
  • GC(日志、参数)

10.1 调优工具-GUI

10.1.1 工具

  • Arths

  • JProfiler、 MAT、VisualVM

  • GC日志分析工具:GC Easy、GCViewer

这里以JProfiler安装和使用教程


10.1.1 JProfiler

1、监控内容:CPU、内存、堆、线程、GC

2、离线观察快照

1)dump一份hprof文件

2)使用JProfiler打开、或在JProfiler的start center中选择open snapshot

3)观察Classes情况

因为内存、cpu等都是动态变化的,只有class对象快照可观察

4)class对象

如图57所示:类名、实例个数、实例总大小
在这里插入图片描述

5)对象实例的引用关系

选中对象,右键-use selected objects,结果如图61所示
在这里插入图片描述

  • 查看我被谁引用的引用链

    如图62
    在这里插入图片描述

在图中可以查看当前实例被谁引用,也可以查看当前实例的GCRoot(右键,show path to GCRoot)

3、本机Idea项目

如图64所示
在这里插入图片描述

1)整体性能指标Overview

如图55所示
在这里插入图片描述

2)内存

如图56所示
在这里插入图片描述

3)cpu

  • 整体情况如图58所示
    在这里插入图片描述

  • 线程占用cpu资源情况:record cpu data(会影响服务器性能

如图63所示
在这里插入图片描述

包|类|方法级别的线程,占用cpu时间片的时间百分比、线程的状态(运行、阻塞)

(可以先以包、再以类、再以方法,逐步缩小线程范围)

4)排查内存泄露

  • 开启:recorded objects(会影响服务器性能

如图59所示
在这里插入图片描述

  • 选中类,右击选择change liveness mode

如图60所示
在这里插入图片描述

  • 选择Live objects模式,查看top20
  • 再选择 -Garbage collected Objects模式,是否包含Live objects模式下的top20
    • 如果不包含,说明有的Live objects,始终未变成垃圾,未被收集过,则有可能为内存泄露对象

5)Threads

主要查看blocked状态表示的红色线程

5、远程服务器实时观察

不推荐,会影响线上服务器性能


10.1.2 命令

top

1、作用

查看 CPU、内存使用情况、正在运行的进程 。定位 哪些进程正在消耗最多的系统资源 (占用cpu执行片、内存)

类似于Windows的任务管理器

2、使用

1)第一行:查看cpu负载率

load average: 值1,值2,值3

  • 分别表示在前,值1 min、值2 min、值3 min的平均负载
  • 如果服务器是8核,值 > 8,则说明cpu处于高负载了

2)第三行:30%id即cpu空闲率(低于20%说明cpu高负载)


ps
  • 查看占用cpu负载最高的top5进程
ps -eo pid,user,%cpu,command --sort=-%cpu | head -n 6
或
ps -eo pid,user,%cpu,command | sort -nrk 3 | head -n 6
  • 看进程下cpu负载最高的top5 线程
ps -eLf -p <PID> --sort=-%cpu | head -n 6
或
ps -eLf -p pid | awk 'NR>1 && $2==12345 {print $0}' | sort -nrk 9 | head -n 5
假设CPU使用率位于第9列
或
top -p pid 或 top pid  : 默认按照cpu占用率降序,结果如果要按照内存降序展示:shift + m(Men即内存)

Windows环境下,建议使用 Git Bash 执行以下命令

jps

1、作用

Java进程pid、Idea中VM Options、JIT信息

9552 org.jetbrains.jps.cmdline.Launcher
9448 com.mjp.onjava.Demo 

-Xms128m 
-Xmx2024m
-XX:HeapDumpPath=C:\Users\admin\\java_error_in_idea64.hprof 
-XX:+UseG1GC
-XX:+HeapDumpOnOutOfMemoryError 

-XX:CICompilerCount=2 

2、场景

  • 列出Java进程,显示完整的包名或主类名
jps -l
  • 列出Jvm启动参数(包含-l功能)
jps -lvm

结果输出 >> “C:\Users\admin\Desktop\my.txt”


jstat

1、作用

查看内存泄漏、GC(次数、时间)内存使用Use情况、类加载等

2、使用

jstat 可选指令 pid xms(每个多少ms) ncount(打印几次)

3、可选指令

  • -class:加载的类情况
  • -gc:查看新、老region、元空间、GC次数和时间等情况
    • 单位一般是kb
    • 0.0并非0值,而是以kb表示,值很小,忽略
    • C结尾表示Capacity即容量
    • U结尾表示user使用量
    • T结尾表示Time时间s
S0C, S1C: Survivor space 0Survivor space 1 的容量(Capacity)即“From”和“ToS0U, S1U: Survivor space 0Survivor space 1 的使用量(UsageEC: Eden space的容量。
OC: Old Generation(老年代)的容量

MC: Metaspace(元空间)的容量

YGC, YGCT: 年轻代垃圾收集的次数和总时间
FGC, FGCT: 完全垃圾收集(Full GC)的次数和总时间
GCT: 垃圾收集的总时间,是YGCTFGCT之和。
  • -gccapacity
NGCMN 和 NGCMX:分别代表新生代(New Generation)的最小和最大容量
NGC:当前新生代已使用的容量
S0C 和 S1C
EC

OGCMN 和 OGCMX:分别代表老年代(Old Generation)的最小和最大容量
OGC:当前老年代已使用的容量
OC:老年代的总容量(或当前容量,取决于JVM的实现和配置)

MCMN 和 MCMX:分别代表元数据区的最小和最大容量
MC:当前元数据区已使用的容量

YGC 和 FGC:分别代表年轻代垃圾收集和Full GC的次数

jinfo

1、作用:查看Jvm启动参数

2、使用

  • jinfo -flags pid:Jvm启动参数(region、新生代、堆初始、最大等)
-XX:ConcGCThreads=1:并发垃圾收集器的线程数
-XX:G1HeapRegionSize=1048576:单个region大小为1MB
-XX:InitialHeapSize=268435456:堆内存的初始大小为256MB
-XX:MaxHeapSize=4269801472:堆内存的最大大小为4GB
-XX:MaxNewSize=2561671168:年轻代(Young Generation)的最大大小为2.4GB
-XX:MinHeapDeltaBytes=1048576:堆内存扩展时的最小增量为1MB
-XX:+UseG1GC
  • java -Xmx4088m -jar your-application.jar :修改jvm参数值

如: -XX:+HeapDumpOnOutOfMemoryError,即发生OOM时,自动将OOM时的堆内存使用情况,dump文件到-XX:HeapDumpPath = 的文件地址


jmap

1、作用:查看堆内存使用情况、类实例的创建情况、dump文件

2、使用

  • jmap -heap pid:堆内存的使用情况
Heap Configuration:
   MaxHeapSize              = 4269801472 (4072.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 2561671168 (2443.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   G1HeapRegionSize         = 1048576 (1.0MB)

Heap Usage:
G1 Heap:
   regions  = 4072
   capacity = 4269801472 (4072.0MB)
   used     = 46269088 (44.125640869140625MB)
   free     = 4223532384 (4027.8743591308594MB)
   1.0836355812657323% used
G1 Young Generation:
Eden Space:
   regions  = 1
   capacity = 12582912 (12.0MB)
   used     = 1048576 (1.0MB)
   free     = 11534336 (11.0MB)
   8.333333333333334% used
Survivor Space:
   regions  = 2
   capacity = 2097152 (2.0MB)
   used     = 2097152 (2.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 84
   capacity = 253755392 (242.0MB)
   used     = 42074784 (40.125640869140625MB)
   free     = 211680608 (201.87435913085938MB)
   16.58084333435563% used

4027 interned Strings occupying 308088 bytes.

列出Java堆中每个类的实例数量、占用的内存大小以及该类实例的总大小

num序号     #instances实例个数         #bytes实例大小  class name类  
----------------------------------------------  
  • jmap -histo <pid> | sort -nr -k2 | head -n 10 :实例个数最多的top10的类

  • jmap -histo <pid> | sort -nr -k3 | head -n 10 :实例总大小最多的top10的类

  • jmap -dump:live,format=b,file=d:\Demo.hprof PID 生成JVM堆内存的快照(命令执行时刻堆内存中使用情况 ) ,影响服务器性能

    下载后的快照,可以使用JProfiler进行分析


jstack

1、作用:定位线程出现长时间停顿的原因 (死锁、死循环、IO等待)

2、使用

1)死循环

  • 代码
public class Demo{
    private  void func() {
  		while (true) {
       		System.out.println("s");
        }
   	}
}
  • 命令行输出:关键字locked
"main" #1 prio=5 os_prio=0 tid=0x000000000204c000 nid=0x2450 runnable [0x000000000253f000]
   java.lang.Thread.State: RUNNABLE
   
	- locked <0x00000006c7e5f358> (a java.io.PrintStream)
	at com.mjp.onjava.Demo.func(Demo.java:27)
	at com.mjp.onjava.Demo.main(Demo.java:15)

main线程是在执行,状态为RUNNABLE

但是有死循环locked情况发生,在Demo类的第15行func方法,方法的第27行

System.out.println(“s”);出现了死循环

2)死锁

  • 产生死锁的Java代码

  • 命令行输出:关键字 deadlock 、BLOCKED

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001bd1c988 (object 0x000000076b57d7c0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001bd1de28 (object 0x000000076b57d7d0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at com.mjp.onjava.DeadlockDemo$2.run(DeadlockDemo.java:48)
	- waiting to lock <0x000000076b57d7c0> (a java.lang.Object)
	- locked <0x000000076b57d7d0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)
"Thread-0":
	at com.mjp.onjava.DeadlockDemo$1.run(DeadlockDemo.java:27)
	- waiting to lock <0x000000076b57d7d0> (a java.lang.Object)
	- locked <0x000000076b57d7c0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
  • 生产了1个死锁
  • 产生死锁的线程0-1
  • 产生死锁的位置DeadlockDemo.java:48、DeadlockDemo.java:27

netstat

tcp、udp查看端口和ip

1、查看mysql服务,是哪个端口在监听

sudo netstat -tulnp | grep mysql
  • -t 显示TCP端口
  • -u 显示UDP端口
  • -l 仅显示监听状态的端口
  • -n 直接显示IP地址和端口号
  • -p 显示监听端口的程序名和PID(需要root权限)

2、 查看3306端口,正在被那个进程占用

sudo netstat -tulnp | grep 3306

3、 查看本机的3306端口,正在被哪个服务器访问

sudo netstat -an | grep ':3306'
  • -a : all

结果显示

127.0.0.1:3306     192.168.163.10:14800  established

10.2 Heap

10.2.1 内存泄漏

定义

因为code不准确,导致无用的对象,仍被引用,一直处于GCRoot可达,无法被回收,占用的内层无法被释放

场景

1)逃逸

原本在方法内部的局部变量,因为逃逸可能变成了被引用的对象,也无法被回收

2)成员内部类

非静态内部类会持有外部类的信息,导致外部类无法被回收,建议使用静态内部类

3)静态集合持有对象

定义了静态集合,且集合中持有的是大量朝生夕死的对象(如字符串),这些对象也无法被正常回收

4)finally中未正确的关闭资源

建议使用try-with

5)单例

  • 单例对象的生命周期和应用程序一样长
  • 如果单例对象持有其他对象的引用,这个引用的对象将无法被回收

6)ThreadLocal使用不当


强软弱虚引用

1、强引用

强引用使得对象GCRoot可达

2、软引用 SoftReference

1)内存回收策略

  • 正常情况下不会回收软引用对象
  • 只有当内存不够了,且又申请了内存,此时会清除掉所有的软引用数据

2)场景

  • 场景1-自定义LRU
public class MyLRU<K, V> extends LinkedHashMap<K, SoftReference<V>> {
    private Integer limit;

    public MyLRU(Integer limit) {
        super(16, 0.75f, true);
        this.limit = limit;
    }

    @Override
    public boolean removeEldestEntry(Map.Entry<K, SoftReference<V>> eldest) {
        return size() > limit;
    }

    public static void main(String[] args) {
        MyLRU<String, Object> lru = new MyLRU<>(3);
        lru.put("1", new SoftReference<>("mjp"));
    }
}
  • 场景2
SoftReference<byte[]> sf = new SoftReference<>(new byte[1024 * 1024 * 1024]);

byte[] byte1 = new byte[1024 * 1024];
System.gc();//此时堆内存仍够用,所以不会回收软引用内存
System.out.println(sf.get());//[B@34c45dca


byte[] byte2 = new byte[1024 * 1024 * 1024];
// 因为byte2申请了很大一块内存,导致GC,会回收软引用sf指向的内存
System.out.println(sf.get());//null

3)实战

  • 网页图片缓存对象,使用软引用持有
    • 快速加载
    • 防止OOM

3、弱引用

1)内存回收策略

  • GC即回收

    但不一定是立即回收

2)场景

WeakReference<byte[]> sf = new WeakReference<>(new byte[1]);
System.out.println(sf.get());//[B@34c45dca
        
System.gc();//只有GC,必回收
System.out.println(sf.get());//null

3)实战

  • WeakHashMap

适用于缓存数量较多的大对象

CglibAopProxyprivate static final Map<Class<?>, Boolean> validatedClasses = new WeakHashMap<>();

4、虚引用

4.1 作用

管理直接内存

4.2 直接内存

1)作用-零拷贝

  • 直接内存分配方式绕过了Java堆,直接在操作系统的内存中分配空间

    对于网络IO场景, 减少数据在Java堆和操作系统之间的复制次数 ,即零拷贝

  • 但直接内存需要程序员自己去回收,这里是通过ByteBuffer类中Clean虚引用机制完成直接内存的回收

2)实战

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);

底层源码

// 分配直接内存
long base = unsafe.allocateMemory(size);
Unsafe unsafe.setMemory(base, size, (byte) 0);

// ByteBuffer成员变量address
long address = base + ps - (base & (ps - 1));
// 虚引用类Cleaner(extends PhantomReference<Object>)
Cleaner cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

成员变量address通过虚引用Cleaner,指向直接引用地址,如图20所示
在这里插入图片描述

4.3 内存回收策略

当ByteBuffer类的byteBuffer对象不再被引用时,我们也应该将os分配的直接内存释放掉。流程如下

  • 当GC回收byteBuffer对象时,Clean虚引用会向Queue队列中发送信息,byteBuffer被回收了
  • GC线程会对Queue一直监视,一旦发现queue队列中有byteBuffer元素
    • 获取byteBuffer对象的address成员变量信息,得知还有一个虚引用指向直接内存地址
    • 此时Jvm会调用线程,回收这块区域

ThreadLocal

一、原理

1、定义

本质是线程级别的全局变量

2、作用

在线程的生命周期中,在不同的模块内,传递值。避免了复杂的参数传递逻辑

3、概念

1)ThreadLocal类结构如图21所示

ThreadLocal类
	静态内部类ThreadLocalMap
		静态内部类Entry extends WeakReference<ThreadLocal<?>>
    		Key: thisThreadLocal-tl
            val: 即tl.set()的值

在这里插入图片描述

2)ThreadLocal 和 Thread关系如图22所示

每个Thread线程,都会通过createMap方法,创建一个ThreadLocalMap

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

并将其作为成员变量

ThreadLocal.ThreadLocalMap threadLocals = null;

在这里插入图片描述

4、set

ThreadLocal<Object> tl = new ThreadLocal<>();
tl.set(1);//等价map.put(tl,1);

源码

    public void set(T value) {
        // 一、获取当前线程
        Thread t = Thread.currentThread();
        // 二、获取当前线程的ThreadLocalMap属性
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 三.有的话,则map设置k-v
            map.set(this, value);
        else
            // 如果没有则new一个
            createMap(t, value);
    }
  • 步骤三、 map.set设置key-val
// 2.1 计算本次的key(this对象-ThreadLocal对象)在长度为16的Entry数组中的下标index索引
Entry[] tab = table;
int len = tab.length;
// 底层是通过AtomicInteger计算的
int i = key.threadLocalHashCode & (len-1);

// ---
// 省略了判断i下是否已有Entry元素,元素值覆盖等情况
// 假设i下无元素,则直接存储
// 2.2 设置key-val
tab[i] = new Entry(key, value);
  • 2.2 new Entry构造函数
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                // 2.2.1 key则继续向上-> WeakReference -> Reference作为构造方法的参数
                super(k);
                // 2.2.2 val为Entry的成员变量
                value = v;
            }
        }
  • 2.2.1弱引用
    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        // 这里的referent即ThreadLocal对象
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

这里的tl是weakreferent,指向堆中分配的TreadLocal内存

  • 2.2.2 Entry对象持有k-v(v是自己的属性、k是其爷爷的属性值)

5、get

Object o = tl.get();//等价map.get(tl);

源码

    public T get() {
        // 一、获取当前线程
        Thread t = Thread.currentThread();
        // 二、获取当前线程的属性map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 三、根据tl对象,获取map中的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                // 四、返回entry的属性值val
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  • 步骤三、Entry e = map.getEntry(this)
private Entry getEntry(ThreadLocal<?> key) {
    // 3.1通过key即tl计算在Entry数组中的下标index
    int i = key.threadLocalHashCode & (table.length - 1);
    // 3.2 获取元素
    Entry e = table[i];
	return e;
}

二、Entry必须为弱引用

1、目的

方式内存泄露

2、过程-假如Entry不是弱引用,而是正常的强引用,分析如图23所示
在这里插入图片描述

1)Demo类成员变量tl,不再引用0x01,即①断掉

正常情况下,此时地址0x01对应的tl对象,GCRoot不可达,应该被回收,但是

2)由于线程池ref仍然引用Thread对象

  • Thread中持有ThreadLocalMap
  • ThreadLocalMap持有Entry
  • 如果Entry是强引用,则由于③这条线的存在,导致0x01地址内存无法被回收

3、Entry弱引用

  • 由于Entry是弱引用,则只要GC,③这条线就会断掉,即③指向的内存地址会被回收

三、 注意事项

1、内存泄露

如上图23所示,即使Entry是弱引用,因为④引用的存在,仍可能导致内存泄露问题

1)分析

  • ①断了
  • ②存在
  • GC时,③断了,0x01被回收了,此时Entry中的key即tl = null
  • 正常情况下tl.get()可以获取key(tl)对应的Entry中的val,但是key为null了,无法再获取val了
  • 导致val引用④无法被回收,即byte[]数据占用10M的空间无法被回收,导致内存泄露

2)解决

方式一:当确定不再使用tl时,手动将val删除

        ThreadLocal<Integer> tl = new ThreadLocal<>();
        tl.set(1);
        
        Integer integer = tl.get();
        System.out.println(integer);//1
        
        tl.remove();
        System.out.println(tl.get());//null
  • remove:会将val对应的内存回收
  • 弱引用: 会将key对应的内存回收

不会存在内存泄露问题

方式二:将val也变为弱引用

ThreadLocal<WeakReference<byte[]>> tl = new ThreadLocal<>();

2、并发时,多个多个请求公用一个线程

背景

100个请求,并发使用5个线程,其中req1使用的t1处理的,后续req21也是使用的t1处理的。此时t1中的tl可能被req1中逻辑改变了

3、父线程不会传递tl副本到子线程

    public static void main(String[] args) throws InterruptedException {
        final ThreadLocal<String> threadLocal=new ThreadLocal<>();
        threadLocal.set("Java");
        System.out.println("父线程的值:"+threadLocal.get());
        
        new Thread(() -> 
                System.out.println("子线程的值:"+threadLocal.get())//null
        ).start();
    }
  • new的子线程,不会获取到父线程(main)的tl副本

解决: 使用TransmittableThreadLocal


四、实战

1、Spring实战

在 Spring 的事务管理中,使用ThreadLocal 存储当前线程的事务信息(主要是Connection连接), 当方法调用链中需要获取当前事务con时,可直接从tl中获取,避免复杂的参数传递

  • 源码位置DataSourceTransactionManager#doBegin:开启事务
// 创建完成事务后,会将当前的连接Connection,绑定到当前的线程上(ThreadLocal)
TransactionSynchronizationManager.bindResource(
    obtainDataSource(), txObject.getConnectionHolder()
);
// resources即ThreadLocal<Map<Object, Object>>
Map<Object, Object> map = resources.get();//即tl.get => val(map)
// 为val(map)绑定内容(k:datasource、val:con)
Object oldValue = map.put(actualKey, value);

2、Mybatis实战

1)背景

  • Mybatis中使用相同的SqlSession创建Mapper,不同的Mapper都使用相同的SqlSession,存在线程安全问题

  • Spring整合Mybatis,使用ThredLcoal解决上述问题(每个线程有单独的DefaultSqlSession)

2)源码

SqlSessionUtils#getSqlSession下registerSessionHolder

TransactionSynchronizationManager.bindResource(sessionFactory, holder);

底层和Spring一样

Map<Object, Object> map = resources.get();
Object oldValue = map.put(actualKey, value);

排查泄露

方式一:使用上述JProfiler

方式二:使用jps + jmap(实例情况)

10.2.2 OOM

1、原因

申请不到足够的内存

2、堆触发OOM过程

1)创建对象

新创建的对象,正常情况下存于新生代的eden区

2)MinorGC

  • 当新生代的eden区满了,触发YGC,将存活对象分放入to survior
  • 继续向eden区 和 to survior区存对象
  • 当eden满了,触发YGC,将eden和to survior存活对象,再放入from surivor区
  • 直到内存担保机制,老年代可用空间 < (新生代对象大小总和,新生代历次晋升对象大小平均值),触发FullGC

3)FullGC

  • FullGC触发完成后,继续执行上述1)和2)动作,直到老年代也彻底满了,整个堆无法再分配足够的空间

3、堆避免OOM

1)内存加载的数据过大

  • 场景1:mysql取数据,不使用分页条件,大量的数据放入运行时内存,直接OOM
  • 场景2:excel的导出

2)while死循环

  • 场景1:while循环结束条件 不是 >=而是==,在高并发的时候,可能死循环

3)http-head设置过大

默认是4096即4k,设置过大,高并发大量请求也会占用大量内存

4)不正确的使用lambda表达式创建大量的对象

  • 循环中、高并发场景下、使用lambda表达式操作大集合、表达式中持有外部对象的引用

5)内存泄露

导致大量空间被占用,无法示释放


4、方法区OOM过程

1)触发过程

本地内存大小已经超过了 -XX:MaxMetaspaceSize=N 值

  • 当加载的类过多或者动态生成的类过多时

5、方法区避免OOM

1)瞬时加载大量类

  • 使用了大量的动态代理

  • 第三方库

    导致瞬时向运行时元空间中加载了大量的类信息(注意:避免引入不必要的依赖和类)

2)内存泄露

自定义的类加载器没有正确实现资源释放,导致已加载的类无法被卸载

6、排查OOM

10.3 GC

10.3.1 GC日志

10.3.1.1 打印日志参数

**-XX:+PrintGCDetails**将日系详细程度设置为更精细

  • 显示每个阶段的平均时间、最短时间和最长时间
  • 根扫描、RSet 更新、RSet 扫描、对象复制
  • 释放 CSet 的时间
  • 显示 eden、survior和 堆总占用率

输出样本

[Ext Root Scanning (ms): Avg: 1.7 Min: 0.0 Max: 3.7 Diff: 3.7] 
[Eden: 818M(818M)->0B(714M) Survivors: 0B->104M Heap: 836M(4096M)->409M(4096M)] 

-XX:+PrintGCDateStamps- 为每条日志添加发生时间戳 yyyy-MM-dd HH:mm:ss.SSS

2024-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]

-XX:+PrintGCDateStamps- GC事件自JVM启动以来的时间s


10.3.1.2 获取GC日志

1、获取本地Idea中运行项目

配置如图65和如图66所示

在这里插入图片描述
在这里插入图片描述

1)配置VM Options参数

-XX:+UseG1GC -Xloggc:./gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+PrintGCDateStamps

gc.log内容输出在此Java项目目录下

2、获取线上运行服务的gc.log

一般线上服务器的GC情况都是写在gc.log文件中,可以直接查看此文件内容进行GC日志分析

分析过程,可以参考下文,或者直接使用工具:GCEasy、GCViewer

3、 当本地SpringBoot项目启动耗时特别长,这时可以打印GC日志看一下 原因

是否有FullGC

[Full GC (Metadata GC Threshold)

因为项目刚刚启动的时候,要加载很多类,如果元数据空间不足,可能引发FullGC。这个时候可以适当调大一下元空间

‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M

10.3.1.3 GCEasy-日志分析

1、直接将gc.log上传到GCEasy中

http://gceasy.io/

2、分析结果说明参考

一文学会使用GCeasy——一款超好用的在线分析GC日志的网站_gc在线分析-CSDN博客

3、其它分析工具:

https://github.com/chewiebug/GCViewer


10.3.1.4 人工-日志分析

1、YGC日志分析

CommandLine flags: -XX:InitialHeapSize=266802112(初始堆内存256M) -XX:MaxHeapSize=4268833792

2024-07-28T10:57:29.710+0800: 0.176: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0197663 secs]
   [Parallel Time: 12.1 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 175.7, Avg: 176.0, Max: 176.3, Diff: 0.7]
      // 1.1初始化标记
      [Ext Root Scanning (ms): Min: 0.0, Avg: 2.3, Max: 7.9, Diff: 7.9, Sum: 9.2]
      // 1.2、并发标记中-更新RS
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      // 2.1、转移节点源RSet
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      // 2.2、转移节点源“根”节点
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      // 2.3 转移-转移对象-复制对象
      [Object Copy (ms): Min: 0.0, Avg: 0.5, Max: 0.7, Diff: 0.7, Sum: 2.0]
      [Termination (ms): Min: 0.0, Avg: 6.1, Max: 9.8, Diff: 9.8, Sum: 24.2]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 7.9, Avg: 8.9, Max: 11.2, Diff: 3.3, Sum: 35.5]
      [GC Worker End (ms): Min: 184.2, Avg: 184.9, Max: 186.8, Diff: 2.7]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.0 ms]:清理卡表
   [Other: 7.6 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 7.4 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.1 ms]
      [Free CSet: 0.0 ms]
 [Eden: 4096.0K(14.0M)->0.0B(10.0M) Survivors: 0.0B->2048.0K Heap: 61.0M(256.0M)->1098.8K(256.0M)] 

1)触发时间

2024-07-28T10:57:29.710+0800: 0.176:[]

时间戳、在Jvm启动了0.176s后执行的此次GC

2)GC类型、原因、GC 对应用造成的实际停顿时间 (STW)

[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0197663 secs]
  • 原因:大对象Humongous分配申请不到足够内层,导致的无法存入老年代region中,触发了GC

    常见的触发GC的其它原因:

    • G1 Evacuation Pause (转移:复制+清理后的对象,没有足够的空间来存储他们)
  • GC的类型:YGC

  • 对应用造成的实际停顿时间: 0.01977s

  • (initial-mark)表示触发YGC后,开始初始化标记步骤

3)转移

[Object Copy (ms): Min: 0.0, Avg: 0.5, Max: 0.7, Diff: 0.7, Sum: 2.0]//2.转移-copy
[Other: 7.6 ms]
      [Choose CSet: 0.0 ms] //1.选择CSet
      [Humongous Reclaim: 0.1 ms]:大对象回收
      [Free CSet: 0.0 ms]:清空CSet

4)堆使用情况: GC之后打印的

[Eden: 4096.0K(14.0M)->0.0B(10.0M) Survivors: 0.0B->2048.0K Heap: 61.0M(256.0M)->1098.8K(256.0M)] 
  • Eden:年轻代中的Eden区从4096.0K(14.0M)减少到0.0B(10M)
    • 表明Eden区在这次GC后被完全清空
    • Eden区的总容量从14.0M变为10.0M,可能是因为GC后调整了Eden区的大小
  • Survivor区从0.0B增加到2048.0K
  • 堆内存从61.0M(256.0M)减少到1098.8K(256.0M),但实际总堆大小(256.0M)未变,只是使用中的部分减少了

2、MixedGC

29.268: [GC pause (G1 Evacuation Pause) (mixed), 0.0059011 secs]
   [Parallel Time: 5.6 ms, GC Workers: 4]
      ... ...
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.3 ms]
      ... ...
   [Eden: 14.0M(14.0M)->0.0B(156.0M) Survivors: 10.0M->4096.0K Heap: 165.9M(512.0M)->148.7M(512.0M)]

3、FullGC

1)人为调用

System.gc();

2)回收无效果

837.762: [GC pause (G1 Evacuation Pause) (mixed)-- 99G->99G(100G), 32.0781615 secs] 
869.842: [Full GC (Allocation Failure) 99G->62G(100G), 169.3897706 secs]

第一行回收后,还是99G,没有效果,堆还是马上要满了。这时FullGC

3)元空间不足

[Full GC (Metadata GC Threshold)

10.3.2 GC日志调优实战

1、背景

发现在14:00 - 14:05之间,服务下载接口偶发性超时

2、分析过程

1)dowp日志

  • 使用公司工具dump一份日志 .hprof

2)分析日志

  • 发现在14:00 - 14:05之间,频繁有FullGC,FullGC时长1s多
  • 发现FullGC的信息不正常,G1中old region的使用率不到45%,就触发了FullGC
    • 说明有大对象
    • 大对象(>50%的region)即需要用humongous即连续的region存储,当没有足够的连续region,会提前触发FullGC
    • 当时我们物理内存是16G,16G / 2048,即每个region差不多 = 8M, 50%region = 4M
  • 使用公司工具(类似MAT和JProfile)
    • 查看 .hprof内容视图,根据视图 对象类型、实例数量和占用空间大小的详细信息,发现了大对象
    • 同时看火焰图,高度表示栈深、宽度表示占用cpu执行时间片长度(序列化方法一般都比较长时间)
  • 大对象业务:这5min内一般是业务在页面下载excel内容的发频发时间
    • 支持80多个仓 * 3000 sku的信息 = 240000 即 20w多条数据
    • 并发查询查询获取sku先关数据response(List<DTO> list),并且将req和resp全部打印了
    • sku详情信息特别多(规格、品类、温层、价格、销量、库存等等),整个下来妥妥的大对象
  • 触发条件
    • 有的业务方,只下载部分仓的部分数据,这部分数据的日志非大对象
    • 大区管理员,会下载全国所有数据,这部分日志会形成大对象,触发FullGC

3、解决

  • 业务角度
    • 推广,尽量下载有用的数据,减少无目的的全量数据下载
  • 研发角度
    • Jvm调优
      • 通过HeapRegionSize参数调整region大小,使得大对象相对于region变成小对象(风险不可控,未采用)
    • 代码调优
      • 优化打印日志,将DTO ->优化成部分有用参数的DTO(因为库存、销量等属性经常涉及到日志排查,所以不能直接忽略)
      • 再次基础上,再将日志进行切分,将FullGC优化成执行更快的YongGC
@Slf4j
@Component
public class PartLogUtils {

    public <V> void partLog(List<V> info,
                                    int count,
                                    Logger logger,
                                    String format) {
        List<List<V>> result = splitInfo(info, count);
        logInfo(result, logger, format);
    }
    
     private <V> List<List<V>> splitInfo(List<V> info, int count) {
        List<List<V>> result = Lists.newArrayList();
        if (CollectionUtils.isEmpty(info) || info.size() < count) {
            result.add(info);
            return result;
        }
        List<V> temp = Lists.newArrayList();
        int index = 0;
        for (V item : info) {
            index++;
            temp.add(item);
            if (index % count == 0) {
                result.add(temp);
                temp = Lists.newArrayList();
            }
        }
        if (temp.size() > 0) {
            result.add(temp);
        }
        return result;
    }
    
    private <V> void logInfo(List<V> infoList, Logger logger, String format) {
        for (V item : infoList) {
            logger.info(format, GsonUtils.toJsonStr(item));
        }
    }
   
}
  • 使用
private  final  static Logger logger = LoggerFactory.getLogger(A.class);

PartLogUtils.partLog(resp, 2000, logger,"queryUserInfo:[{}]");
  • 后续

将同步导出,优化为异步导出

10.3.3 G1 VM Options调优

设置-XX:G1ReservePercent=n值更大一些,默认为 10

  • 预留一部分堆内存

  • 用于在并发标记周期,暂时存储那些“晋升失败”的对象

    • 晋升失败:老年代没有足够的空间来容纳晋升对象
    • 晋升失败代价很高

    直到下一次垃圾收集发生

  • 或使用**-XX:ConcGCThreads=n**选项增加标记线程的数量

设置堆ms = mx

  • 除非遇到了STW(G1可控),都则建议设置
  • 默认内存往往太小
    • 频繁GC
    • ms <–> mx扩容缩容

年轻代

不要 通过-Xmn 设置年轻一代的大小

设置年轻一代的大小会禁用暂停时间目标,即G1的STW可控功能失效

Yong Only Phase STW时长变长

  • Jvm表现:主要是转移-CSet 阶段耗时过长(尤其是对象复制子阶段)

  • 根因:大概率是年轻代占比变大了,可以降低

    -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent

参考资料

  • 周志明老师的《深入理解Java虚拟机》
  • 中村成洋老师的《深入java虚拟机:jvm+g1gc的算法与实现》
  • 尚硅谷康师傅的《详解java虚拟机》
  • Oracle官网
    《hotspot-virtual-machine-garbage-collection-tuning-guide》
    《java-virtual-machine-guide》
    《jvms22》
  • 及其其它网上资料
    如:Getting Started with the G1 Garbage Collector、CSDN、博客等
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值