JVM 上篇

这门课程基于Java虚拟机规范来理解。只要实现此规范就是java虚拟机。

1.8的Java虚拟机规范https://docs.oracle.com/javase/8/docs/

授课大纲

image-20211223134529298

介绍

image-20211215151928570

虚拟机与JAVA虚拟机

  • Java HotSpot JVM 是Oracle官方的虚拟机
  • java 的跨平台性是通过JVM完成的
  • java 虚拟机是个跨语言的平台可以接收比如Scala Kotlin java等多种语言
  • JAVA虚拟机有哪些
    • Classic
    • HotSpot
  • 垃圾回收器
    • G1
    • ZGC
    • Shenandoah GC

JVM 的整体结构

image-20211215161250997

image-20211215161324983

  • 类装载子系统
  • 运行时数据区
    • 方法区
    • java栈
    • 本地方法栈
    • 程序计数器
  • 执行引擎
    • 解释器
    • JIT 即时编译器
    • 垃圾回收器

JAVA代码执行流程

image-20211215161744823

image-20211215161819098

  • java编译器

    • 词法分析
    • 语法分析
    • 语法抽象语法树
    • 语义分析
    • 注解抽象语法树
    • 字节码生成器
  • JAVA 虚拟机

    • 类加载器

    • 字节码校验器

    • 执行引擎

      • 翻译字节码

        逐条执行指令

      • JIT编译器

        将字节码编译成机器指令后缓存,并作为热点代码反复执行

  • 操作系统

JVM的框架模型

指令集架构分为两种

  1. 基于栈的指令集架构

    永远执行栈顶所以可以只通过内存而无需有一个额外的指令寄存器就能执行。所以无需记忆指令集。

    但代价是什么了?压入堆栈和弹出堆栈也属于操作。

    好处比较明显的是不需要考虑CPU架构

    • 将一系列要执行的方法依次压入堆栈,如果发现后面要执行的方法低于本方法的优先级则弹出并执行然后压入堆栈。如果发现所有方法都已压入堆栈则依次弹出并且执行。
  2. 基于寄存器的指令集架构。

    指令更少

JAVA的生命周期

启动 java虚拟机的启动

是通过一个引导类加载器 Bootstrap class loader 创建一个 initial class来完成的,引导类加载器是需要加载很多类的。比如Object

  • 自定义的类加载会通过系统加载器加载的,
  • Object是通过引导类加载器加载的
  • 先加载父类再加载主类,所以Object是比较早期加载的类型。
  • 再RunTIme中会调用一个 hat0是一个native方法,掉了C
  • 一个JVM对应一个运行时数据区也就是RunTime
  • RunTiem-运行时函数,是个单立对象。
  • JNI (Java Native Interface) JAVA本地接口接口调用C语言的

java虚拟机初始化> 加载类> 运行> 正常执行结束的时候会退出,或者异常终止

Java虚拟机的历史版本

  • classic 第一个商用版本1.4被淘汰了

    • 这款虚拟机只提供虚拟机
    • 现在主流的虚拟机还会提供JIT编译器

    image-20211216224533745

    • JIT编译器可以保存热点代码
    • 字节码翻译成机器指令也会要时间。
  • Exact VM

    • Exact Memory Management 尊确式雷车管理
    • 从这开始Java虚拟机会知道这个地址是什么类型的,比如是引用还是数值
    • 热点探测(只是针对热点代码进行几十遍其)
    • 编译器与解释器混合工作模式
    • 只是再Sun Solaris平台短暂使用
  • hotspot 是现在OpenJDK 与 OracleJDK都默认使用的这款

    • 主要是GC机制
    • 多本地方法栈 Native Method Stack
    • Hot Spot就是只热点代码探测技术也是这个虚拟机的核心特性
  • BEA 的JRocKit

    • 专注于服务端应用
    • 全部代码都铱靠即时编译器,编译后执行
    • 是世界上最快的JVM
    • 有全面的低延时解决方案
    • Mission Control服务套件,一套消耗极低的监控套件
      • JMC是用来监控内存泄露的
    • 现在Oracle正在整合
  • IBM J9 IT4J

    • IBM Technology for Java Virtual Machine
    • 2017年开源
    • Eclipse Open J9
  • KVM CDC/CLDC Hotspot

  • Azul VM

    • 还有一款Zing也是Azul的
  • Liquid VM 是BEA开发的,本身就是个专用操作系统

    暂停了

  • Apache 的 Harmony

    IBM 基于 Apache协议做的 没有加入JCP

  • Microsoft JVM

  • TaobaoJVM 严重Intel CPU

  • Dalvik VM 基于5.0前的Android系统应用的

    选用的是寄存器的架构模式

    Android 5.0支持提前编译

  • Graal VM

  • 所有虚拟机都遵守一个原则,一次编译到处运行

类加载子系统

image-20211223133952809

image-20211223134125963

image-20211223134236659

image-20211223134358442

类加载器就是负责从文件系统中将Class加载自执行引擎中Execution,ClassLoader 只负责class文件的加载。

class会被加载到运行时常量池、方法区、

过程

c

加载 > 连接 > 初始化

  1. 加载 Loading 获取字节流数据、

    • 通过类的全限定名(全名)获取自定义的二进制字节流。
    • 将字节流所代表的静 态存储结构存储到运行时数据结构MATA DATA中。
    • 内存中会生成一个Class对象,作为方法去这个类的格子数据的访问入口。
  2. 连接 linking

    1. 验证(Verify)

      确保Class保证字节流中包含符合虚拟机要求,保证加载类的正确性,不危害虚拟机自身的安全。

      主要包括四种验证,文件格式验证、元数据验证、字节码验证、符号引用验证。

      起始内容是Cafebabe。不合法会报Verify错误

    2. 准备(Prepare)

      会定义变量并分配空间,并且赋值为0值。

      这里不包含final static,因为编译时就确定为常量了。

      这里不会为实例变量分配初始化。因为还没创建实例对象。还没初始化。

    3. 解析(Resolve)

      将常量池中的符号引用转换为直接引用。在这之前这些类只是一个符号。在解析之后会成为引用。

  3. 初始化

    clinit 用于处理静态代码块和静态变量的方法,但是不包括final static

    反编译后在 Methods.clinit

    执行类构造器方法的过程()里面会把静态变量putstatic到系统中,它会把显示初始化和静态代码块中的内容合并到一起。

    执行顺序会安装我们在源文件中的编写顺序来执行。

    由于在Loader.Prepare过程中已经初始化过所有静态对象。所以这里先执行number=20也不会由于没有声明导致异常。

    但是要注意,未声明前不能调用。

    clinit与init不同,是对class进行初始化的方法而不是实例化的方法

    反编译能看到

    image-20211223142823250

    image-20211223143251808

    每个类都有个内部构造器

class Loder 可以加载 系统中的文件、网络获取、zip中读取、运行计算中生成、有文件生成(JSP)从数据库中提取,加密文件中获取(反反编译)

  • 加载顺序会根据连接先压栈再执行。

  • 所以会先加载父类或依赖类最执行本类型的init方法。

  • 先执行clinit方法再执行init

  • clinit是线程被枷锁的。

总结

加载分为

加载>[验证 > 准备> 解析] > 初始化

  1. 吧class文件读取出来
  2. 验证是不是jvm能够使用的文件
  3. 加载静态变量并且给定0值类
  4. 替换符号成连接关系,并且要指定class的初始化顺序。从main方法开始引用并压栈。
  5. 按顺序执行clinit方法如果有静态方法或者静态块的话,然后执行init方法。

类加载器的种类

image-20211224150814566

image-20211224161805198

类加载器有主要分两种

  • 引导类加载器 Bootstrap ClassLoader(非java实现的)

  • 自定义类加载器 User-Defined ClassLoader

    image-20211224161511635

    所有派生于ClassLoader都属于自定义加载类

加载器是包含关系但是不是继承关系。

rt.jar下都是使用bootstrap ClassLoader加载器加载的

jre/lib/ext子目录也可以由Extension ClassLoader进行加载

加载器类型,更具包含关系进一步细化

  • Bootstrap ClassLoader

    加载rt.jar中的类.使用C编写

    image-20211224155928048

  • Extension ClassLoader

    加载/jre/lib/ext 中的类.使用Java编写继承与ClassLoader

    //可以用 *.class.getClassLoader()来加载。

  • App ClassLoader

  • 用户自定义加载器

    • 隔离加载器

    • 修改类加载的方式

    • 扩展加载源

    • 防止源码泄露

    开发一个加载类只需要继承Class Loader

    需要重写findClass

    也可以继承URLClass就不需要重写findCLass了

    过程 根据二进制流的方式读取进来,然后可以比如解密。

getClass Loader的途径

image-20211224161844579

双亲委派机制(尽量用父类加载)

java加载类的形式是什么时候需要了什么时候加载。

image-20211224162843545

  • 如果一个类加载器收到了类加载请求,他是会请求委托给父类的加载器去执行的,如果还有父类加载器还是会继续委托。知道Bootstrap。如果父类加载器可以完成加载任务则会通过父类加载器去执行。否则使用子类
    • 避免重复加载
    • 保护类的安全

沙箱安全机制(核心API的保护)

保证我们运行的代码处于沙箱之中不能超过系统限定的区域。

其他

  • 两个class是否是同一个类,判断两个类的包名是否是完全一致的。加载这个ClassLoader必须是同样的。

  • 解析类的引用时jvm需要保证这两个类的加载器是相同的

  • 类分为主动和被动的加载模式

    这个类的clinit是否被执行,这个类是否再初始化时被加载

    • 创建类的实例
    • 访问某个接口的静态变量或对静态变量复制
    • 调用静态方法
    • 反射
    • 初始化一个类的子类时也会初始化这个类
    • 作为启动类启动
    • JDK8开始提供的动态语言支持。
    • 除了以上七种都属于被动加载类

运行时数据区

image-20211224171809292

  • image-20211224172336571

image-20211224171959576

线程

  • JVM允许多线程

  • 再HotSpot种每个线程都与系统中的本地线程一一对应的。

  • RUN至于线程就是MAIN至于JVM

  • RUN方法如果异常终止了那么本地线程就会判断是否是最后一个JVM的非守护线程。如果是则关闭JVM。

线程种类

使用jconsole会看到以下线程种类

  • 虚拟机线程
  • 周期任务线程
  • GC线程
  • 编译线程
  • 信号调度线程

PC寄存器/程序计数器

参考: JVM Specification

https://docs.oracle.com/javase/specs/jvms/se8/html/

https://docs.oracle.com/javase/specs/jvms/se11/html/index.html

怎么找这个地址?

https://docs.oracle.com/javase/specs/index.html 开始找

image-20211224185330359

全名是 Program Counter Register

image-20211224185628958

JVM中的PCR是物理的寄存器的模拟

也可以理解为行号计数器,每个线程由一份、

PC寄存器就是用来存储下一行指令的地址的。

与线程的生命周期保持一直。

程序计数器会存储当前方法的JVM地址。如果是个本地方法栈的话就不是JVM地址会出现undefined

不需要考虑GC 垃圾回收,是不会报异常的

Stack Area 会溢出

总结

就是来存储下一条执行的地址

  • 为什么用PC寄存器记录当前线程的地址?

    用于记录CPU执行到哪里了,每个线程都有一个PC寄存器

虚拟机栈

接下来的课程大纲

image-20220119001451144

基于栈指令集比较小,编译器比较容易实现,跨平台,它的指令集比较多。缺点是性能会下降。

  • 运行时数据,基本类型的局部变量都是放在栈中的。

    静态常量加载时就被加载了。

    java虚拟机栈是线程私有的也就是一个线程一个 。

    生命周期:

    与线程一致。

    • 栈帧

      栈内部保存着一个一个的栈帧,一个栈帧就对应一个方法。

    • 他保存局部变量的基本数据类型,包括:

      char boolean short int long float double byte 和引用

    • 它不需要考虑垃圾回收问题,但是会出现OutOfMemoryError(尝试扩张时)和StackOverflowError(超出制定容量)

    • 如果要制定栈大小可以去docs.oracle.com中看文档。

      路径: docs.oracle.com >> java >> Java SE Technical Documentation >> Java SE >> JDK 11 >> Tools Reference >> 2 >> java >> 搜索 -Xss

      Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, and g or G to indicate GB. The default value depends on the platform:

      idea 需要在 configuration中的modify options 中添加VM Options才能有

栈的存储单位

栈的存储单位是栈帧

正在执行的虚拟机方法都对应着一个栈帧。

  • 在一个时间点上只有一个活动的栈帧。

  • 当前栈帧是正在执行的。

    Current frame,current Method,Current Class

    • PC寄存器执行的是当前方法的行。
  • 不同的线程所包含的栈帧是不允许互相引用的。

  • 异常或return会弹出栈帧。

栈帧

栈帧的内部结构

image-20220126235906848

分别由5部分

  • 局部变量表(Local Varibales)
  • 操作数栈(Operand Stack)(表达式栈)
  • 动态连接(Dynamic Linking)(只想运行时常量池的方法引用)
  • 方法返回地址(Return Address)
  • 附加信息
局部变量表

(Local Variables)

是个数字类型的数组(八大类型、returnAddress、reference都是数字),编译期就会确定局部变量表的容量大小。

局部变量表包含:

  • 当前变量
  • 入参
  • 方法体内定义的变量
  • 局部变量表与新能调优的关系最为密切
  • 局部变量表是垃圾回收的根节点,只要被局部变量表中引用的对象都不会被垃圾回收。(根搜索算法)
Slot

局部变量表中的每个值都是一个槽,64位类型(long、double)需要占用两个Slot

image-20220127005707597

  • 非静态与构造方法会把当前Object放在index为0的Slot上。

  • Slot会重用,如果除了作用域那么这个Slot可以被其他方法替代。

    image-20220127011115663

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public int getzz(){
    this.age++;
    {
    long k = 123124123984723984l;
    System.out.println(k);
    }
    int is=123123;
    {
    double d = 4.234234d;
    System.out.println(d);
    }
    return this.age;
    }
  • 局部变量必须显示复制。

操作数栈

一个栈帧除了局部变量表以外还包含一个后进先出的表达式栈。

主要用于保存计算中出现的临时结果、中间结果。

32bit的类型占用一个深度、64bit的类型占用两个深度。

其所需最大深度在编译期就定义好了。为max_stack

image-20220128003340476

  • 后续打印的二叉树可以实现。
  • JVM的解释引擎就是基于操作数栈!!!

例:

1
2
3
4
5
public static void main (String[] fuck){
int i =0;
int j =1;
int k=i+j;
}

image-20220128005549613

image-20220128005623905

image-20220128010704412

栈顶缓存技术

将栈顶的元素缓存在物理CPU寄存器中,降低对内存的读写次数。

动态连接

保存了虚拟机栈的

image-20220128014016598

image-20220128014526055

  • 这个#1就是动态连接。
  • 动态链接就是将这些符号引用(#1)转换成符号引用,在多态时能够调用父类方法但执行子类方法也是源于动态连接。

image-20220128015421452

方法返回地址 Return Address

image-20220128202440458

  • 保存的就是该方法在上一个栈帧中的PC寄存器的值。返回就返回到上一个栈帧的PC寄存器的所在位置。

  • 返回的指令有 ireturn lreturn freturn dreturn areturn(引用类型) return(void)自己猜什么意思

  • 正常执行完毕时会返回到Return Address地址并执行指令 xreturn 。

  • 如果在本方法的异常表中没有搜说道异常处理器,那么就异常退出。异常退出是不会调用return的,是没有返回值的。

    异常处理表:

    image-20220128203535665

一些附加信息

栈帧还会有一些附加信息,可能用于调试。

方法的调用
  • 静态连接

    如果调用的方法在编译期间就可知而且在运行过程中不变。

  • 动态连接

    编译期间确定不下来运行期间才可以确定下来的。

    invokedynamic //JAVA7 才增加的 JAVA8 Lambda表达式才真正实现。

    • Lambda表达式

      1
      Func func = s ->{ return true;};

    调用Lambda就是使用的invokedynamic

    Lamabda是匿名函数。 java8的这个修改扩充了jvm对动态语言的支持,可以运行JS之类的动态语言

    动态类型语言是判断变量值的类型。

    静态类型语言入JAVA,是判断变量自身的类型信息。

  • 晚期绑定

    有多态的情况下,比如方法内传入一个对象并且调用。并且这个对象有多个实现类。

  • 早期绑定

    对应于静态连接。不采用多态时。

    super.x()是早期绑定

    带有final修饰符的方法也是早期绑定。

  • 多态性

    相当于C++的虚函数,允许父类的方法指向子类的实例。

  • 非虚方法

    对应早期绑定

    final、私有方法、静态方法、实例构造器、父类方法,都属于非虚方法。

    invokestatic //祈求静态的

    invokespecial //请求特殊的 ( 私有的、super父类的、解析阶段定位以为唯一版本的、init的构造函数)

    任何使用final修饰符的方法。

  • 虚方法

    其他方法都是虚方法。

    invokevirtual //祈求虚拟的

    invokeinterface //祈求接口

方法重载

image-20220204171314895

虚方法表

虚方法表在类加载器的Resolve (解析)阶段构成。

  • 在方法区会建立一个虚方法表,使调用重载方法时不需要每次都指向这个步骤。

    作为实际方法的入口。

本地方法栈

java栈是关于native方法的堆栈,对应于虚拟机栈是关于java方法的调用的栈。

  • 是线程私有的
  • 与java方法栈一样可以设置大小是扩展的也可以是有限的。
  • 肯能会抛出OutOfMemberError 或StackOverflowError
  • jvm规范没有要求必须有 native Mothod Area。Hotspot有但是很多没有

本地方法接口

红框内的部分

image-20220204172125244

Native 方法是指一个非java语言实现的。

比如Thread中就有很多本地方法。

image-20220204174005645

为什么要使用本地方法
  • 要调用外部环境
  • java不是自举的语言

堆(堆空间)

存储的机构,主题的对象都是放在堆中的。

堆占用的区域比较大

对空间差不多就是最大的内存空间了

核心概述

image-20220205174014509

堆空间是在运行时数据区中的 Runtime Date Area

堆空间堆进程是唯一的。多个线程是共享同一个方法区

声明堆空间的方法 -Xms10m -Xmx10m

验证 通过程序 D:\Program Files\Java\jdk1.8.0_65\bin\jvisualvm.exe

image-20220205184553922

  • 堆中有个线程私有的区域叫做TLAB(Thread Local Allocation Buffer) 堆空间中的线程私有空间。

  • 堆是GC(Garbage Collection)最重要的垃圾回收区域

  • JVM中几乎所有的对象和数组都被存储在堆空间中。

  • 栈帧中保存的对象数据都是通过引用的形式。

    image-20220205212449797

    • 创建对象或数组的字节码风别是new 和newarray。
  • 为了不平凡GC访问堆空间所以才有GC。

内存细分

现在的垃圾回收通常都是通过分代来进行垃圾回收的

通过分代垃圾分级算法进行分类和回收。

JDK8以后内存区域分类为New>Tenure>Meta

image-20220207225457325

  • new 新生代区

    几乎所有的对象都是新时代创建的

    大部分对象的销毁都是在新生代

    • eden 伊甸园区
    • survivor 幸存者区域
      • S0 FROM
      • S1 TO

    复制算法会导致S0或S1中只有一个会使用,他们是主备

  • Tenure 后生代区

永久区和元空间是不包含在堆空间中的

  • Mata 元数据区/永久区
  • JDK7中Mata叫做perm / 永久区

设置堆内存大小(年轻代与老年代)

  • 如果内存用完会报错OutOfMemor
  • -VM options中设置
    • -Xms 设置初始堆空间大小
    • -Xmx 设置最大堆空间大小

X 代表JVM的运行选项

ms 是Memor start 的缩写

mx 是Memor max

默认Xms是电脑内存的1/64

最大内存是物理内存的1/4

  • 设置新生代和老年代的比例参数

    1
    2
    3
    4
    -XX:NewRatio=2 #表示 新1老2 默认是2
    -XX:NewRatio=4 #表示 新1老4
    -XX:SurvivorRatio=8 #表示 表示伊甸园区是幸存者区的8被。两个幸存者暂2,所以是2/10和8/10
    -Xmn:200m #显示指定新生代大小,它的优先级高于NewRatio=2

    查看方式

    1
    2
    jinfo -flag ServivorRatio #{id}
    # jinfo -flag SurvivorRatio 20984
  • 自适应

    但是设置了比例实际运行不一定按照设定来,由于jvm有一个自适应参数

    1
    2
    3
    4
    #-XX:-UseAddptiveSizePolicy # 关闭自适应内存分配策略
    #但是实际上没用
    #用这个可以关闭自适应分配
    -XX:SurvivrRatio=8
如何获取当前虚拟机使用的内存大小。
1
2
Jps
jstat -gc 2824

image-20220207223312581

图解对象分配过程

在内存分配实JVM设计值不仅需要考虑内存如何分配,并且需要考虑分配后如何进行垃圾回收已经运行过程中产生的内存碎片。

survivor区满了不会触发垃圾回收的。

在进行垃圾回收时经常操作新生代,较少操作老年代,几乎不懂元空间。

新生代的对象分配过程
  • 1.当伊甸园区满的时候进行YGC并进行垃圾清理,仍然有引用的对象会被放入S0或S1。

    image-20220208234643652

  • 再进行垃圾回收时

    image-20220208235626080

  • 当幸存者区域的对象达到永久区域的age限制默认是16,这个对象会晋升(Promotion)到Tenured区域

    image-20220209000900332

    • -XX:MaxTenuringThreshold= 可以设置
整体对象分配过程

image-20220209011203357

特殊情况
  • 默认情况下对象会在eden中被创建,然后再eden满了后进行YGC,YGC会先然后将survivor from区内的对象进行存活判断将存活的对象与eden中新存活的对象一起复制到survivor to区域并且在age上加1,此时sfrom与eden区都会被清空,自此survivor to区与survivor from角色发生对调。
  • 如果对象大于eden的情况下会直接放入old区。
  • 如果小于eden但是eden放不下则会进行YGC。
  • 如放入对象时old区满了会进行FULL GC。
  • 如果FGC后对象无法存入则报出错误OOM。
  • 如果Survivor 区域的对象age超过了阈值,默认是15则发送到old区。
  • Survivor 区域中相同年龄的对象的总大小要大于Eden的50%则直接将此年龄段放入OLD中。
  • 空间分配担保 如果伊甸园放不下直接进入老年区 -XX:HandlePromotionFailure
常用调优工具
  • JDK指令 JMAP Jinfo Jawap
  • JvisualVM

Minor GC、Major GC、Full GC

在執行GC线程时间会暂停用户线程“STW”,

重点需要关注的是Major GC和Full GC,由于他们的执行时间较长通常关于java的新能调优就是针对它两的。

垃圾分类主要分类两类一类是Full GC 整堆的垃圾收集,和Partial GC 部分垃圾收集。

  • Full GC 收集整个Java堆的方法区。

    • System.gc()
    • 老年代的空间不足
    • 方法区空间不足
    • 要存入的对象大于老年去的剩余空间

    full GC要尽量避免

  • Partial GC

    • Minor GC/Young Gc

      主要是Eden区满了后会触发YGC。Survivor满不会触发GC

      • Survivor区如果是满的那么YGC时会直接吧对象放到Old区

      • 在进行Minor GC前需要确保MinorGC是安全的。

        所以会先盘点老年去是否有一块大于YoungGC的连续区域。

        如果HandlePromotionFailure = True 那么Old区只需要保证比历次晋升的最大对象的平均值大就可以了。

        如果尝试失败才会进行FULL GC

        如果HandPromotionFailure = FALS否则会先进行FULL GC。

    • Major GC/Old Gc

      会触发STW

      通常会伴随一次Minor GC

      • 目前只有CMS GC会有单独收集老年代的行为。
      • 很多时候Major GC和Full GC容易混淆
    • Mixed GC 混合收集 收集整新生代和部分老年代

      目前只有G1 GC会有这种行为

举个例子

写个会OOM的例子VM Option中加入

1
-XX:+PrintGCDetails

image-20220210160356864

对象分配内存TLAB

堆区是线程共享区域,TLAB是堆区域中一个私有的线程私有的缓冲区。是在Eden中分配的。

image-20220210162542358

  • 通过-XX:UseTLAB设置是否开启TLABTLAB是内存分配空间的首选。
    • TLAB仅占Eden的1%
    • 可通过jinfo -flag UseTLAB #id 来查看TLAB是否开
  • 可以通过VM Option-XX:TLABWasteTargetPercent来设置TLAB在Eden中占用的比例。
  • 如果线程对象在TLAB中分配失败了则会使用加锁的形式存放在Eden中。
TLAB设计堆内存的优化作用。
  • 线程安全性增加。
  • 不需要考虑TLAB中的对象的同步问题所以侧面提升了内存的吞吐量。
考虑TLAB区域的对象创建图解

image-20220210163716627

小结堆空间的参数设置

  • -XX:+PrintFlagsInitial:查看所有参数默认值

  • -XX:+PrintFlagsFinal:查看所有参数最终值

  • -Xms: 初始堆空间大小 默认 1/64物理内存

  • -Xmx: 最大堆空间大小 默认 1/4物理内存

  • -Xmn:设置新生代大小 优先级较高

  • -XX:NewRatio:配置新生代与老年代的比例()

  • -XX:SurvivorRatio:设置新生代中Eden区域与Survivor的比例

  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄

  • -XX:+PringGCDetails:输出详细的GC处理日志

  • -XX:+PrintGC -verbose:gc 打印gc简要信息

  • -XX:HandlePromotionFailure 设置是否空间分配担保

    在进行Minor GC前需要确保MinorGC是安全的。

    所以会先盘点老年去是否有一块大于YoungGC的连续区域。

    如果HandlePromotionFailure = True 那么Old区只需要保证比历次晋升的最大对象的平均值大就可以了。

    如果尝试失败才会进行FULL GC

    如果HandPromotionFailure = FALS否则会先进行FULL GC。

  • -XX:+PrintEscapeAlanlysis 开启逃逸分析

堆是分配对象的唯一选择吗

  • 《在深入理解JAVA虚拟机》中有这样的描述,随着JIT编译器的发展与逃逸分析技术的成熟,栈上分配与标量替换技术可能导致对象不一定被分配在堆上。
  • 通过逃逸分析发现一个对象并未陶出一个方法,则可以在栈上分配。
  • TaoBaoVM 其中的 GCIH (invisible heap) GC 不考虑GCIH中的对象
  • 通过逃逸分析Hotspot虚拟机能够分配出来一个新的对象的引用使用范围,从而决定是否要将这个对象分配到堆上。
    • 如果只在方法内部引用就是没法发生逃逸。
    • 显示打印逃逸分析-XX:+PrintEscapeAlanlysis
    • 关闭逃逸分析 -XX:-DoEscapeAnalysis
答案

是只分配在堆空间上的。

  • 是在Oracle Hotspot中仍然是的,因为还没有实现栈上分配,只实现了标量替换。

  • JDK7之后String 已经被分配道堆空间而不是方法区中了

优化JVM性能

基于逃逸分析进行代码优化

能在方法内作为局部变量的不要通过传递获得。

使用以下代码 B

-XX:+PrintGCDetails -XX:-DoEscapeAnalysis -Xms256m -Xmx256m

再使用jvisualvm查看User对象个数会发现 + - DoEscapeAnalysis不同情况下有明显不

同步省略

编译器JIT在执行同步代码块的时候会判断同步代码块所使用的锁对象是否只能被一个对象所访问而没有被发布到其他线程中去。如果是可以吧锁对象给消除。也叫消除锁

标量替换、分离对象

有些对象可以不使用连续的内存空间存储

  • 标量(Scalar) 不可分割的量、如int long
  • 聚合量(Aggregate) 含有多个标量的对象

如果通过逃逸分析发现这个对象只在本方法(栈帧)中使用到,那么会将简化为内部的标量在内存中创建

开启标量替换参数设置

-XX:+EliminateAllocations 默认开启的

思考

如果一个有参对象被创建,事实上就进入了另一个栈帧。就无法保证这个量是线程安全的。

但是如果进行标量替换那么就不会调用如同构造函数之类的方法。就能保证在本栈帧中执行完毕。

方法区

运行时区的完整结构

image-20220216212320346

运行时线程共享角度

image-20220216212433848

方法区、栈、堆的交互关系

image-20220216212802050

Class相关的内容我们叫做方法区

在堆中的对象会有一个指针指向方法区。

方法区在哪里

方法区还有一个名字叫做non-heap 非堆

指的是方法区不是堆的一部分,在JVM规范8中规定方法区可以不做垃圾回收或内存压缩

方法区的理解

  • 方法区与java堆一样,是各个线程共享的区域。
  • 方法区再JVM启动的时候被创建,并且他的实际物理内存空间中的Java堆区域一样都可以是不连续的。
  • 方法区的大小跟堆空间一样,可以选择固定大小或者扩展。
  • 发放去大小决定了系统可以报错多少类,如果系统定义太多类,导致方法区一次,虚拟机同样会OOM。
  • 关闭JVM会是否这个区域的内存。
  • 生成了过多的放射类也可能导致JVM方法区OOM
  • 方法区和原空间并不等价
  • 原空间没在用JVM的内存,用的是native的内存
  • 原空间与永久代的重要区别在于使用的内存是JVM的还是本地内存。

设置方法区的大小以及OOM

  • 方法区可以设置为一个可怎张的空间,也可以奢侈成固定大小。

  • 默认MataSpaceSize是21m大小

  • 默认的MaxMataSpaceSize是-1(无限制)

  • -XX:MetaspaceSize=111m

  • MaxMetaspaceSize也叫高水位线,如果超出那么会金仙FULL GC

  • 每次触发Full GC都会调高或者调低mataspace的空间阈值

    如果释放的空间较多则调低阈值,如果释放的空间较少则调高阈值。

测试它:

使用参数-XX:MaxMetaspaceSize=100m

再配合代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package test;

import javassist.bytecode.Opcode;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
* @Author akachi
* @Email zsts@hotmail.com
* @Date 2022/2/17 16:09
*/
public class _3MataSpace溢出测试 extends ClassLoader {
public static void main(String[] args) {
long j =0;
try {
_3MataSpace溢出测试 test = new _3MataSpace溢出测试();
for (long i = 0; i < 10000000000l; i++) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "ClassMataSpace" + i, null, "java/lang/Object", null);
byte[] code = classWriter.toByteArray();
test.defineClass("ClassMataSpace" + i, code, 0, code.length);
j=i;
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(j);
}
}
}

执行后会抛异常Metaspace OOM

如何解决OOM
  1. 要解决OOM异常在堆空间中发生的,我们可以使用dupm工具来判断是内存泄漏或者是溢出。

  2. 如果是内存泄漏可以通过工具查看泄露对象,找到GC Roots的引用连接。掌握泄漏对象类型信息以及GC Roots引用连接信息就可以比较准确的定位出泄漏代买的位置。

  3. 如果不存在内存泄漏那么就应该适当的修改-Xms -Xmx来怎加内存。

    另外需要确认一下内存中的对象的生命周期是否太长,是否可以提前释放。

方法区的内部结构

image-20220217163302496

image-20220217163509511

存放的数据信息:

  • 类型信息

    • 域信
    • 方法信息
  • 静态变量

  • 运行时常量池

  • JIT代码缓存

  • 变量放在哪

    • 成员变量的引用(类的变量)

      放在堆中

    • 方法内局部变量的引用

      放在栈帧中

    • 对象实例

      放在堆中

类型信息
1
2
3
4
5
6
7
public class test.Test3
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #15 // test/Test3
super_class: #18 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 1

每个加载的类型(Class、interface、enumerate、annotation)

  • 这个类型完整有效的名称
  • 这个类型直接父类完整有效的名称(对于interface或是Object,都没有父类)
  • 这个类型的修饰符(public ,abstrace,final)
  • 这个类型的直接接口的顺序列表
域信息(field)
1
2
3
private byte[] bytes;
descriptor: [B
flags: (0x0002) ACC_PRIVATE
  • JVM会在方法区中保存类型中阈的相关信息以及声明顺序。
  • 域的相关信息包括:阈名称、类型、修饰符(public final volatile)
方法信息(Monthod)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public test.Test3();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=8, locals=2, args_size=1
51: return
LineNumberTable:
line 12: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
15 36 1 i I
0 52 0 this Ltest/Test3;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 15
locals = [ class test/Test3, int ]
stack = []
frame_type = 250 /* chop */
offset_delta = 35
  • 方法名称
  • 返回类型包括void
  • 方法参数和类型按顺序
  • 方法的修饰符 native、public、abstract等
  • 方法的字节码 bytecodes、操作数栈、局部变量表及大小(abstract、native)方法除外。
  • 异常表

来玩方法区

1
javap -v -p Test.class
non-final的类型变量

static finale 与static的处理方式是不同的。

每个全局常量在变异时就以及分配了

image-20220218164158333

  • static属性

  • static final

    在编译的class的文件中会出现contantValue:int 2

    在loader class时 在Linking>Prepare环节会默认初始化并且赋予0值

运行时常量池与常量池

常量池

1
2
3
4
5
6
7
8
9
10
11
12
Constant pool: //常量池
#1 = Methodref #18.#47 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#48 // test/Test3.bytes:[B
#3 = Class #49 // java/lang/Double
#4 = Methodref #50.#51 // java/lang/Math.random:()D
#5 = Double yis 2.0d
#7 = Methodref #3.#52 // java/lang/Double."<init>":(D)V
#8 = Methodref #3.#53 // java/lang/Double.byteValue:()B
#9 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
#10 = String #56 // 1
#11 = Methodref #57.#58 // java/io/PrintStream.println:(Ljava/lang/String;)V
#12 = String #59 // 2

一个字节码文件包含了一个常量表 ContentPool 其中包含了自变量和各种引用,以上的#5,就是个Double类型的字面量,其他都是引用。

比如以上实际代码中有这样一段bytes[i] = new Double(Math.random() * 2).byteValue();

中的2被创建为一个Double并且是被引用的。

1
bytes[i] = new Double(Math.random() * 2).byteValue();

也叫符号引用。

常量池中有什么

  • 字符串值
  • 数量值
  • 类引用
  • 字段引用
  • 方法引用

小结:

常量池可以看作是一张表,虚拟机指令根据这张表找到要指向的类名、方法名、参数类型、字面量类型等等。

运行时常量池

image-20220218164603836

字节码文件中的常量池通过类加载器加载道内存中以后,就叫做运行时常量池了。

运行时常量池就是字节码中的常量池表的运行形式.

每个类或者接口都会对应一个常量池引用.

运行时常量池具备动态性, 类似于String.intern()

方法区使用举例

看这样一个方法的方法区对应表的情况

image-20220221162402557

image-20220221163510515

代码具体执行步骤详解:

操作数栈是栈、本地变量表是数组。

  • sipush 500 在操作数栈中放入int 500

  • istore_1 将操作数栈的500放入本地变量表的1的位置

  • bipush 100操作数栈中存储100

  • istore_2 操作数栈中去除放入本地变量表2中

  • iload_1 从本地变量表中取出位置1的int放入操作数栈东

  • iload_2 取出2的位置放入操作数栈中

  • idiv 弹出两位做除法操作得到5压入栈中

  • istore_3 弹出并存储5道本地变量表的3位置中

  • bipush 50 压入50到局部变量表中

  • istore 4弹出并且存入局部变量表4

  • getstatic #2 从常量表中取出#2并压入栈

    image-20220221170113134

  • iload_3 取出3 也就是数字5

  • iload 4 取出4 50

  • iadd 弹出两位并做加法获得55

  • invokevirtual #3 执行这样一个方法 这会创造新的栈帧并且会把55(操作数栈中的对象)放入操作数栈中

    这是个虚方法调用,可能被重写的方法都会调用这个。

  • return 返回

与此同时程序计数器会一直记录当前位置。

方法区的演进细节

image-20220223205900875

hosport 在1.8将permspace改为了mataspace.并且其中的方法区移交到了mataspace.

  • mataspace 是在JVM管理以外的内存.

  • 字符串现在在堆内维护

  • 原来只有在进行FullGC世字符串才会进行回收.现在放在堆中回收频率会较高.

  • 变量放在哪

    • 成员变量的引用(类的变量)

      放在堆中

    • 方法内局部变量的引用

      放在栈帧中

    • 对象实例

      放在堆中

方法区的垃圾回收

  • 判断是否废弃、该类的实列已经全部被回收
  • 该类的加载器已经被回收该类的
  • java.lang.Class对象没有在任何地方被引用。

总结

image-20220223220230660

对象实例化内存布局与访问方式

对象的实例化

对象的创建方式
  • new java/long/object

  • Class的newInstance()

    放射方式,调用空参public的构造器

  • Constructor的newInstance(Xxx)

    反射,调用空参或者代参的构造器,权限没有要求。

  • 使用clone()

    不用调用任何构造器,类需要实现Cloneable

  • 使用反序列化

    使用对象二进制流

  • 使用第三方库Objenesis

创建对象的步骤

image-20220226195404102

  1. 先加载类型Object
  2. dup (duplicate)
    1. 复制并再操作数栈中加入一条
  3. new时会做一个0值初始化
  4. invokespecial时再赋值.
  5. 从操作数栈中取出来放到局部变量表里
六个步骤

image-20220227210216939

  1. 判断这个Class是否已经被加载,如果没有被加载则通过类加载器加载。

  2. 分配内存,判断内存所占用的空间大小

    GC的方式决定了分配对象的方式。

    1. 如果内存规整 指针碰撞

      内存中有个当前所在指针,会由这里开始开辟内存空间。并且更改指针所在位置。

      串行的Serial和并行的ParNew垃圾收集算法(标记压缩)。

    2. 内存不规整的情况下需要维护一个空闲列表来进行分配(标记清除)。

      交错内存从FreeList中找到足够大的空间来存放实例。比如CMS使用的标记清楚算法

  3. 并发安全问题

    1. 采用CAS失败重试,区域枷锁保证更新的原子性
    2. 使用TLAB( thread local allocation buffer)
  4. 初始化分配到的空间

    初始化有以下几种形式

    • 复默认初始化值基本上是复0
    • 显示初始化和代码块初始化
    • 构造器当中初始化

    初始化分配到的空间指的是第一种 默认初始化值。

  5. 设置对象头

    • 类型数据(元数据区的指针)
    • 对象的Hash Code
    • GC信息
    • 对象锁信息
  6. 执行init方法进行初始化

对象的内存布局

  1. 对象头(Header)

    • 运行时元数据(Mark Word)

      • 哈希值(HashCode)

        局部变量表指向堆空间的地址的指针就是这个Hash Code

      • GC分代年龄

      • 锁状态标志

      • 线程持有的锁

      • 偏向线程ID

      • 偏向时时间戳

    • 类型指针——指向元数据InstanceKlass,确定该对象所属的类型

    • 长度,如果这是个数组

  2. 实例数据(Instance Data)

    真正存储有效信息的区域,包括父类和本身的成员变量。

    规则

    • 先父类再子类
    • 相同长度的变量会放在一起。
    • 如果CompactFields=true窄变量会放在父变量的间隙中
  3. 对其填充(Padding)

    保证类是基于额定的大小。

参考图

这是个叫做Customer的对象在内存中的具体图解,从main方法的栈帧中的局部变量表引用一直到堆空间。

image-20220227223840123

对象访问定位

JVM就是通过栈上指针找到堆区中的对象的,对象的meta信息又来源于对象堆meta space中的klass的引用。

image-20220227225737837

句柄访问

image-20220227230143763

实现:

好处:

在标记整理算法时,移动了引用时修改起来比较方便。

直接指针host sport使用

image-20220227230228134

好处:

效率较高,但是如果对象进行了整理可能需要修改reference

直接内存

直接内存不是JVM运行时中的一部分,也不是JVM规范中定义的内存区。

最早引用是在1.4的NIO,在1.7升级为NIO2.0

在1.4的DirectByteBuffer 中就有操作Native内存

也可以理解为 Non-Blocking IO

操作NIO时通常都是用过一个Stream 在内存中读写byte[],每个流都可以建立一个channel

比如netty框架中操作

  • 使用直接内存也会在操作系统的java进程中显示
  • 直接内存不属于Meta space
  • 如果直接内存超出会报错为OutOfMemoryError: direct buffer memory
  • 可以在JVM启动项中设置MaxDirectMemorySize大小

执行引擎

image-20220302154141554

1.执行引擎概述

执行引擎是JAVA的核心部分之一。

主要负责编译与执行

这里的编译指的是后端编译与javac的前端编译不是一个概念

image-20220302155250246

执行引擎主要面对的有 PC寄存器\操作数栈\局部变量表

主要操作的就是操作数栈.执行引擎要执行的指令完全依赖于PC寄存器.

2.代码编译和执行过程

image-20220302155736284

程序源码在转化为目标代码或是解释执行之前都需要经历上述步骤

  1. 先从执行源码开始
  2. 前端编译器(javac)负责橙色部分,前端编译器会把源代码编译成线性的命令流
  3. 绿色部分是解释执行的过程
  4. 蓝色部分是传统计算机的编译执行过程

JAVA语言的前端编译过程

image-20220302160112322

JAVA执行过程

image-20220302160258426

!之所以JAVA叫半编译半解释行语言不是我们默认理解的先做前端编译然后再做解释执行.

是因为java的执行引擎中既可以使用解释器也可以使用编译器

1.0时只有解释器,后来才有后端编译器.

这样可以把字节码文件翻译成本地代码,并且使用方法区meta space做缓存.

在执行的时候可以直接调用机器指令.

而且可以在执行过程中进行优化.

什么叫解释器(Interpereter)

image-20220302161420754

什么叫编译器JIT

image-20220302161443655

3.机器码 指令 汇编语言

  • 二进制的指令集

  • 机器指令

    被计算机理解和接受,但是不易与人类理解

  • 高级语言

    CPU可以直接读取和执行的语言,执行速度最快

  • 汇编

    不同CPU执行不同的命令集

  • 指令

    表示一系列机器码的集合,我们也可以叫做单词.

    比如mov是传送数据,inc等等.

  • 指令集

    指令的集合.不同平台有不同的指令集 比如X86 arm等

  • 汇编用助记符替代机器指令的操作码.

  • 高级语言

    image-20220302163159984

    先吧高级语言编译成汇编,再把汇编汇编成机器指令。

    C语言

    image-20220302163438974

  • java

    JAVA有个叫做字节码的中间过程。

    字节码再翻译成汇编。

    汇编再汇编执行

    java的执行引擎是专门用于执行字节码文件的

4.解释器

解释器主要是针对字节码的。

解释器的性能更加低下。

  • 解释器一启动就可以直接执行字节码,省去了不必要的编译时间。

5.JIT编译器

即时编译器会把整个函数体翻译成机器码,并且会缓存到meta space空间中。

在Hostport虚拟机中 解释器与JIT同步存在。代码可能会采用其中一种办法执行。

  • 随着程序的运行时加逐步启动,根据热点探测将字节码编译为机器指令并缓存到内存中。
  • 在JRockit VM中只包含JIT
  • 编译器会在某些极端优化情况下新能低于解释器,所以在特殊情况下解释器可以作为编译器的逃生门。
  • 反复执行的循环代码和多次执行的方法在hotsport虚拟机中会进行栈上替换。
  • hotsport是基于计数器的热点探测。包括方法调用计数器和回边计数器。
  • -XX:CompileThershold 来认为设置在client模式下是1500次,在server模式下是10000次。
  • -XX:-UserCounterDecay 来进行关闭热度衰减,这样在进行统计时指挥进行绝对次数的统计。
  • -XXCounterHalfLifeTime设置半衰期时间。
  • 超过这个阈值会触发JIT
  • -Xint 是解释器模式,-Xcomp纯编译器模式, -Xmixed 混合模式
  • -Server 模式和 Client模式的区别,C1编译器是client,C2是指Server编译器。C2更加激进的优化。
  • -client和-server可以进行选择,64为操作系统是不可以切换的,必须是-client
    • C1的优化方式,方法内连、去虚拟化:对唯一实现进行内连(主要针对虚拟类)、冗余消除(String 的+处理)。
    • C2的优化,标量替换(拆分对象)、栈上分配、同步消除。使用C++编译
    • 分层编译策略开启时:不开启性能监控会使用C1使用 ,开始性能监控时会使用C2

什么情况下使用JIT编译器

  • 方法调用计数器

image-20220303213251654

回边计数器

image-20220303214324966

graal编译器

JDK19引入的目前已经 追平了C2

  • 使用-XX:+UnlockExperimentalVMOptions -XX:UseJVMCICompiler 两个options来开启

6.ATO编译器(Ahead Of Time Compiler)

Jdk9引入的静态提前编译器,编译工具jaotc,它接住了Grall编译器,将所有输入的Java类文件转换为机器码,并存放到动态共享库中。

格式为.so

.java =====javac===>>.class =====jaotc=====>> .so

StringTable

StringTable是指的String常量池,通常保存的是引号包裹的字面量。和调用Intern的String。

StringTable是一个唯一的HashSet存储的LinkedList

String的基本特性

string是字符串,双引号表示。

可以new String,也可以使用””来创建字符串。

String是final的不可被集采。

String可被序列化,通过Serializable。

  • JDK8 使用char[]

  • jdk1.9就开始使用byte[]

    原因:

    string是堆空间的主要不问,大部分的string都是拉丁字符。拉丁字符使用一个byte就可以存储。所以UTF-16的char array修改为byte[] 加一个标识符来完成。

    再1.9后string buffer,string builder都做了更改

  • stirng是个不可变的字符序列。

  • 字符串常量池是绝对没有相同的字符串的。

  • 任何对string的“修改”都是创建一个新的字符串。

  • String的String Pool是一个固定大小的Hashtable默认大小再1.6是1009,jdk7中是60013。1009是可设置的最小值。这个长度指的是hash的分区长度。如果相同的hashcode会导致hash冲突。

  • 使用-XX:StringTableSize 进行配置。

  • jinfo -flag StringTableSize 4060

  • jdk1.6中可以任意修改table大小,jdk1.7开始就必须保持1009以上。

链表出现的概率小执行效率会更块。

String的内存分配

从JDK7以后都是保存至堆空间heap space中的

String的基本操作

  • 通过IDEA的Debug工具中的Memory的java.lang.String中可以看到所有字符串

相同的字符串不会被再次加载

image-20220306203833123

字符串拼接操作

  • 常量相加 “”+”” 会在编译器优化

  • Sting pool是不会存在相同的常量的

  • 只要拼接的时候有一个是变量,那结果就是变量。 比如b=1 ;b+””和 “1”+“”是不同的

    如果前后符号中出现了变量则相当于在堆空间中出现了new String();具体内容的拼接的结果。

    只要new了对象就是一个新的引用地址。就算他们在String pool中的指针都是相同的。

    image-20220306213344204

    调用了inter()会进行判断Sting Pool中是否存在,如果存在就返回。否则就创建返回。

    在调用变量的字符串相加的时候,在字节码中使用的是StringBuilder.append(“A”)。

    最后sb.toString。这个操作约等于 new String(“ab”)。这会在内存中有个独立的对象

    String在进行==判断时判断的是内存地址

    在5.0前使用的是StringBuffer

  • 带final的变量等同于字面量。

    多用final修饰代码,可以尽可能的将内存创建在栈中,这样能有效减少gc。

    使用final在编译时就已经确定再字节码中了。

intern()的使用

如果字符串常量池中没有则再常量池中生成。

intern()的方法比较等价于.equals(t)

image-20220306223642794

在1.7之后的包括1.7的JDK版本中,如果调用intern()方法则会在String Pool中创建一个对象或者直接使用这个String[]对象的引用存储在String Pool中。

  • 关键测试题

  • 执行intern时如果String Pool中没有这个对象,但是堆空间heap space中有,那么会把String Pool中的引用指向它。

    如果直接声明字面量则会创建在Pool中

使用intern会减少内存对象数量。

StringTable的垃圾回收

使用了Intern()就是唯一的了不需要做垃圾回收了。

否则new出来的String作为一个对象该怎么回收就怎么回收。

-XX:+PrintStringTableStatistics

image-20220307002906037

StringTable是会进行垃圾回收的。YGC就会堆StringTable进行垃圾回收。

G1中的String中去重操作

G1的垃圾回收会对String进行去重

  1. 当垃圾回收工作时堆每一个访问对象都会检查是否是候选的要去重的String对象。
  2. 如果是就吧这个对象插入到一个待处理队列中。
  3. 使用一个hastable来记录所有的String对象。并且释放原来的数组。并且使用这个引用。如果不存在则插入到这个hashtable中。

开启字符串去重-XX:+UseStringDeduplication

打赢去重详情-XX:+PrintStringDeduplicationStatistics

-XX:StringDeduplicationAgeThreshold=5StringDeduplicationAgeThreshold uintx 到达这个年龄的对象会被认为是去重的候选对象。

垃圾回收

什么是垃圾回收

  • 什么是垃圾?

    没有任何指针指向的对象。

    可能有指针的地区比如String pool、栈帧、堆空间中的其他对象

  • 什么是内存泄漏

    广义上来说就是那些不被系统到达的内存,但是没有进行销毁。

    java中指的是仍然有引用但是与系统运行无关的对象。

  • 为什么要进行GC

    清理记录碎片

    清理垃圾

早期的垃圾回收

需要手动进行垃圾回收。使用delete来进行释放。

垃圾回收算法

image-20220313200334034

标记阶段

  • 垃圾标记阶段对的主要意义就是标记哪些对象应该被回收,哪些则不该。

    基本原则是当一个对象不再被任何对象引用了则为垃圾。

    • 引用计数算法

      对每个对象都保存一个整型的计数器,如果为0就可以被回收了。

      • 优点

        实现简单、判定效率高、回收没有延时。

      • 缺点

        需要单独的字段存储计数器,增加了内存的开销。

        每次复制都需要更新计数器。

        无法处理循环引用。两个互相引用的对象永远无法销毁。所以Java的垃圾回收机制中没有使用这类算法。

      Python是如何解决引用计数算法的循环引用?

      1. 手动解除
      2. 使用弱引用weakref Python。只要发生弱引用就回收。
    • 可达性分析算法。

      也可以叫做追踪性垃圾收集,或者根搜索算法。

      可达性分析是根据一组根对象集合为起点,搜索所有可引用的对象。通过引用路径Reference Chain来进行判断。

      只要是存活的对象都应该直接或间接连的连接到。

      • GCRoots可以是哪些元素?

        • 栈中的引用对象(本地方法栈,虚拟机栈)。

        • 静态变量。

        • 字符串常量池中的引用。

        • 所有被同步锁synchronized持有的对象。同步监视器。

        • java虚拟机的内部引用。一些常驻对象。如OutOfMemoryError。系统类加载器等等。

        • 反映Java虚拟机内部情况的JMXBean,JVMTI注册的回调、本地代码缓存等。

        • 在进行分代回收或局部回收时有可能临时性的额外增加GC Roots的对象。

          比如收集新生代时老年代的对象也可以作为GC Roots

      概括性的理解,垃圾回收指针对Meta Space和堆空间,在其他空间中引用的堆空间或Meta space中的对象就是应该保留的对象。

      • 缺点:GC Root需要在可以保持一致性的快照中完成,这就是STW 的原因。

清除阶段(三种算法)

  • 标记-清除算法 (Mark - Sweep)

    分文两个阶段,第一个环节是标记 Mark,第二个环境是Sweep。最早在1960年Lisp语言开始使用。

    image-20220313175242284

    • 先标记可被引用的对象。

    • 然后清楚不可被引用的对象

      通过线性遍历内存空间找到没有被引用但是有内容的区域清除它。

    • 这种回收机制会导致内存是非规整内存。

    • 可以维护一个空闲列表,有空闲列表的情况下就可以不真的去擦除数据。

    在这个过程中会导致stop the world

    所谓的清除斌不是真的制空,而是吧无数据的地址保存至地址放在空闲列表里。

    缺点

    • 效率不高

    • GC需要停止用户线程

    • 清理的内存不是连续的

  • 复制算法(Copying)

    image-20220313183324555

    为了解决标记算法在垃圾回收时的效率问题。1963年发明了复制算法。

    使用两个区域,每次清除垃圾时将内存区域中的数据复制到对面。一次完成垃圾回收。

    • java 的survival

    优势

    效率搞。

    复制的数据有连续性。

    无需做碎片整理。

    如果对象很多复制算法的新能就会较低。

    劣势

    需要两倍的数据内存空间。

    需要更新栈中的引用地址。

    Java在YGC中使用Servival区中使用这样的方式是非常好的。

  • 标记-压缩算法(Mark - Compact)

    1970年代发明了这个算法

    • 第一阶段也是标记,方法和标记清除一样。
    • 第二阶段将幸存的对象移动到内存的一端。
    • 然后重新定义内存末尾指针。创建对象时使用指针碰撞。

    标记压缩算法是规整内存算法。

    优点

    • 内存不用减半
    • 也没有碎片

    缺点

    • 需要STW stop the world
    • 需要修改引用

    Java 老年代用这个

  • 对象finalization

    垃圾回收时会调用finalize方法。

    可以自定义销毁逻辑。

    通常重写它在对象回收之前释放资源,比如关闭文件,套接字等。

    这个方法永远不要主动调用。它是给GC调的。

    finalize方法可能会导致对象复活。

    一个糟糕的finalize()会明显的影响GC性能。

    • 从功能上来说finalizae()方法与C++中的析构函数相似。
    • 由于finalize()方法的存在,虚拟机中的对象会有三种状态

      finalize只会被调用一次,哪怕调用finalize时重新建立关联,下次再被标记为不可触及也不会再调用。

      • 可触及的

      • 可复活的

        还未调用finalize的对象有可能在finalize中被重新关联引用而复活。

      • 不可触及的

        回收的是这样的对象。

    • 判断一个Object是否可回收至少要经历两次标记过程。

      第一次判断是否要调用finalize()

      如果是Object的finalize或已经被调用过则没必要执行

    • 如果对象重写了finalize()方法而且未被执行,那么这个对象会被插入到F-Queue队列中,由一个虚拟机自动创建的低优先级的Finalizer线程促发这个方法执行。

      image-20220310004700185

    • finalize()方法只会被调用一次,如果通过这个方法重新进入引用链,下次再被移除时这个方法不会再被执行,而是进入销毁列表。

分代收集算法(符合算法)

  • 年轻代全部使用的是复制算法。
  • 老年代则不同
CMS使用这这些算法
  • 采用Mark-Sweep
  • 使用Serial Old回收器作为补偿措施。
  • 如果发生大量碎片(Concurrent Mode Failure)将使用Serial Old进行Full GC
  • CMS在指向Full GC时使用的是 Mark - Compact(标记压缩)。

增量搜集算法

  • 无需进行,能够减少 Stop The world。
  • 每次收集一小片区域。
  • 总体说来他的基础还是标记清除和复制算法。

缺点

导致系统吞吐量的下降。

分区算法

image-20220313203349302

将堆空间划分成不同的小区间(region)

G1在使用。

  • 每个小区间都是独立回收的

增量收集算法、分区算法

MAT与JProfiler的GC Roots溯源

我们要使用MAT和JProfiler来查看GC Roots有哪些

使用这两个工具来查看GCRoot有哪些。

使用jmap 生成dump文件

1
jmap -dump:format=b,live,file=test1.bin 14036

使用java visualVM来保存dump

image-20220311022053267

image-20220311023751998

  • 安装的同时按照提示在IDEA安装插件。

  • 点击上图打开JProfiler

  • 标记当前值

    image-20220311024030852

  • 观察内存的变化

    image-20220311024136337

  • 溯源

    image-20220311024433220

    image-20220311024656357

    使用JProfiler的溯源是这样一个过程,首先监听进程> 在实体类中查看或查找有问题的对象 > 在堆遍历器中显示内容 > 然后查看

使用JProfiler做OOM分析

在options 中添加-XX:+HeapDumpOnOutOfMemoryError

image-20220311033223128

这样可以查看对象被引用情况。被谁引用。

垃圾回收的相关概念

System.gc()的理解

这是个Full GC

  • 调用的是Runtime.getRuntim.gc()
  • 不能确保马上执行GC
  • 执行的是Full GC
  • System.runFinalization() //强制调用引用对象的finalize方法

image-20220313210844803

这样的代码中的buffer是不会被GC掉的。

因为这个localvarGC3这个栈没有被回收。而且局部变量表的index为1的部分仍然指向buffer。除非将index为1的局部变量吗重新指向其他对象否则它不能被回收。

内存溢出于内存泄漏

image-20220313213235441

  • 内存溢出

    • 在进行OOM前通常会进行一次FULL GC,即便进行FULL GC也无法提供内存时才会报OOM。
    • 一个对象非常大,超过了Old heap
  • 内存泄漏(Memory Leak)

    严格上来讲只有对象不被程序用到了,但是GC又不能回收他们的情况下,才叫内存泄漏。比如使用可达性分析算法进行内存回收,对于循环引用就是内存泄漏。

    宽泛意义的内存泄漏,生命周期超过了对象应有的生命周期长度。也可以理解为内存泄漏。

    • 无法正常回收的区域就是内存泄漏。

Stop The World

在进行垃圾清理时,比如做可达性分析时就需要进行Stop the world 。

使用可达性分析算法时需要枚举GC Roots 这些数据在程序的执行过程会发生变动,所以必须在某个特定的时刻进行枚举,否则的话系统就会有错漏。

垃圾回收的并行于并发

并发(Concurrent)

image-20220314200228005

在一个时间段当中有多个程序处于在运行过程中,并且在同一个处理器上执行。

并行(Parallel)

image-20220314200537888

在同一个时间点上有多个任务同时在执行。

多个核心同时处理处理多个线程。

垃圾回收中的并行与并发

image-20220314200824252

  • 串行的垃圾回收器

    serial、serial Old

  • 并行的垃圾回收器

    ParNew、Parallel Scavenge、Parallel Old

  • 并发执行的垃圾回收器

    image-20220314201741501

    CMS、G1

安全点于安全区域

用户线程必须在安全点才能停下来进行GC。

  • 安全点 safepoint

    代码执行过程中能够安全的等待的点位为安全点。

    安全点的选择很重要,大部分指令执行都很短暂,所以我们要选择较为长时间执行的代码来作为安全点。

    所以具有让程序长时间执行的特征的指令就成为了安全点指令的首选。

    比如方法调用和循环跳转等。

    压入虚拟机栈作为safepoint比较合适。

  • 安全区域

关于Java的引用

有一类对象,当内存足够的时候会将其保存到内存中。在进行GC后内存仍然不够时就将其抛弃。

引用有强>软>弱>虚

引用强度依次减弱

强引用(StrongReference)

Object o = new Object

就是强引用。

所谓强引用指的就是只要关系还存在就不能回收。

软引用(SoftReference)

系统将要OOM之前会把这些对象列入会回收方位中进行二次回收。

1
SoftReference<Object> sf = new SoftReference<obj>;

弱引用(WeakReference)

只要进行垃圾回收就会进行垃圾收集。

1
WeakReference<Object> sf = new WeakReference<obj>;

缓存图片时可以考虑弱引用。

面试题:使用过WeakHashMap

虚引用(PhantomReference)-对象回收跟踪

没有被创建在内存中,也无法通过虚引用获得实例。虚引用像个钩子,希望能够在对象被回收之前收到一个系统通知。

虚引用完全不会对对象的生命周期造成影响。

1
2
3
4
Object object = new Object();
ReferenceQueue rq = new ReferenceQueue();
PhantomReference<Object> pr = new PhantomReference<Object>(object,rq)
Object object = null;

在以上的代码中使用pr.get()也无法获取对象。因为虚引用无法通过get方法获取。

在创建虚引用时必须要提供一个引用队列作为参数。

Java在清理一个只有虚引用的对象时会通知创建虚引用时带入的引用队列。

虚引用唯一的作用就是就是追踪对象的垃圾回收。

当虚引用的对象被垃圾回收后,创建虚引用时使用的引用队列中会获得这个虚引用。

  • 守护线程 t.setDaemon(true);

    当系统中所有非守护线程都停止时守护线程也会停止工作。

    比如垃圾回收线程就是守护线程。

终结器引用(FinalReference)

无需手动编码,其内部配合引用队列去使用。

垃圾回收器

GC分类与性能指标

垃圾回收器没有在JVM规范中明确规定。可以由不同厂商、不同版本的JVM来实现。

如何关注Java不同版本的新特性:

1、语法层面Lambda switch

-> 使用他来定义并执行一个方法

  • 一个参数可以直接 x ->
  • 无参或有多个参数(x,y) ->
  • 一行就结束 可以 x -> x+1;Return 和大括号可以省略
  • 多行需要大括号来确定代码块,并且何以return。

2、API层面:Stream API、新的时间的API、集合框架。

3、底层优化:JVM的优化,GC的优化、元空间、静态域、字符串常量池等。

分类方式
  • 线程数分类

    image-20220425021836441

    • 串行

      单核情况下串行回收器有优势。

      默认被使用在clinet 32位系统中使用。

    • 并行

  • 工作模式分类

    • 独占式
      • Stop the World!
    • 并行式Concurrent
      • 同时执行
  • 碎片处理方式

    • 压缩式

      垃圾回收完毕后会进行压缩整理,消除回收后的碎片

    • 非压缩式

      不进行垃圾回收操作

  • 按内存区间分类。

评估GC的性能指标
  1. 吞吐量(throughput)

    运行用户代码的时间占总运行时间的比例。a/(a+b)

  2. 垃圾收集开销

    b/(a+b)是吞吐量的比例

  3. 暂停时间(pause time)

    STW时间,交互式应用通常更加关注暂停时间。

  4. 收集频率

  5. 内存占用

    堆区所占用内存大小

  6. 快速

    对象生命周期时间。

吞吐量、暂停时间、内存占用构成了不可能三角。

一款优秀的垃圾收集器最多同时满足两项。

不同的垃圾回收器概述

不同JVM的厂商使用自己研发的GC。

垃圾回收器的发展史

Garbage Collector.

  • Serial GC 第一款GC 串行版。

  • ParNew 基于Serial的多线程版本。

  • Parallel GC 2002

  • Concurrent Mark Sweep GC 标记清除算法GC JDK1.4.2发布

  • Parallel GC 再1.6之后成为HoSpot默认GC。

  • 2012年再JDK1.7u4版本中G1成为可用。

  • 2017年JDK9中G1垃圾回收器替代CMS。

    在不同环境下我们通常使用

    ParNew GC 和CMS GC组合应对低延迟需求环境。

    使用Parallel Scavenge GC和Parallel Old GC组合应对高吞吐量组合

    Serial GC和Serial Old GC(MSC) 单线程回收器,主要面对低性能单核服务(x32 Clinet情况下优先考虑回收器)

  • G1 2018年的GDK10中并行完整垃圾回收,实现并行性来改善最坏情况的延迟。意味GDK10开始使用并行垃圾回收器

  • Epsilon 2018年JDK11引入了两个垃圾回收器( 又称之为No-Op 无操作)

  • ZGC同时JDK11中引入的希望之星 可伸缩的低延迟垃圾回收器(目前还是 Experimental 实验性的)

  • 2019年3越,发布JDK12增强G1,自动返回内存给操作系统。

  • Shenandoah GC 同时 由Redhead(Open JDK)引入低停顿时间的GC Shenandoah和ZGC的目标基本上是一致的(Experimental)。

  • JDK13 增强 ZGC,自动返回未用堆内存给操作系统

  • 2020年3越 JDK14发布。删除CMS垃圾回收器 扩展ZGC在Mac OS和Windows上的应用

经典垃圾回收分类:

image-20220429165921093

  • 串行

    Serial、Serial Old

  • 并行

    ParNew、Parallel Secavenge、Parallel Old

  • 并发

    CMS、G1(后期优化为并行回收器)

经典回收器与垃圾分代之前的关系:

image-20220429170243000

垃圾回收器搭配关系:

image-20220429170621570

image-20220429171101129

注意 CMS GC是并发的,在执行回收时用户线程可以继续制造垃圾,如果内存溢出需要采取Serial Old GC作为后备方案。

JDK 1.8中使用的是Parallel 回收器。

查看垃圾回收器
1
2
3
4
5
6
#JVM Option 
-XX:+PrintCommandLineFlags
#也能使用命令
jinfo -flag 相关垃圾回收参数 ID
#C:\Users\dell>jinfo -flag UseParallelOldGC 3464
#-XX:+UseParallelOldGC

Serial(串行)

image-20220504195842887

GDK1.3之前是新生代唯一的选择

后来有了ParNew 并行版本。

是客户端32位系统的默认回收器。

Serial采用串行回收、STW、复制算法的机制来回收

针对老年代会有一个SerialOld来进行老年代的回收

SerialOld在回收老年代时同样是穿行回收、有STW、采用标记压缩算法。

在Server模式下SerialOld主要有两个用途、1与Parallel、ParNew配合使用或者与CMS配合使用作为他的备选方案。

在JDK8和10分别溢出了SerialOld和ParNew和Parallel的配合方案。并且在JDK14中删除了CMS

serial在进行回收时会等待safepoint来进行回收。

优势:

单线程环境下是很高效的。作为Client是不错的选择

参数设置:
1
-XX:+UseSerialGC
使用场景:

不会在Web多用户访问的情况下使用它

ParNew

ParNew是一个新生代的垃圾回收器。

ParNew是Serial的多线程版本有多条垃圾回收线程

ParNew在很多服务器端上是新生代的垃圾回收器

image-20220504235632108

JDK1.9之后不再使用,因为在移除与Serial Old的关系并且JVM去掉CMS回收器之后已经没有可以与ParNew合作的老年代回收框架与之配合了。

改变垃圾回收线程数量的方式
1
2
-XX:ParallelGCThreads
#使用Parallel来改变垃圾回收线程的线程数量
参数设置:
1
2
3
-XX:+UseParNewGC
# 与CMS一起使用的情况下
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

Parallel

Parallel 分为 Parallel Scaveng与Parallel Old GC 分别用于YGC和Old GC,二者会相互激活。

Parallel 使用多线程进行收集,机制上与ParNew相似。性能上也接近。

可控制的吞吐量(Throughput)是Parallel的主要设计目的。吞吐量优先的垃圾回收器

JDK1.6时提供了一个Parallel Old收集器

JDK1.8中默认使用的是Parallel

参数:
1
2
3
4
5
6
7
8
9
10
11
12
#启用
-XX:+UseParallelGC
#启用老年代
-XX:+UseParallelOldGC
#更改垃圾回收线程数
-XX:+ParallelGCThreads
#垃圾会回收的比例取值范围(0,99)默认值位99
-XX:GCTimeRatio=1
#最大停顿时间 毫秒为单位 JVM会调整堆大小以增加回收频率
-XX:MaxGCPauseMillis=100
#自适应策略 默认情况下是开启的
-XX:+UseAdaptiveSizePolicy
优势:

由于Parallel是高吞吐量优先的,所以适合于不需要太多交互的场景。

CMS

CMS是一款低延迟的收集器。所以web类的业务我们更期望使用CMS。

CMS是Hotspot第一款并发收集器

垃圾回收线程可以和用户线程同时工作。

JDK9中被标记为弃用7

JDK14中被移除。使用参数指定CMS不起作用仍然使用G1

image-20220505035224523

在初始标记、重新标记两个环节中是STW环节。

并发标记和并发清理是最耗时的,但是在CMS中使用了并发模式。所以STW时间相对较少得多。

CMS需要设置阈值,在达到阈值时需要进行回收。如果在回收过程中发生内存溢出那么会临时启动Serial Old进行回收

由于CMS采用的是标记清除算法并非标记压缩。所以无法使用内存碰撞来分配内存区域。采用的是空闲列表的形式。

阶段:
  • 初始标记(Inital-Mark)

    仅仅标记出GC Roots能直接关联的对象

    这个过程是非常块的

  • 并发标记(Concurrent-Mark)

    遍历整个对象图的过程

  • 重新标记(Remark)

    用于标记在并发标记过程中导致变化的那部分记录

  • 并发清理(Concurrent-Sweep)

    并发的清除

  • 重置线程

劣势:
  • 可能由于没有足够大的内存区域导致大对象无处分配,触发FULL GC

  • 对CPU资源比较敏感,可能执行垃圾清理导致吞吐量降低。

  • CMS无法处理浮动垃圾Concurrent Mode Failure

    在并发标记过程中用户线程产生的垃圾是不会进行清除的

参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 手动指定使用CMS 默认会和 XX:+UseParNewGC 同时使用
-XX:+UseConcMarkSweepGC

# 什么情况下启动CMS
# 在JDK1.6以前是68% JDK1.6开始时92%
-XX:CMSInitiatingOccupancyFraction

# CMS在执行FullGC后是否对内存空间进行压缩整理
-XX:+UseCMSCompactAtFullCollection
# 执行多少次FullGC对内存进行整理
-XX:CMSFullGCsBeforeCompaction=6
# 设置CMS的垃圾回收线程数量
-XX:ParallelCMSThreads=1

G1 区域化分代式

可控的暂停时间内尽可能提高吞吐量。ZGC也是基于这个逻辑。

G1是一个全功能垃圾回收器。

G1 的设计初衷是为了简化我们在调优GC中的复杂度。我们可以通过设置使用G1收集器、设置最大内存、设置最大停顿时间,三部来实现堆JVM的调优。

G1的垃圾回收模式:

image-20220505045616813

G1在执行垃圾回收时必须包含以下三个环节

  • YoungGC

    多线程独占是STW的垃圾回收过程

    Eden空间耗尽的时候促发young。

    image-20220505062547251

    注意指针指向的区域、代表内存的移动情况。

    在进行YoungGC时通过标记复制算法讲Eden区中的数据复制到Survivor区中。也将年龄限制达标的对象存放到Old区中。

    具体回收过程:

    • 扫描GCRoots 也包括RSet

    • 更新RSet

      • 脏卡表(Dirty Card queue)

        在创建或更新一个引用时我们会在脏卡表中添加一条记录。在之后同步的从脏卡表中跟新到RSet中去。以保证引用更新RSet时的效率问题。

      处理Dirty Card Queue中的Card。

      此阶段完成后RSet才能反映真实的引用

    • 处理RSet

      识别所有被引用的对象,这和第一部分相同重点在于处理RSet。

    • 复制对象

    • 处理引用

      堆不同引用进行处理。包括Soft、Weak、Final、JNI Weak等引用。最找Eden空间的数据被全部清理GC停止工作。

  • 老年代并发标记的过程Concurrent Marking

    单阈值达到45%时就进行并发标记过程

    1. 初始标记

      这与CMS相同,只是标记GCRoots可以直接到达的对象。

    2. 根区域扫描(Root Region Scanning)

      GC扫描Survivor区域直接可达的老年代被引用的对象。这一过程必须在YoungGC之前完成。

    3. 并发标记(Concurrent Marking)

      引用程序还能执行,它与CMS相同。

      如果发现一个Region中全是垃圾会直接回收区域。

      并且会计算每个区域的活性(区域中存活对象比例)。

      回收时会从高活性的区域开始回收。如果无法在指定STW时间内完成回收则会停止回收。

    4. 再次标记(Remark)

      需要修正上一次结果,时STW的。G1采用了比CMS更快的初始快照算法 Snapshot-at-the-beginning(SATB)。

    5. 独占清理(cleanup,STW)

      计算各个区域存活对象和GC回收比例,并进行排序。识别可以混合回收的区域。

      这个阶段并不会进行垃圾回收

    6. 并发清理阶段

      识别并清理完全空闲的区域。

  • Mixed GC

    完成并发标记刚才就进行混合回收过程。

    会讲被清理的老年代放到空闲的Region中。

    混合回收同时也会执行YoungGC。

    image-20220505065554343

    Minxed GC并不会回收所有的老年代而是在指定时间内优先回收高价值的老年代。

    • 实时回收

      并发标记阶段100%是垃圾的区域直接被回收了。

      可以通过-XX:G1MixedGCCountTarget设置

    • 混合回收的会收集(Collection Set)

      包括8分之一的老年代Region,EdenRegion和SurvivorRegion。

      混合回收算法和Young算法相同都是使用复制算法。

    • 占用垃圾比例越高越先被回收。

      混合回收会进行8次-XX:G1MixedGCLiveThresholdPercent=65 来设置Region最低被回收的垃圾比例。

    • 浪费比例

      允许通过-XX:G1HeapWastPercent 来设置允许的内存浪费比例。默认为10% ,意思是允许整个堆中有10%的空间被浪费。如果剩余可回收的垃圾比例低于堆空间的10%则会停止垃圾回收。

  • Full GC(非必须,只是针对GC失败的一种保护机制,是高强度单线程独占式的GC。)

    触发條件:

    • 在并发处理中还没有处理完内存空间就被耗尽了。
    • evacuation的时候没有足够的to-space空间了。(清理垃圾是没有Region可以被用于存放对象了)
G1的内存形式:

image-20220505045823756

image-20220505054547302

特点:
  • 间距并行与并发

    • 并发:G1能够是GC线程与用户线程同时执行。
    • 并行:G1能够多个垃圾回收线程同时执行。
  • 可以建立可预测的停顿时间模型(soft real-time)

用户何以指定在M毫秒内对垃圾整理的时间小于N毫秒

  • G1会维护一个针对Region的空闲列表已记录空闲的Region,但堆Region内的空间分配采用指针碰撞(Pump-the-pointer)

  • Region中可以分配TLAB。

缺点:
  • 性能平衡点

    G1在内存越大的情况下约有优势,与CMS对比性能平衡点在6-8G之间,超过6-8G的内存情况下G1会更有优势。

  • 额外空间占用

    相对其他垃圾回收算法有10%-20%的垃圾占用。

回收过程:
  • G1会有计划的回收各个Region中的垃圾避免全区域进行回收。
  • 首先G1 会判断每个Region的价值判断回收G1能释放多少空间。
  • G1会根据允许的暂停时间优先去回收那个比较大的。
核心概念
  • Region

    内存会被划分为一个一个不相关的Region。

    • Region被选择作为某种角色 比如Eden、Servivor、Old、Humongous。垃圾回收时会将Region通过标记复制算法清理到空Region或其他Region中。

      达到1.5个Region大小的对象会被放进Humongous的Region中。

    • Region之间采用的是复制算法。针对整体堆空间可以看作标记压缩算法。

  • 分代收集

    G1仍然属于分代收集垃圾回收器。只是它不要求Region是连续的。

  • 记忆集(Remembered Set)

    image-20220505061338446

    一个对象有可能被不同区域引用,新生代也可能被老年代引用。扫描全堆时间比较长。

    在G1中每个Region中都有一个记忆集,Region中所有被引用的对象的引用都会被记录在记忆集中。

    在任何引用操作时会进行短暂的write Barrier 并判断其引用是否在同一个Region中如果不是则通过CardTable记录相关的引用。

    进行垃圾回收时CardTable也作为GC Roots。

调优建议
  • 避免设置年轻代大小

    不要使用-Xmn或-XX:NewRatio来设置年轻代大小,固定大小会覆盖G1的最大暂停时间。

  • 暂停时间不要太过严苛

    容易导致FullGC并且会降低吞吐量

    G1的吞吐量目标是90%除非你愿意承受跟高的垃圾回收比例开销。

参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用G1 GC
-XX:+UseG1GC
# 设置Region的大小值是2的幂,范围是1MB到32MB之间 尽量保证2048个Region在java堆中。
-XX:G1HeapRegionSize
# 期望最大停顿时间 默认值是200ms
-XX:MaxGCPauseMillis
# STW工作线程数的数值。最多8
-XX:ParallelGCThread
# 设置并发标记的线程数。通常ParallelGCThread 1/4左右。
# 这个值是指与用户线程共同执行时激活的垃圾回收线程数量
-XX:ConcGCThreads
# 设置触发GC周期大Java堆占用率阈值。默认是45%
-XX:InitiatingHeapOccupancyPercent

总结

image-20220505071419542

GC日志分析

参数:
  • 输出日志

    -XX:+PrintGC

  • 输出详细日志

    -XX:+PrintGCDetails

    参数解析:

    image-20220506214746605

  • 输出GC的时间戳

    -XX:+PrintGCTimeStamps

  • 输出GC时间戳以格式化的时间戳

    -XX:+PrintGCDateStamps

  • 在执行GC的前后打印出堆的信息

    -XX:+PrintHeapAtGC

  • 日志文件的输出路径

    -Xloggc:../logs/gc.log

日志分析工具:

GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等。

垃圾回收器的新发展

性能监控

参考

JVM的解释引擎就是基于操作数栈!!!重点关注操作数栈

面试测试

1虚拟机栈

  • 距离栈溢出 StackOverflowError

    • 通过-Xss设置栈大小
  • 调整栈大小不能保证不出现溢出。

    • 不能保证
  • 垃圾回收是否会涉及到虚拟机栈。

    • 不会,所有栈都不会涉及垃圾回收,城区计数器连ERROR都没有。
  • 方法中定义的局部变量是否是线程安全的。

    • 方法中定义的局部变量,如果是值是线程安全的。

      因为LocalVariableTable是线程安全的。

      但是引用的内容就不一定是线程安全的。

      有可能会发生作用域逃逸。

工具介绍

jvisualvm.exe

在jdk1.8中的bin里。用于查看每个虚拟机的相应属性。

打印垃圾回收具体细节

VM options 中

1
-XX:+PrintGCDetails

查看垃圾回收

1
2
3
4
5
6
7
8
9
10
C:\Users\dell>jps
15060 Launcher
2824
5160 Jps

C:\Users\dell>jstat -gc 2824
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
0.0 3072.0 0.0 3072.0 638976.0 115712.0 947200.0 416013.4 570468.0 540738.6 77572.0 67190.8 257 1.472 0 0.000 30 0.356 1.827

C:\Users\dell>

image-20220207223318184

JEP: JDK增强建议

https://openjdk.java.net/jeps/0