JVM学习记录

JVM的学习视频推荐尚硅谷康师傅的JVM合集
我刷的P1-P203,中间跳过了一些参数和日志分析
主要学习JVM的整体概念
收获颇多,图片上传懒得弄啦
祝各位在校招拿到理想的offer

JVM整体结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7rOArHvP-1626592537326)(JVM.assets/1625039943704.png)]

线程私有:java虚拟机栈,本地方法栈,程序计数器,生命周期和线程一致

线程共享:方法区,堆

java程序编译为字节码文件

JVM架构模型

java编译器输入的指令流

  1. 基于栈的指令集架构
    • 设计实现更简单,适用于资源受限的系统
    • 使用零地址指令方式分配
    • 指令集更小,依赖于操作栈
    • 不需要硬件支持,跨平台
  2. 基于寄存器的指令集架构
    • 性能优秀,执行搞笑
    • 依赖硬件,可移植性差

JVM生命周期

虚拟机的启动

通过引导类加载器创建一个初始类来完成

虚拟机的执行

执行一个java程序即执行一个java虚拟机的进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tO7cYBG5-1626592537328)(JVM.assets/1625042565308.png)]

  1. 正在执行
  2. 执行完成

虚拟机的退出

  • 程序正常执行结束

  • 程序执行过程中晕倒了异常或错误终止

  • 由于操作系统出现错误导致java虚拟机进程终止

  • 某线程调用了runtime类或System类的exit方法,runtime类的halt方法,并且java安全管理区运行这次exit/halt操作

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2hgFDT0-1626592537329)(JVM.assets/1625043119108.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-keDIM66O-1626592537330)(JVM.assets/1625043132456.png)]

执行引擎

执行引擎

  • 解释器
  • 即时编译器JIT
  • 垃圾回收器

类加载器子系统作用

  • 类加载器子系统从文件系统或网络中加载Class文件(物理磁盘上的一个文件),class文件在文件开头又特定的文件表示
  • ClassLoader只负责class问价你的加载,是否可以运行则由ExecutionEngine(执行引擎)决定
  • 加载的类信息存放于一块称为方法区的内存空间,除了类信息外,方法区还会存放运行时的常量池信息,可能包括字符串字面量和数字常量

类的加载过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F01KH4jG-1626592537331)(JVM.assets/1626075857528.png)]

  1. 加载

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IgMddp4Z-1626592537332)(JVM.assets/1626076221607.png)]

  2. 链接

    • 验证
      • 目的时确保Class文件的字节流中包含的信息符合要求
      • 大致有四个阶段:文件格式验证,元数据验证,字节码验证,符号引用验证
    • 准备
      • 为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配,在JDK8后,类变量会随着Class对象一起粗放囊在JAVA堆中
    • 解析
      • 将常量池内符号引用替换为直接引用的过程
      • 符号引用:符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量
      • 直接引用:直接指向目标的指针,相对偏移量或是间接定位到目标的句柄

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXpCO6H4-1626592537333)(JVM.assets/1626076611571.png)]

  3. 初始化

对于初始化阶段,严格规定了以下几种情况必须立即对类进行“初始化”(加载,验证,准备在此之前开始)

  • 遇到new,getstatic,putstatic或invokestatic这四条字节码指令,如果类型没有进行过初始化,则需要先出发其初始化阶段
    • 使用new创建实例化对象
    • 读取或设置一个类型的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)
    • 调用一个类型的静态放啊的时候
  • 使用反射,如果类型没有初始化,则需要先出发初始化
  • 当初始化类的是偶,如果发现其父类还没有进行过初始化,则需要先出发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化整个主类
  • 当一个接口定义了JDK8新加入的默认方法(default关键字修饰的接口方法),如果有这个接口的实现类发生初始化,那该接口要在其之前被初始化
  • 当使用JDK7新加入的动态语言

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1w65FGpL-1626592537334)(JVM.assets/1626077104416.png)]

进行准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序编码指定的主观计划区初始化类变量和其他资源,初始化阶段就是执行类构造器**()**方法的过程

  • **()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源程序出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在之后的变量,在前面的静态语句块可以赋值,但不能访问

  • public class Test{
    	static{
    		i=0;//给变量赋值可以正常编译通过
    		System.out.print(i);//这句编译器会提示“非法向前引用”
    	}
    	static int i=1;
    }
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cuc6cbZY-1626592537334)(C:\Users\32537\AppData\Roaming\Typora\typora-user-images\1626079753797.png)]

先编译后选择视图的show ByteCode可以观察字节码

类加载器分类

类加载器用于实现类的加载

JVM支持两种类型的类加载器

  1. 引导类加载器Bootstrap ClassLoader(使用C++语言实现,是虚拟机自身的一部分)
  2. 自定义类加载器(包括外部导入类和自定义类的加载器,继承于抽象类java.lang.ClassLoader)
    1. 扩展类加载器Extension Class Loader,这个类加载器是在Launcher类下的ExtClassLoader以java代码形式实现的,负责加载\ext目录下的类库
    2. 应用程序类加载器Application Class Loader,这个类加载器是在Launcher类下的AppClassLoader以java代码形式实现的,负责加载用户类路径(ClassPath)上所有类库

他们之间的关系时包含关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zA0l3jGW-1626592537335)(JVM.assets/1626079919076.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwinPrmm-1626592537336)(JVM.assets/1626088104124.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MG79cTni-1626592537336)(JVM.assets/1626088134869.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNqHdm5x-1626592537337)(JVM.assets/1626088149646.png)]

双亲委派机制

Java虚拟机对class文件采用按需加载的方式,也就是当需要使用该类时才会对他的class文件加载到内存生成class对象,加载过程是按双亲委派机制,即把请求交由父类处理

ClassLoader类中的loadClass()方法实现双亲委派,先检查

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

沙箱保护机制

自定义S听类,但是加载自定义String类的时候会首先使用引导类加载器(BootStrap)加载,会先加载jdk的自带文件,报错信息说没有main方法,是因为自带类中的String类内没有main方法,这样保证了java核心源代码的保护

运行数据区

程序计数器

  • PC寄存器是用来存储指向下一条指令的地址
  • 每个线程都由它自己的程序计数器,是线程私有的,生命周期和线程的生命周期保持一致
  • 在任何时间一个线程只有一个方法在执行,也就是所谓的当前方法。程序计数器都会存储当前线程正在执行的Java方法的JVM指令地址
  • 它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器
  • 字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
  1. 使用程序计数器存储字节码指令地址有什么作用
  2. 为什么使用程序计数器记录当前线程的执行地址

因为CPU需要切换各个线程,这时候切换回来以后,就能知道线程执行的位置

JVM的字节码解释器需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令

(一个CPU并发进行线程中的程序)

  • 程序计数器为什么设定为线程私有
    • 为了能够准确记录各个线程正在执行的当前字节码指令地址, 设置每个线程一个程序计数器,可以达到互不干扰的效果

CPU时间片

CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片

虚拟机栈

  • 栈是运行时的单位,堆是存储的单位
  • 生命周期和线程一致
  • 主管Java程序的运行,保存方法的局部变量,部分结果,并参与方法的调用和返回

常见的异常

  1. StackErrorTest:采用固定大小的Java虚拟机栈,每一个线程的虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,会抛出这个异常
  2. OOM:采用动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,会抛出这个异常

配置中可以设置内存空间的大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0yBvd7h-1626592537337)(JVM.assets/1626101540639.png)]

栈的存储单位

  • 栈中的数据都是以栈帧的格式
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系方法执行过程中的各种数据

运行原理:

  • 操作只有两个,压栈和出栈

  • 在一条活动线程中,一个时间点上,只有一个活动的栈帧,这个栈帧被称为当前栈帧,对应的方法就是当前方法,定义这个方法的类就是当前类

  • 执行引擎运行的所有戒子吗指令只针对当前栈帧进行操作

  • 如果该方法调用其他方法,对应新的栈帧会被创建出来,为新的当前栈帧

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ha1JzuQf-1626592537338)(JVM.assets/1626102896577.png)]

栈帧的内部结构

  • 局部变量表
  • 操作数栈
  • 动态链接(指向运行时常量池的方法引用)
  • 方法返回地址
  • 附加信息

局部变量表

  • 也称为局部变量数组,最小单位是变量槽
  • 定义为一个数字数组,主要存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型
  • 局部变量表所需的容量大小在编译期确定下来的
  • 线程私有数据,不存在数据安全问题
  • java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KsXQCw4l-1626592537339)(JVM.assets/1626136555891.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0qcK7rXz-1626592537340)(JVM.assets/1626104056565.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yfQJXotc-1626592537340)(JVM.assets/1626105530531.png)]

如果调用double的变量值,它占有两个slot(4和5)这里调用起始索引4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RavNzxhq-1626592537341)(JVM.assets/1626106336032.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWDScCX4-1626592537341)(JVM.assets/1626106357871.png)]

这里c占用的是b的索引位置,b离开大括号就销毁了

局部变量表方法结构(序号≈索引)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c77Mg5VR-1626592537342)(JVM.assets/1626136432570.png)]

LineNumberTable是Code属性中的一个子属性,用来描述java源文件行号与字节码文件偏移量之间的对应关系。当程序运行抛出异常时,异常堆栈中显示出错的行号就是根据这个对应关系来显示的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PGK7Kbgp-1626592537342)(JVM.assets/1626105070918.png)]

静态变量和局部变量的对比

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0EstXFK-1626592537343)(JVM.assets/1626106436953.png)]

  • 类变量:在Linking的准备阶段,给类变量默认赋值—initial阶段,给类变量显式赋值
  • 局部变量:在使用前,必须进行显式赋值,否则编译不通过

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收(可达性回收方法)

操作数栈

  • 也称表达式栈

  • 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈或出栈

  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间

  • 是JVM执行引擎的一个工作区,当一个工作方法刚开始执行,新的栈帧被创建出来,这个方法的操作数栈式空的

  • 操作数栈并非采用访问索引的方式进行数据访问的

  • 栈中的任何一个元素都可以任意的java数据类型

    • 32bit的类型占用一个栈容量
    • 64bit的类型占用两个栈容量
  • 如果被调用的方法带有返回值,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令

load获取上一个栈帧返回的结果

常见的i++和++i的区别

动态链接

  • 每一个栈帧内部都包含一个指向运行常量池该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
  • 动态链接的作用就是为了将这些符号引用(#指向运行常量池)转换为调用方法的直接引用

方法的引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jA5AjabT-1626592537344)(JVM.assets/1626139937367.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y38VlTWE-1626592537344)(JVM.assets/1626139996480.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1pE1dP7M-1626592537345)(JVM.assets/1626139977976.png)]

调用了methodA的构造方法

方法的调用

  • 静态链接
    • 当一个字节码被装进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接
  • 动态链接
    • 如果被调用的方法在编译期无法确定下来,即只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换具有动态性,因此称为动态链接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-poYlsrYb-1626592537345)(JVM.assets/1626141681425.png)]

虚方法:在编译期间无法确定的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GkfvLi24-1626592537346)(JVM.assets/1626143561150.png)]

方法重写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ah1nNTfe-1626592537346)(JVM.assets/1626144282248.png)]

方法的返回地址

  • 存放调用该方法的程序计数器的值
  • 一个方法的结束有两种方式
    • 正常执行完成
    • 出现未处理的异常,非正常退出
  • 无论哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的程序计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址时要通过异常表来确定,栈帧中一般不会保存这部分信息
  • 正常完成和异常完成出口的区别在于:通过异常完成出口退出的不会给它的上层调用者产生返回值

异常表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OPblAb7k-1626592537347)(JVM.assets/1626147390278.png)]

字节码4行跳转到第8行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sAUtPGGt-1626592537347)(JVM.assets/1626147413513.png)]

按11行的方式进行处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6nc8Vke-1626592537348)(JVM.assets/1626147599461.png)]

Java虚拟机栈面试题

  • 举例栈溢出的情况(StackOverflowError)

  • 通过-Xss设置栈的大小:OOM

  • 调整栈大小,就能保证不出现溢出吗

  • 不能保证

  • 分配的栈内存越大越好吗

    • 不是,会占用其他数据区域的内存空间
  • 垃圾回收是否涉及到虚拟机栈

    • 不存在GC,但是可达性分析会从局部变量表出发
  • 方法中定义的局部变量是否线程安全

    • /**
       * 面试题:
       * 方法中定义的局部变量是否线程安全?具体情况具体分析
       *
       *   何为线程安全?
       *      如果只有一个线程才可以操作此数据,则必是线程安全的。
       *      如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
       * @author shkstart
       * @create 2020 下午 7:48
       */
      public class StringBuilderTest {
      
          int num = 10;
      
          //s1的声明方式是线程安全的,每个线程都有一个S1不会去调用其他线程的S1
          public static void method1(){
              //StringBuilder:线程不安全
              StringBuilder s1 = new StringBuilder();
              s1.append("a");
              s1.append("b");
              //...
          }
          //sBuilder的操作过程:是线程不安全的
          public static void method2(StringBuilder sBuilder){
              sBuilder.append("a");
              sBuilder.append("b");
              //...
          }
          //s1的操作:是线程不安全的,该方法返回类型是StringBuilder可能会被其他方法调用
          public static StringBuilder method3(){
              StringBuilder s1 = new StringBuilder();
              s1.append("a");
              s1.append("b");
              return s1;
          }
          //s1的操作:是线程安全的
          public static String method4(){
              StringBuilder s1 = new StringBuilder();
              s1.append("a");
              s1.append("b");
              return s1.toString();
          }
      
          public static void main(String[] args) {
              StringBuilder s = new StringBuilder();
      
      
              new Thread(() -> {
                  s.append("a");
                  s.append("b");
              }).start();
      
              method2(s);
      
          }
      
      }
      

本地方法栈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cAKMnCjC-1626592537348)(JVM.assets/1626159558984.png)]

当某个线程调用一个本地方法时,它就进入了一个全新地并且不再受虚拟机限制地世界,它和虚拟机拥有同样的权限

  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
  • 它可以直接使用本地处理器中的寄存器
  • 直接从本地内存的堆分配任意数量的内存

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域,所有的对象实例和数组都应当在运行时分配在堆上
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定,是JVM管理的最大一块内存空间
    • 堆内存的大小是可以调节的(-Xms20m -Xmx20m)以此类推
  • 堆可以处于物理上不连续的内存空间,但是逻辑上它被视为连续的
  • 所有线程共享Java堆,在这里还可以划分线程私有的缓冲区

在方法结束后,堆中的对象不会马上被移除,仅在垃圾回收时才会被移除

堆是GC执行垃圾回收的重点区域

内存划分

JAVA8的堆空间逻辑上分为三部分

  • 新生区
  • 老年区
  • 元空间(方法区的实现)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kZuXoaeN-1626592537349)(JVM.assets/1626188103286.png)]

  • -Xms用于表示堆区的起始内存
  • -Xmx用于表示堆区的最大内存(起始内存和最大内存一致可以避免堆自动扩展)
    • 一旦超过-Xmx所指定的最大内存,将会抛出OOM的异常

年轻代&老年代

  • 存储在JVM的java对象分为两类
    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
    • 另外一类对象的生命周期却非常长,甚至可以和JVM的生命周期保持一致
  • 年轻代可以划分为Eden空间,Survivor0空间,Survivor1空间(from区和to区)
    • 分配比列8:1:1

大多数情况下,对象在新生代Eden区中分配,绝大部分的Java对象的销毁都在新生代进行

当Eden区内存不够的时候,就会出发MinorGC,对新生代进行垃圾回收,From区要是满了则会移动到老年代

  • From区和To区:在GC开始的时候,对象只会存在于Eden区和From区,To区是空的,经过一次MinorGc后,Eden区和From区存活的对象会移动到To区,然后情况Eden区和From区,并对存活的对象年龄+1,如果年龄达到15,则分配给老年代。MinorGC完成后,From区和To区的功能互换,下一次MinorGC时,会把To区和Eden区存货的对象放入From区,并计算对象存活的年龄

“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oDU5R675-1626592537349)(JVM.assets/1626234732716.png)]

垃圾回收后,对象都会移动到To区,原本的from区就清空了,成为下一次GC的To区

若创建的对象过大,可能直接分配到老年代

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3uR9CLuB-1626592537350)(JVM.assets/1626242520818.png)]

在Java8中,永久代已经被移除,取而代之的是一个称之为“元数据区”(元空间)的区域。元空间和永久代类似,都是对JVM中规范中方法的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中。这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

采用元空间而不用永久代的原因:

  • 为了解决永久代的OOM问题,元数据和class对象存放在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
  • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

MinorGC,MajorGC与FullGC

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XShD0ts2-1626592537350)(JVM.assets/1626242858875.png)]

  • MinorGC
    • 当年轻代空间不足时,会出发MinorGC,这里的年轻代满指Eden区满,Survivor满不会引发GC
    • 因为Java对象大多都具有朝生夕灭的特性,所有MinorGC非常频繁,回收速度也比较块
    • MinorGC会引发STW,暂停其他用户的线程,等待垃圾回收结束,用户线程才恢复进行
  • 老年代GC(MajorGC/FullGC)
    • 出现MajorGC,经常会伴随至少一次的MinorGC(非绝对)
    • 老年代空间不足时,会先尝试出发MinorGC,如果之后空间还不足,则触发MajorGC
    • 速度比MinorGC慢,STW时间更长
    • MajorGC后内存还不足则会报出OOM的异常

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BKTROwZN-1626592537351)(JVM.assets/1626243504448.png)]

为什么要Java堆分代?

  • 分代原因:优化GC性能,如果没有分代,进行GC需要扫描所有区域

内存分配策略

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 避免Eden区及两个Survivor区之间来回赋值,产生大量的内存复制操作
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等待规定的年龄
  • 空间分配担保
    • –XX:HandlePromotionFailure

TLAB(Thread Local Allocation Buffer)

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

什么是TLAB

对Eden区域进行划分,JVM为每个线程分配一个私有缓存区域,包含在Eden空间内

多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式称为快速分配策略

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ErofnYqQ-1626592537352)(JVM.assets/1626246282554.png)]

  • 堆是不是分配对象的唯一选择

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZqClRNKB-1626592537352)(JVM.assets/1626248890508.png)]

  • 逃逸分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bbY3IqNd-1626592537353)(JVM.assets/1626249117418.png)]

判断对象是否发生逃逸,主要分析对象是否在方法外被调用

代码的优化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yrGtyW4a-1626592537354)(JVM.assets/1626249673191.png)]

方法区

  • 栈,堆,方法区的交互关系[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dr00Udhm-1626592537355)(JVM.assets/1626250938060.png)]
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kTwpnuea-1626592537355)(JVM.assets/1626250959748.png)]

方法区可以看作是一块独立Java堆的内存空间

是各个线程共享的内存区域

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

**方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。**在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XnSJ5qiT-1626592537356)(JVM.assets/1626259091546.png)]

方法区内部结构

存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存

  • 类型信息
    • 对每个加载的类型(类,接口,枚举,注解),JVM必须在方法区中存储以下类型信息
    • 这个类型的完整有效名称
    • 这个类型直接父类的完整有效名(对于interface或Object没有父类)
    • 这个类型的修饰符(public,abstract,final的某个子集)
    • 这个类型直接接口的一个有序列表
  • 域信息
    • JVM必须在方法区中保存类型的所有域的相关信息以及声明顺序
    • 包括:域名称,域类型,域修饰符
  • 方法信息
    • 方法名称
    • 返回类型(或void)
    • 修饰符
    • 字节码,操作数栈,局部变量表及大小
    • 异常表
      • 每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引

类型信息如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QaLEuMuC-1626592537357)(JVM.assets/1626260874346.png)]

域信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-leCaP4GE-1626592537357)(JVM.assets/1626261021917.png)]

方法信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9snV6CHN-1626592537358)(JVM.assets/1626261058152.png)]

异常表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xSdhNcG3-1626592537358)(JVM.assets/1626261193254.png)]

运行时常量池和常量池

  • 方法区,内部包含了运行时常量池(JDK8后在元空间实现)
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z9rezHUQ-1626592537358)(JVM.assets/1626263998318.png)]
  • 字节码文件,内部包含了常量池
    • 各种字面量和对类型,域和方法的符号引用
    • 一个java源文件的类,接口,编译后产生一个字节码文件;而Java中的字节码需要数据支持,这种数据通常很大不会直接存到字节码,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用,动态链接的时候会用到运行时常量池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rLjeoCQx-1626592537359)(JVM.assets/1626265235485.png)]

  • 字符串常量池为什么调整

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KziD0EYP-1626592537359)(JVM.assets/1626266116894.png)]

  • 静态变量的存储

    • 静态变量的引用存储在堆中

    方法区的垃圾手机主要回收两部分:常量池中废弃的常量和不再使用的类型

    方法区常量池主要存放两大类常量:字面量和符号引用

    字面量比较接近Java语言层次的常量概念,如文本字符串,final声明的常量值

    符号引用则属于编译原理方面的概念,包括下面三类常量:

    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符
  • HotSpot虚拟机堆常量池的回收策略:只要常量池中的常量没有被任何地方引用,就可以被回收

  • 回收废弃常量与回收Java堆中的对象相似

判定类回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ba6u5xpj-1626592537359)(JVM.assets/1626267522245.png)]

对象实例化

  • 创建对象的方式
    • new
    • Class的newInstance():反射的方式,无参,public
    • Constructor的newInstance(xxx):反射的方式,有参/无参,权限无要求
    • clone():不调用任何构造器
    • 反序列化
    • Objenesis
  • 创建对象的步骤
    • 判断对象对应的类是否加载,链接,初始化;在元空间的常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析,初始化(判断类元信息是否存在),如果没有,那么在双亲委派模型下,使用当前类加载器查找对应的.class文件,如果没有找到文件,则抛出ClassNotFound的异常,如果找到则进行类加载,并生成对应的Class类对象
    • 为对象分配内存
      • 如果内存规整–指针碰撞
      • 内存不规整–虚拟机需要维护一个列表/空闲列表分配
      • 说明
    • 处理并发安全问题
      • 采用CAS配上失败重试保证更新的原子性
      • 每个线程分配一块TLAB
    • 初始化分配到空间
      • 所有属性设置默认值,保证对象实例字段不赋值也可以被使用
    • 设置对象的对象头
    • 执行方法进行初始化

JVM分配内存的两种方式

当使用new关键字创建一个类的对象时,虚拟机需要为新生对象分配内存空间,而对象的大小在类加载完成后已经确定了,所以分配内存只需要在Java堆中划分出一块大小相等的内存。在Java虚拟机中有指针碰撞和空闲列表两种方式分配内存。

  1. 指针碰撞方式
    如果Java堆中内存是规整排列的,所有被用过的内存放一边,空闲的可用内存放一边,中间放置一个指针作为它们的分界点,在需要为新生对象分配内存的时候,只要将指针向空闲内存那边挪动一段与对象大小相等的距离即可分配。

  2. 空闲列表方式
    如果Java堆中内存不是规整排列的,用过的内存和可用内存是相互交错的,这种情况下将不能使用指针碰撞方式分配内存,Java虚拟机需要维护一个列表用于记录哪些内存是可用的,在为新生对象分配内存的时候,在列表中寻找一块足够大的内存分配,并更新列表上的记录。

  3. Java虚拟机选择策略
    Java虚拟机采用哪种方式为新生对象分配内存,取决于所使用的垃圾收集器,当垃圾收集器具有整理过程时,虚拟机将采用指针碰撞的方式;当垃圾收集器的回收过程没有整理过程时,则采用空闲列表方式。

内存布局
  • 对象头(如果创建的是数组,还需要记录数组的长度)
    • 运行时元数据
      • 哈希值(指向堆空间的地址)
      • GC分代年龄
      • 锁状态标志
      • 线程持有的锁
      • 偏向线程ID
      • 偏向时间戳
    • 类型指针–指向类元数据,确定对象的所属类型
  • 实例数据
    • 它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来和本身拥有的字段)
    • 父类定义的变量会出现在子类之前
    • 相同宽度的字段总是被分配在一起
    • 如果CompactFields参数为true(默认),子类窄变量会插入父类变量的空隙
  • 对齐填充
    • 起到占位符的作用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOy8g85y-1626592537360)(JVM.assets/1626308009364.png)]

联系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5R7aPVS-1626592537360)(JVM.assets/1626312064009.png)]

对象访问定位

JVM是如何通过栈帧中的对象引用访问到内部的对象实例?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6o5dGui4-1626592537360)(JVM.assets/1626312457780.png)]

定位,通过栈帧上的reference(引用)访问

对象访问的两种方式:

  • 句柄访问
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XHh8hM4t-1626592537361)(JVM.assets/1626312619557.png)]
  • 直接指针(HotSpot采用)节省空间
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zNQRGg7J-1626592537361)(JVM.assets/1626312996669.png)]
直接内存
  • 直接内存是在Java堆外的,直接向系统申请的内存空间
    • 访问直接内存的速度会优先于Java堆

执行引擎*(搞不懂)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XYHLDbJd-1626592537362)(JVM.assets/1626328905813.png)]

工作过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-REBZNVf6-1626592537362)(JVM.assets/1626329633750.png)]

Java代码编译和执行的过程
  • 解释器
    • 当Java虚拟机启动时会根据预定义的规范堆字节码采用逐行解释的方式执行,将每条字节码文件的内容“翻译”为对应平台的本地机器指令执行
  • JIT编译器
    • 虚拟机将源代码直接编译成和本地机器平台相关的机器语言

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kuJnRzJw-1626592537362)(JVM.assets/1626330884344.png)]

  • 字节码
    • 是一种中间状态(中间码)的二进制代码,需要转译才能称为机器码
    • 为了实现特定软件运行和软件环境,与硬件无关
    • 编译器将源码编译成字节码

StringTable

字符串常量池不会存储相同内容的字符串,字符串常量池位于堆

String
  • 声明为final,不可被继承
  • 实现了Serializable接口:表示字符串支持序列化的
  • 实现Comparable接口:表示String可以比较大小
  • 当对字符串重写赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
  • 当对现有的字符串进行连接操作,也需要重新指定内存区域赋值
  • 当调用replace()方法时同样
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时字符串值声明在字符串常量池中

jdk8底层存放char类型数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ao1q1bmQ-1626592537363)(JVM.assets/1626344274777.png)]

字符串拼接
  1. 常量与常量的拼接结果在常量池,原理时编译器优化
  2. 常量池中不会存在相同的内容的常量
  3. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结果是调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回对象地址
@Test
    public void test1(){
        String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
        String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
        /*
         * 最终.java编译成.class,再执行.class
         * String s1 = "abc";
         * String s2 = "abc"
         */
        System.out.println(s1 == s2); //true
        System.out.println(s1.equals(s2)); //true
    }

    @Test
    public void test2(){
        String s1 = "javaEE";
        String s2 = "hadoop";

        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";//编译期优化
        //如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        System.out.println(s3 == s4);//true
        System.out.println(s3 == s5);//false
        System.out.println(s3 == s6);//false
        System.out.println(s3 == s7);//false
        System.out.println(s5 == s6);//false
        System.out.println(s5 == s7);//false
        System.out.println(s6 == s7);//false
        //intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
        //如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回次对象的地址。
        String s8 = s6.intern();
        System.out.println(s3 == s8);//true
    }
1. 字符串拼接操作不一定使用的是StringBuilder!   如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。
2. 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。
    /*
    体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
    详情:① StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象
          使用String的字符串拼接方式:创建过多个StringBuilder和String的对象
         ② 使用String的字符串拼接方式:内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;如果进行GC,需要花费额外的时间。

     改进的空间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
               StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
     */
intern()方法

双引号声明“”即字面量形式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCABUOdp-1626592537363)(JVM.assets/1626362873248.png)]

如何保证变量s指向字符串常量池的数据?

  • 方法一:String s=“abc” 字面量的方式
  • 方法二:String s=new String(“”).intern();
new String()创建对象
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9RlqJUBM-1626592537364)(JVM.assets/1626399052873.png)]

new String("ab")会创建几个对象?看字节码,就知道是两个。
 *     一个对象是:new关键字在堆空间创建的
 *     另一个对象是:字符串常量池中的对象"ab"。 字节码指令:ldc
public class StringNewTest {
    public static void main(String[] args) {
//        String str = new String("ab");

        String str1 = new String("a") + new String("b");
    }
}
思考:
 * new String("a") + new String("b")呢?
 *  对象1new StringBuilder()
 *  对象2new String("a")
 *  对象3: 常量池中的"a"
 *  对象4new String("b")
 *  对象5: 常量池中的"b"
 *
 *  深入剖析: StringBuildertoString():
 *      对象6new String("ab")
 *       强调一下,toString()的调用,在字符串常量池中,没有生成"ab"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-35MYvyxB-1626592537364)(JVM.assets/1626399526819.png)]

public class StringNewTest {
    public static void main(String[] args) {
        String str="ab";
    }
}
//字面量创建仅生成一个对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9T4bhOu8-1626592537364)(JVM.assets/1626399977283.png)]

关于intern()

/**
 * 如何保证变量s指向的是字符串常量池中的数据呢?
 * 有两种方式:
 * 方式一: String s = "shkstart";//字面量定义的方式
 * 方式二: 调用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  18:49
 */
public class StringIntern {
    public static void main(String[] args) {

        String s = new String("1");
        s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false   jdk7/8:false


        String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
                                            //         jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
        String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

}

String的拼接原理是使用StringBuilder,常量池中不存在拼接后的字符串

public class StringExer1 {
    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代码执行完以后,字符串常量池中并没有"ab"

        String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
                               //jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回

        System.out.println(s2 == "ab");//  jdk8:true      x:true
        System.out.println(s == "ab");//  jdk8:true       x:false

    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0gSIzM4i-1626592537365)(JVM.assets/1626403172868.png)]

垃圾回收

什么是垃圾

  • 垃圾是指运行程序中没有任何指针指向的对象

标记阶段:

  1. 引用计数算法
    • 对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的清空
    • 对于一个对象A,任何一个对象引用了A,则计数器加1;引用失效则减1
    • 优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性
    • 缺点:
      • 增加了存储空间的凯西奥
      • 每次赋值需要更新计数器,增加时间开销
      • 无法解决循环引用的情况
  2. 可达性分析算法
    • 以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
    • 搜索过的路径称为引用链
    • 如果目标对象没有任何引用链相连,则是不可达,即垃圾对象
    • 只有能够被根对象集合直接/间接连接的对象才是存活对象
  3. GC Roots对象
    • 虚拟机栈(栈帧中国的局部变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量
    • 方法区中类静态属性引用的对象,比如Java类的引用类型静态变量
    • 方法区中常量引用的对象,比如字符串常量池里的引用
    • 本地方法栈中JNI(Native方法)引用的对象
    • Java虚拟机内部的引用,基本类型对象的Class对象,异常对象等等
    • 所有被同步锁(synchronized关键字)持有的对象
  4. finalization机制
    • 对象被销毁之前的自定义处理逻辑
垃圾收集算法
  1. 标记-清除算法
    • 执行过程:当堆中的有效内存空间被耗尽,就会STW然后进行两项工作,一是标记,二是清除
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9sdj04t-1626592537365)(JVM.assets/1626424540238.png)]
    • 缺点:
      • 执行效率不稳定,如果堆中包含大量对象且需要被回收,那么需要进行大量的标记和清除动作,导致标记和清除两个过程的效率随着对象数量增长而降低
      • 内存空间碎片化问题,产生大量不连续的内存碎片
  2. 标记-复制算法
    • 将内存空间分为两块,每次只是用其中一块,垃圾回收时将正在使用的内存中的存活对象复制到另外一块上面,再把使用过的内存空间清理掉
    • 应用场景:
      • 在新生代,对常规应用的垃圾回收,可以回收大部分的内存空间,存活对象较少,适合使用复制算法
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4oTSadJ-1626592537366)(JVM.assets/1626425518720.png)]
    • 优点:
      • 没有标记和清除过程,实现简单高效
      • 复制保证空间的连续性,不会出现”碎片“问题
    • 缺点
      • 如果内存中存活对象较多,会产生大量的内存复制开销
      • 需要两倍的内存空间
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aVnfK6mg-1626592537366)(JVM.assets/1626424876212.png)]
  3. 标记-整理算法
    • 分为两个阶段,第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象;第二阶段将所有存活对象压缩到内存的一端,按顺序排放;之后清理边界外的所有空间
    • 它与标记-清除算法的差异在于它是移动式的,而清除算法则是一种非移动式的回收算法,标记存活的对象将会被整理,按照内存地址依次排列,当我们需要给新对象分配内存时,只需要持有一个内存的起始地址即可
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n9HMOHeH-1626592537366)(JVM.assets/1626431614461.png)]
    • 优点
      • 解决了清除算法的内存区域分散的缺点
      • 解决了复制算法的内存减半的代价
    • 缺点
      • 效率低于复制算法
      • 移动对象的同时,如果对象被其他对象引用,还需要调整引用的地址
      • 移动过程需要STW
垃圾收集算法小结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NRrPdCCc-1626592537367)(JVM.assets/1626432446988.png)]

分代收集算法

不同生命周期的对象可以采取不同的收集方式,以便提高回收效率

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ev99XRji-1626592537367)(JVM.assets/1626434599699.png)]

增量收集算法

垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成

其基础仍是标记-清除和复制算法,增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或复制工作

缺点:

  • 减少了系统的停顿时间,但是因为线程切换和上下文转换消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5AUXBVzQ-1626592537367)(JVM.assets/1626437632117.png)]

System.gc()
  • 通过System.gc()或者Runtime().getRuntime().gc()的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
  • System.gc()调用附带一个免责声明,无法保证对垃圾回收器的调用
package com.atguigu.java;

/**
 * @author shkstart  shkstart@126.com
 * @create 2020  14:49
 */
public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc();//提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc
        //与Runtime.getRuntime().gc();的作用一样。

        System.runFinalization();//强制调用使用引用的对象的finalize()方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重写了finalize()");
    }
}

内存溢出(OOM)

没有空闲内存,并且垃圾收集器无法提供更多内存,即Java虚拟机堆的内存不够

  • Java虚拟机的堆内存设置不够,通过设置参数-Xms,-Xmx来调整
  • 代码中创建了大量大对象,并且长时间不能够被垃圾收集器收集
    • 随着元数据区的引入,方法区内存不足的情况改善了许多

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6y4cRd2-1626592537368)(JVM.assets/1626442232785.png)]

内存泄漏(Memory Leak)

只有对象不再被程序用到,但是GC又不能回收它们的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QTFxW8Ju-1626592537368)(JVM.assets/1626450782291.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XgrMP8EM-1626592537369)(JVM.assets/1626451200793.png)]

STW

GC事件发生过程中,会产生程序的停顿,停顿产生时整个应用程序线程都会被暂停,没有任何响应

  • 可达性分析算法枚举根节点(GC Roots)会导致所有Java执行线程停顿
    • 分析工作必须在一个能确保一致性的快照进行
    • 一致性指整个分析期间整个执行系统看起来像被冻结
    • 如果出现分析过程中对象引用关系不断变化,则分析结果的准确性无法保证

并行与并发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jr9ZJnyz-1626592537369)(JVM.assets/1626481062467.png)]

垃圾回收的并行&并发

  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
    • ParNew,Parallel Scavenge等
  • 串行:
    • 单线程执行
    • 如果内存不够,程序暂停,启动垃圾回收,回收完再启动程序的线程
  • 并发:指用户线程与垃圾收集线程同时执行,但不一定是并行的,可能时交替执行,垃圾回收线程再执行时不会停顿用户程序的运行
    • 用户程序再继续运行,而垃圾收集线程运行在另一个CPU上
    • 如CMS,G1
安全点和安全区域

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BAiHemni-1626592537370)(JVM.assets/1626481648276.png)]

引用

强引用(大部分场景)
  • 最传统的引用定义,指程序代码中普遍你存在的引用赋值,类似“Object obj=new Object()”这种引用关系,不论何种情况下,只要强引用关系还在,垃圾收集器就不会回收掉被引用的对象
  • 默认的引用类型
  • 强引用的对象是可触及的,垃圾收集器不会回收被引用的对象
  • 强音用是造成Java内存泄漏的主要原因之一
软引用(缓存场景,内存不足即回收)
  • 在系统要发生溢出之前,将会把这些对象列入回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
  • 软应用是用于描述一些还有用但非必须的对象,只要被软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收
  • 通常用于实现内存敏感的缓存,当垃圾回收器在某个时刻决定回收软可达的对象时,会清理软引用,并可选地把引用存放到一个引用队列
弱引用(缓存场景,发现即回收)
  • 被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
  • 被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 由于垃圾回收器的线程通常优先级较低,并不一定能很快发现持有弱引用的对象,在这种情况下,弱引用对象可以存活较长时间
  • 软引用,弱引用适合保存那些可有可无的数据
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-THb9Q7dm-1626592537370)(JVM.assets/1626487928442.png)]
虚引用
  • 一个对象是否有虚引用的存在,完全不会对其生存事件构成影响,也无法通过虚引用来获得一个对象的实例,为一个对象设置虚引用关联的唯一目的就是在这个对象被收集器回收时收到一个系统通知
终结器引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYWniIA3-1626592537371)(JVM.assets/1626489166467.png)]

垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iMViTPlh-1626592537371)(JVM.assets/1626490032838.png)]

串行回收指的是同一个时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾回收结束

并行回收可以运用多个CPU同时执行垃圾回收,提升了应用的吞吐量,不过并行回收和串行回收一样,采用独占式,即STW机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JyquakKL-1626592537372)(JVM.assets/1626490326195.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSXEnhzq-1626592537372)(JVM.assets/1626490592071.png)]

评估GC的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例
    • 总运行时间:程序运行时间+内存回收的时间
  • 垃圾收集开销:吞吐量的补数,垃圾收集时间与总运行时间的比例
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
  • 收集频率:相对于应用程序的执行,收集操作发生的频率
  • 内存占用:Java堆区所占的内存大小
  • 快速:一个对象从诞生到被回收所经历的时间

在最大吞吐量的情况下,降低暂停时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ioIJknYb-1626592537373)(JVM.assets/1626493891850.png)]

经典收集器与垃圾分代之间的关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5sXRmMHG-1626592537374)(JVM.assets/1626502189286.png)]

SerialGC

  • Serial收集器时最基本,历史最悠久的垃圾收集器
  • HotSpot中Client模式中默认新生代垃圾收集器
  • Serial收集器采用复制算法,串行回收和STW机制的方式进行内存回收
  • Serial Old收集器同样也采样了串行回收和STW机制,内存回收算法使用的标记-压缩(整理)算法
    • Serial Old收集器是Client模式下默认的老年代的垃圾回收器
    • Serial Old收集器的用途:
      • 与新生代的Parallel Scavenge配合使用
      • 作用老年代CMS收集器的后备垃圾收集方案
  • 只会使用一个CPU或一条收集线程去完成垃圾收集,必须暂停其他工作的线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f4R4Uquh-1626592537374)(JVM.assets/1626503950105.png)]

ParNew回收器(并行回收)

  • ParNew收集器可以理解为Serial收集器的多线程版本
  • 采用并行回收的方式执行内存回收以外,两块垃圾收集器几乎没有任何区别,对年轻代的回收同样采用复制算法,STW机制
  • ParNew是Server模式下的新生代默认垃圾收集器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T48Nxfsy-1626592537375)(JVM.assets/1626504338940.png)]

  • 对于新生代,回收频繁,使用并行高效
  • 对于老年代,回收次数少,使用串行方式节省资源

Parallel Scavenge收集器(吞吐量优先收集器)

  • 目标是达到一个可控制的吞吐量
  • 自适应调节策略也是Parallel Scavenge和ParNew的重要区别
  • Java8默认此垃圾收集器
  • 高吞吐量可以高效的利用CPU时间,尽快地完成程序地运算任务,主要适合在后台运算不需要太多交互的任务

Parallel Old收集器

Parallel Old可以看成Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现

采用了标记-压缩算法,同样是基于并行回收和STW机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u9y16sXj-1626592537375)(JVM.assets/1626506052501.png)]

设置参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GtlWHDmF-1626592537376)(JVM.assets/1626506467346.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOtEtyZj-1626592537377)(JVM.assets/1626506501190.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bZGQBT8s-1626592537377)(JVM.assets/1626506620700.png)]

CMS回收器(低延迟)

  • CMS收集器是一种以获得最短回收停顿时间为目标的收集器,实现了垃圾收集线程和用户线程同时工作,停顿时间越短越适合与用户交互的程序

  • CMS的垃圾收集算法采用标记-清除算法,并且也会造成STW

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iY9HTt7u-1626592537377)(JVM.assets/1626506954335.png)]

CMS的运作过程可以分为4个主要阶段

  1. 初始标记
    • 程序中所有的线程会因为STW机制出现暂停,这个阶段主要任务仅仅是标记出GC Roots能直接关联到的对象,一旦标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快
  2. 并发标记
    • 从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  3. 重新标记
    • 由于并发标记过程中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分的对象的标记记录,这个阶段停顿时间比初始标记阶段稍长,但是比并发标记阶段时间短
  4. 并发清除
    • 此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
  • 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体回收时低停顿的
  • CMS回收过程中,应确保用户线程有足够的内存可用,它和其他收集器不一样等到老年代填满再进行收集,而是当堆内存使用率达到某一阈值时,便开始回收;要是CMS回收期间预留的内存无法满足程序需要,便会出现Concurrent Mode Failure
  • CMS收集器的垃圾收集算法采用标记-清除算法,这意味每次执行完内存存放回收后,由于被执行内存回收的无用对象所占的内存空间可能是不连续的一些内存块,会产生一些内存碎片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J4peAHPf-1626592537378)(JVM.assets/1626510131109.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-addmeIDJ-1626592537378)(JVM.assets/1626510255619.png)]

  • 最小化使用内存和并行开销:Serial GC
  • 最大化应用程序的吞吐量:Parallel GC
  • 最小化GC的中断/停顿时间:CMS GC

G1回收器(区域化分代式)

G1是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,兼顾停顿那时间和高吞吐量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDM9hnfc-1626592537379)(JVM.assets/1626528738124.png)]

G1的特点优势

  • 并行与并发

    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程STW
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  • 分代收集

    • 从分代来看:G1依然属于分代型垃圾回收器,他会区分新生代和老年代,新生代依然有Eden区和Survivor区;从堆结构上看,它不要求整个Eden区,新生代或者老年代都是连续的,也不再坚持固定大小和固定数量
    • 将堆空间分为若干个区域,这些区域中包含了逻辑上的新生代和老年代
    • 和之前的回收器不同,它同时兼顾新生代和老年代
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suaOVDrA-1626592537379)(JVM.assets/1626534379688.png)]
  • 空间整合

    • CMS:“标记-清除”算法,内存碎片,若干次GC后进行一次碎片整理
    • G1将内存划分为一个个region,内存回收是以region作为基本单位,Region之间是复制算法,但整体上可看作是标记-整理算法,两种算法都可以避免内存碎片
  • 可预测的停顿时间模型(软实时)

    • 相对于CMS的另一优势,除了低停顿外还可以简历可预测的停顿时间模型,能汤使用者指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒
    • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿的情况得到较好的控制
    • 每次根据允许的收集时间,优先回收价值最大的Region,获取尽可能高的收集效率

G1的缺点

  • 相较于CMS,G1在垃圾收集的内存占用和程序运行时的额外执行负载都要比CMS高
  • 小内存应用CMS表现大概率优于G1,G1在大内存应用上发挥优势
G1需要去解决的问题
  1. 跨代引用
    • 我们使用记忆集避免全堆进行GC Roots扫描,我们需要在新生代建立一个全局的数据结构(即记忆集),这个结构把老年代划分为若干小块,表示出老年代的哪一块内存会存在跨代引用,此后发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描

分区Region

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QIPmg51C-1626592537380)(JVM.assets/1626575723728.png)]

G1垃圾收集器还增加一种内存区域,Humongous内存区域,主要存储大对象,如果一个H区存放不下,会存放在连续的H区来存储

G1收集器的运作过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0174ikl-1626592537380)(JVM.assets/1626579582063.png)]

  1. 初始标记

    • 标记GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用地Region中分配新对象。这个阶段需要暂停线程,耗时较短,而且借用进行MinorGC的时候同步完成,所以G1收集器在这个阶段没有额外的停顿
  2. 并发标记

    • 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但可与用户程序并发执行,当对象图扫描完成以后,还要重写处理SATB记录下的在并发有引用变动的对象,如发现区域中的所有对象都是垃圾,那这个区域会被立即回收
  3. 最终标记

    • 对用户线程做另一个暂停,用于处理并发阶段结束后仍遗留下来少量的SATB记录
  4. 筛选回收

    • 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来指定回收计划,可以自由任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象赋值到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及到存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHIzPc73-1626592537381)(JVM.assets/1626590454309.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ahoY3M2-1626592537381)(JVM.assets/1626590470269.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zkMw3rN-1626592537381)(JVM.assets/1626590357316.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m8uCQc4o-1626592537382)(JVM.assets/1626590485185.png)]

小结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZfocPOlE-1626592537382)(JVM.assets/1626591087481.png)]

的时间不超过N毫秒

  • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿的情况得到较好的控制
  • 每次根据允许的收集时间,优先回收价值最大的Region,获取尽可能高的收集效率

G1的缺点

  • 相较于CMS,G1在垃圾收集的内存占用和程序运行时的额外执行负载都要比CMS高
  • 小内存应用CMS表现大概率优于G1,G1在大内存应用上发挥优势
G1需要去解决的问题
  1. 跨代引用
    • 我们使用记忆集避免全堆进行GC Roots扫描,我们需要在新生代建立一个全局的数据结构(即记忆集),这个结构把老年代划分为若干小块,表示出老年代的哪一块内存会存在跨代引用,此后发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描

分区Region

[外链图片转存中…(img-QIPmg51C-1626592537380)]

G1垃圾收集器还增加一种内存区域,Humongous内存区域,主要存储大对象,如果一个H区存放不下,会存放在连续的H区来存储

G1收集器的运作过程

[外链图片转存中…(img-O0174ikl-1626592537380)]

  1. 初始标记

    • 标记GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用地Region中分配新对象。这个阶段需要暂停线程,耗时较短,而且借用进行MinorGC的时候同步完成,所以G1收集器在这个阶段没有额外的停顿
  2. 并发标记

    • 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但可与用户程序并发执行,当对象图扫描完成以后,还要重写处理SATB记录下的在并发有引用变动的对象,如发现区域中的所有对象都是垃圾,那这个区域会被立即回收
  3. 最终标记

    • 对用户线程做另一个暂停,用于处理并发阶段结束后仍遗留下来少量的SATB记录
  4. 筛选回收

    • 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来指定回收计划,可以自由任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象赋值到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及到存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成

    [外链图片转存中…(img-jHIzPc73-1626592537381)]

    [外链图片转存中…(img-9ahoY3M2-1626592537381)]

[外链图片转存中…(img-6zkMw3rN-1626592537381)]

[外链图片转存中…(img-m8uCQc4o-1626592537382)]

小结

[外链图片转存中…(img-ZfocPOlE-1626592537382)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值