JVM学习————运行时数据区(三)

运行时数据区

运行时数据区概述及线程

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

在JVM中表示两个class对象是否为同一个类存在两个必要条件

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

JVM必须直到一个类型是由启动类加载器加载还是由用户类加载器加载。如果由用户类加载器加载,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载是相同的

类的主动使用和被动使用

  • 主动使用
    1. 创建类的实例
    2. 访问某个类的接口或者静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射
    5. 初始化一个类的子类
    6. Java虚拟机启动时被标明为启动类的类
    7. JKD7开始提供动态语言的支持 java.lang.invoke.MethodHandler实例的解析结果
  • 除以上7种,其他使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化

不同的JVM对于内存的划分方式和管理机制存在着部分差异

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

进程是随着虚拟机的启动而创建,退出而销毁

每个线性独立享有:虚拟机栈、本地方法栈、程序计数器

多个线程共享:堆,堆外内存(永久代或元空间、代码缓存)

每个JVM只有一个Runtime实例

线程是一个程序里的运行单元,JVM中的多个线程可以并发执行,在Hotspot JVM中的线程与操作系统中的本地线程有之间直接映射的关系

  • 当Java中的线程准备好执行以后,此时一个操作系统的本地线程也同时创建,Java线程执行终止后,本地线程也会收回
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,他就会调用Java线程中的run()

后台线程中不包含main线程以及其自己创建的线程

在Hotspot JVM中的后台线程主要包括

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

程序计数器

JVM中的程序技计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象

作用

PC寄存器用来存储指向下一条指令的地址,也即要执行的指令代码。由执行引擎读取吓一条指令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OJDqMkzv-1618969645087)(JVM.assets/image-20210403140858142.png)]

介绍

  • 它是一块很小的内存,几乎可以忽略不记。也是运行速度最快的存储区域
  • 在JVM规范中,每个线程都有他自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前正在执行线程的Java指令地址,或者如果是在执行native方法,则是未指定(undefined)
  • 它时程序控制流的指示器,分直、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
  • 它是唯一一个在Java虚拟机中没有规定任何OutOtMemoryError情况的区域

OOM(OutOtMemory):堆、方法区、本地方法栈、虚拟机栈

GC:堆、方法区

在这里插入图片描述

常见问题

使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址?

  • 因为CPU需要不停的切换各个线程,PC寄存器可以记录下一条指令的地址,当线程切换运行其他线程后,再次运行此线程时,就知道该从哪里开始执行
  • 因为JVM字节码解释器就需要PC寄存器的值来明确下一条该执行什么样的字节码指令

PC寄存器为什么会被设定为线程私有?

  • 因为CPU会在多个线程中不停的切换,如何保证每次切换回来后,还能继续执行之前的指令,此时就需要PC寄存器,它可以记录上一次执行线程中的指令,可以准确的让CPU继续执行,而不会造成由于多线程切换而造成的相互影响

CPU时间片

  • CPU时间片即CPU分配给各个程序的时间,每个程序被分配一个时间段,称为时间片
  • 在宏观上:可以同时打开多个程序,程序同时运行
  • 但是在微观上,由于CPU只有一个,一次只能处理一个程序的要求的一部分,如何处理公平,此时就引入了时间片,每个程序轮流执行

虚拟机栈

优点:跨平台,指令集小,编译器容易实现;缺点:性能下降,实现同样的功能需要更多的指令

运行时的单位,而存储的单位

虚拟机栈
Java虚拟机栈是什么?
  • 每个线程在创建时都会创建一个Java虚拟机栈,其内部是一个个栈帧,对应着一个个方法
  • 线程私有
  • 生命周期和线程一致
  • 主管Java程序的运行,它保存方法的局部变量、部分变量,并参与方法的调用和返回

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

栈的优点
  • 栈是一种快速有效的分配方式,访问速度仅次于程序计数器
  • JVM直接对Java栈的操作只有两个
    1. 每个方法的执行,伴随着入栈和出栈
    2. 执行结束后的出找工作
  • 对于栈来说不存在垃圾回收

面试题:开发中遇到的异常有哪些?

Java虚拟机允许Java栈的大小存在固定不变或者是动态的

  • 如果采用固定不变,那么每一个线程在创建的时候都可以各自分配独立大小Java虚拟机栈的容量,当线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机会抛出一个StackOverflowError异常
  • 如果Java虚拟机的栈可以动态扩展,并且在扩展时无法申请到足够的内存,或者在创建新线程时没有足够的内存区创建对应的虚拟机栈,那么Java虚拟经济会抛出一个OutOfMemoryError异常

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

设置栈内存大小

通过-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

栈中存储什么?
  • 每个线程都有自己的栈,每个栈中都是以栈帧的格式存在
  • 在这个线程上每个方法都对应着一个栈帧
  • 栈帧是一个内存区,是一个数据集,维系着方法执行过程中的各种数据信息
栈运行原理
  • JVM直接堆Java栈的操作只有出栈入栈,并且遵循“先进后出”的原则
  • 在一条活动的线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶的栈帧)是有效的,这个栈帧被称为当前正在,此方法被称为当前方法,此类被称为当前类
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新栈会被创建出来,放在栈顶,称为新的当前栈帧
  • 不同线程中的栈帧不允许相互引用,即不存在一个栈帧中引用另一个线程的栈帧
  • 如果当前方法调用了其他方法,方法返回之际,会将自己的返回结果给前一个栈帧,接着,丢弃当前栈帧,使前一个栈帧称为当前栈帧
  • Java方法有两种返回函数的方式:一种使正常的函数返回,使用return指令;另一种使抛出异常,不管哪种方式,都会导致栈帧弹出
栈帧的内部结构

每个栈帧存储着:

  1. 局部变量表(Local Variable)
  2. 操作数栈(Operand Stack)(或表达式栈)
  3. 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
  4. 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
  5. 一些附加信息

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

局部变量表
  • 局部变量表也称为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数以及定义在方法体内的局部变量,这些数据类型包括各种基本数据类型、对象引用(reference),以及returnAddress类型
  • 由于局部变量表是建立在栈中,每个栈为线程私有,因此不存在安全问题
  • 局部变量表的容量大小是在编译时期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变大小的

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

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

关于slot理解
  • 参数值的存放总是在局部变量表数组的index0开始,到数组长度-1的索引结束
  • 局部变量表,最基本的存储单元是slot
  • 局部变量表中存放编译期间可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型
  • 在局部变量表里,32位以内的类型只占一个slot(包括returnAddress类型,对象),64位的类型(long和double)占用两个slot
    • byte、char、short在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true
    • long和double则占据两个

JVM会为局部变量表中的每一个slot分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量

当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot中

如果要访问64位的局部变量时,只需要访问第一个索引即可

如果当前正在是由构造函数或者实例方法创建的,那么该对象引用this将会存放在index=0的slot处,其余参数的索引依次排列

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

slot重复利用

栈帧中局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其后声明的变量很有可能会利用过期局部变量的槽位,从而达到节省资源的目的

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

局部变量表
  • 方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用次数越多
  • 局部变量表中的变量只在当前方法调用中有效。当方法结束,此对应的栈帧就会被销毁,局部变量表也会随之销毁

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

  • 参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配
  • 类变量表有两次初始化的机会,第一次是在“准备阶段”对类变量进行默认初始化,第二次是在“初始化阶段”对类变量进行代码中初值的赋予
  • 和类变量不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量就必须人为的显示初始化

在执行方法时,虚拟机栈使用局部变量表完成方法的传递

局部变量表的变量也是重要的垃圾回收的根节点,只要被局部变量表直接或者间接引用的对象都不会被回收

操作数栈

每一个独立的栈帧中处理包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈

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

  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们以后再把结果压入栈
  • 比如:执行复制、交换、求和等操作

如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令

操作数栈中的字节码指令的类型必须要与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载器加载过程中的类检验阶段的数据流分析阶段要再次验证

另外我们说的Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

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

操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之创建出来,这个方法的操作数栈是空的

每一个操作数栈都有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值

栈的任何一个元素都可以是任意的Java数据类型

  • 32bit的类型占用一个栈的深度
  • 64bit的类型占用两个栈的深度

操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据的访问

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

栈顶缓存技术

程序员面试过程中,常见的i++和++i区别

将栈顶元素全部缓存在物理CPU寄存器中,以此降低对内存的读/写,提升执行引擎的执行效率

动态链接

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

帧数据区

  1. 动态链接
  2. 方法返回值
  3. 一些附加信息
  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如:invokedynamic指令
  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符合引用报保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符合引用来表示的,那么动态链接的作用就是为了将这些符合引用转换为调用方法的直接引用

常量池的作用:就是为了通过一些符合和常量,便于指令识别

(方法的调用【额外】)

在JVM中将符号引用转为调用方法的直接引用与方法的绑定机制相关

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

对应的方法的绑定机制为:早期绑定和晚期绑定。绑定是一个字段,方法或者类在符合引用被替换为直接引用的过程,这仅仅发生一次

  • 早期绑定
    • 目标方法如果在编译期间可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被确定目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
  • 晚期绑定
    • 如果被调用的方法无法在编译期间确定,只能在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就是称为晚期绑定

非虚方法与虚方法

非虚方法

  • 如果方法在编译期间就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法
  • 静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法,(判断非虚方法,其一不能实现重写,因为重写可以作为多态的标志,多态主要是在运行时期确定其调用的方法)
  • 其他方法称为虚方法

虚拟机中提供了以下几条方法的指令:

  • 普通调用指令

    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
    2. invokespecial:调用方法,私有的及(super显示调用)父类的方法,解析阶段确定唯一方法版本
    3. invokevirtual:调用所有虚方法
    4. invokeinterface:调用接口方法
  • 动态调用指令

    1. invokedynamic:动态解析需要调用的方法,然后执行

      • 直到Java8中的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成
      interface Func {
          boolean func(String s);
      }
      public class Lambda {
          public void lambda(Func func) {
              return;
          }
      
          public static void main(String[] args) {
              Lambda lambda = new Lambda();
              // invokedynamic
              Func func = s -> {
                return true;
              };
              lambda.lambda(func);
      
              lambda.lambda(s -> {
                  return false;
              });
          }
      }
      

前四条指令固化在虚拟机内部,不可认为干涉,而invokedynamic指令则支持由用户确定方法版本。其中,invokestatic指令和invokespecial指令调用调用的方法称为非虚方法,其余的(除final修饰的)称为虚方法【final无法被子类重写,虽然是invokevirtual,但其实是非虚方法,当显示通过super调用时是invokespecial

说白了虚方法就是可以实现多态的即重写的方法,因为多态在编译期间无法确定,只能在运行期间确定

public class Son extends Father {
    public Son() {
        // invokestatic
        super();
//        System.out.println("son");
    }

    public Son(int age) {
        // invokestatic
        this();
//        System.out.println("son age is " + age);
    }

    //不是重写父类的方法,因为静态方法不能重写
    public static void showSta(String s) {
        System.out.println("son static " + s);
    }

    private void showPri(String s) {
        System.out.println("son private " + s);
    }

    public void show() {
        //编译时期就是确定的
        // invokestatic
        showSta("look!");
        // invokestatic
        super.showSta("look!");
        // invokespecial
        showPri("pri!!");
        //invokespecial
        super.showComm();

        //编译期间无法确定的,即会被重写的方法
        //invokevirtual
        showFin();//因为此方法被final修饰,因此无法被重写,因此在编译期间是可以确定的,所以也认为是虚方法
        //invokevirtual
        showComm();
        //invokevirtual
        info();

        Method method = null;//是由实现接口的类进行重写的方法
        // invokeinterface
        method.method();

    }
    public void info() {
        System.out.println("info");
    }

    public static void main(String[] args) {
        Son son = new Son();
        son.show();
    }
}
class Father {
    public Father() {
        System.out.println("father");
    }

    public static void showSta(String s) {
        System.out.println("father static " + s);
    }

    public final void showFin() {
        System.out.println("father final");
    }

    public void showComm() {
        System.out.println("father common");
    }
}

interface Method {
    void method();
}
方法重写的本质
虚方法表

为了解决每次动态分配的问题,提高性能,JVM采用在类的方法区建立一个虚方法表(非虚方法不会出现在表中)来实现,使用索引表来代替查找

每一个类中都有一个虚方法表,表中存放着各个方法的实际入口

虚方法表会在类被加载的链接阶段被创建和初始化,类的变量初始值准备完毕之后,JVM会把该类的方法表也初始化完毕

(动态语言和静态语言【额外】)

动态语言和静态语言的区别就在于两者对于类型的检查是在编译期间还是在运行期间,满足前者就是静态语言,满足后缀就是动态语言

方法返回地址

存放调用该方法的pc寄存器

一个方法的结束,有两种方式

  1. 正常执行完毕
  2. 出现未处理的异常,非正常退出

无论哪种方式的退出,在方法推出后都返回到该方法的调用的位置,方法正常退出时,调用者的pc寄存器的值作为返回地址值,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址值是由通过异常表来确定,栈帧中一般不会保存这部分信息

当一个方法开始执行后,只要两种方式可以退出这个方法:

  1. 执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,简称正常完成出口

    • 一个方法正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值传递给上层的实际数据类型决定
    • 在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int)、lreturn、freturn、dreturn以及areturn(引用类型),另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口初始化方法使用
  2. 在方法执行过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口

方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便再发生异常时候找到处理异常的代码

实际上,方法的退出实际就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何返回值

一些附加信息

【有关栈的面试题】

举例栈溢出的情况?

StackOverflowError

通过-Xss设置栈的大小

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

不能,只能延缓

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

不是

垃圾回收是否会涉及到虚拟机栈?

不会

名称ErrorGC
程序计数器××
本地方法栈×
虚拟机栈×
方法区
方法中定义的局部变量是否线程安全?

具体问题具体分析

以StringBuilder为例:

  1. 方法内部创建且无返回值,安全
  2. 以参数形式接收,不安全,因为可能存在多方方法接收此变量
  3. 方法内部创建,但返回值类型是StringBUilder,不安全,因为其他方法或许会接收,进行更改
  4. 方法内部创建,但返回值类型是String(return s.toString),安全,因为toString会创建一个新的对象,原对象(StringBuilder s)会在方法中死亡,返回的是新创建的String的对象

本地方法接口

本地方法

一个native方法就是一个Java调用非Java代码的接口,该方法的实现是由非Java语言实现的,其作用是融合不同的编程语言为Java所用初衷是融合c/c++,其次因为操作系统大多数也是由c实现的,如果用c编写的代码,其运行效率也高,这就是使用本地方法的主要原因(与外界环境交互)

标识符native可以与所有其他的Java标识符连用,但是abstract除外

本地方法栈

Java虚拟机是用来管理Java方法的调用,而本地方法栈是用来管理本地方法的调用

本地方法栈是线程私有

运行本地方法栈实现成固定大小或者动态扩展(与虚拟机栈相同)

  • 固定大小,StackOverflowException
  • 动态扩展,OutOfMemory

本地方法是由c语言实现的

具体做法是通过在本地方法栈中登录本地方法,在执行引擎执行时加载本地方法库

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

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

并不是所有的虚拟机都支持本地方法,因为Java没有明确要求本地方法栈的使用语言,具体实现方式,数据结构等,如果不支持本地方法也就没有要本地方法栈的必要了

在HotSpot中,将本地方法栈和虚拟机中合二为一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dUctquOK-1618969670225)(JVM.assets/image-20210420132751120.png)]
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值