JVM 第二部分-1(程序计数器,虚拟机栈,本地方法栈)

本文详细解析了Java虚拟机中的运行时数据区,如程序计数器、虚拟机栈、栈帧、局部变量表、操作数栈和本地方法栈等,以及方法的调用机制,包括动态链接、方法重写和线程安全问题。还探讨了栈溢出、本地方法使用和垃圾回收与虚拟机栈的关系。
摘要由CSDN通过智能技术生成

第二部分:运行时数据区

请添加图片描述

1.程序计数器:

全称是程序计数寄存器,像CPU的寄存器一样,存放线程的下一条指令的地址。每个线程都有一个

(区域小,执行速度快,不会有垃圾回收,也不会报oom错)

两个问题:

  • 使用PC寄存器存储字节码指令地址有什么用呢 或者表述为 为什么使用PC寄存器记录当前线程的执行地址呢
    • 因为CPU在不停地切换线程,当切换回来时,需要通过PC寄存器知道从哪个地方开始执行
    • JVM的解释器也需要PC寄存器告诉它下一条指令在哪里
  • PC寄存器为什么被设定为线程私有
    • 因为CPU在不停地切换线程,当切换回来时,需要通过PC寄存器知道从哪个地方开始执行。要是只有一个PC寄存器,就不知道线程在中断时执行到哪个地方

2.虚拟机栈:

  • 是线程私有的,线程创建时会创建一个虚拟机栈,栈里面有栈帧,对应一个个方法。
  • 作用:管理Java方法的调用,保存方法的局部变量(不是成员变量)(8种基本类型,以及引用类型的引用),部分结果(中间结果),并参与方法的调用和返回
  • 只会进行入栈出栈操作,不会GC,但是会OOM
  • 速度快,仅次于程序计数器
  • 栈越大,可调用方法越多。栈帧越大,可,越小。局部变量表和操做数栈越大,栈帧越大

会有OOM和 StackOverflow 异常,死循环 / 内存泄露 / 方法区class对象太多 会OOM ,一直自己调用自己或者递归可能SOF

请添加图片描述

栈帧:

  • 栈顶的栈帧是当前栈帧,对应的是当前方法,当前方法所属的类是当前类
  • 执行引擎只操作当前栈帧
  • 如果当前方法调用了新的方法,会新建一个栈帧,放在栈顶,成为当前栈帧。当前方法返回,当前栈帧就会被抛弃,前一个栈帧成为当前栈帧
  • 不同的线程有不同的栈,不能引用其他栈的栈帧
  • Java方法有两种返回函数的方式:一是正常的return,void方法在字节码里有return指令,在源代码里面写不写return都行。二是有异常,但是没有处理,被抛出,也会退出程序,程序报错结束
  • 内容:
    • 局部变量表(Local Variables)(或 局部变量数组 本地变量表)
      • 定义为一个数字数组(长度为32位以内的数据占一个slot,64位占两个slot)(其他数据类型都转为int,double,long占两个slot)
      • slot是它的最基本的存储单元(变量槽)【引用变量只占一个槽】
        • slot可以回收,当局部变量过了作用域,后面的局部变量可以重复利用它之前占的槽
      • 主要用于储存方法参数和方法体里的局部变量,这些数据类型包括 基本数据类型,对象引用,returnaddress类型
      • 局部变量表是建立在线程的栈上,是线程的私有数据,所以线程安全**(所以多线程使用同一个类的同一个方法,是线程安全的)**
      • 表里面的变量只在当前方法调用中有效。方法调用完,栈帧销毁,表销毁
      • 局部变量表的大小在编译期确定下来的。并保存在方法的code属性的maximum local varibles数据项中。方法运行期间是不会改变它的大小的。
    • 操作数栈(Openrand Stack)(或称为 表达式栈)
      • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的储存空间
      • 用数组实现
      • 操作数栈是JVM执行引擎的一个工作区。当方法开始执行时,创建新的栈帧,栈帧里创建空的操作数栈
      • 操作数栈的大小在编译期就已经确定,保存在方法的code属性中的max_stack
      • 操作数栈中的元素可以是Java数据类型的任意一个。32位的类型占用一个深度。64位占用两个
      • 操作数栈不是采用访问索引的方式进行访问。只能是通过入栈出栈进行操作
      • 如果被调用的方法有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器指向下一条指令
      • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
      • Java虚拟机的执行引擎是基于栈的执行引擎,里面的栈是操作数栈。
      • 栈顶缓存技术:因为操作数栈是在内存中的,字节码指令多,读写操作频繁,所以hotSpot jvm的设计者把栈顶元素缓存到物理的寄存器中,提高速度
    • 动态链接(Dynamic Linking)(或 指向运行时常量池的方法引用)
      • 就是一个指向运行时常量池的方法引用
      • 每个栈帧都有一个动态链接
      • 作用:
        • 使当前方法的代码支持动态链接。比如:invokedynamic指令
        • 字节码文件里有一个常量池,里面是变量和方法的引用,都是符号引用。【动态链接的作用就是把符号引用转换成调用方法的直接引用】
      • 常量池:作用就是为了提供一些符号和常量,便于指令的识别
    • 方法返回地址(Return Address)(或 方法正常退出或异常退出的定义)
      • 作用:存放调用该方法的方法的PC寄存器的值,
        • 方法正常退出:调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址
        • (不是很清晰)有未捕获异常退出:直接退出该方法,会给该方法的调用者产生返回值
        • 如果方法中有try catch异常,捕获异常时,根据异常表,正常捕获跳转
        • 共同点:调用结束之后,都返回调用该方法的位置
      • 返回指令:ireturn(返回值为boolean,byte,char,short,int)、lreturn、freturn、dreturn、areturn(返回引用)、return(void方法,实例初始化方法,类和接口的初始化方法)
    • 一些附加信息,例如:对重新调试提供支持的信息
    • 【有时会把 动态链接, 方法返回地址, 一些附加信息统称为 帧数据区】
    • 方法的调用:
      • 链接与绑定
        • 静态链接与动态链接
          • 都是把符号引用换成直接引用。区别是前者需要被调用方法在编译期可知,且运行时不变。后者是被调用方法在编译器无法确定下来,只能在运行时转换
        • 早期绑定和晚期绑定
          • 绑定是一个字段,方法,类在符号引用转换成直接引用的过程,这仅仅发生一次
          • 都是将方法和所属类型绑定。区别是前者是方法在编译期确定,且运行期不发生改变,后者是方法在编译期不确定
        • 面向对象的编程语言都有多态,自然具备早期绑定和晚期绑定两种方式
      • 虚方法
        • Java中的普通方法都具有虚方法的特征,相当于C++的虚函数。如果希望某个方法不具有虚函数的特征,加final修饰
          • 非虚方法:方法在编译期间就确定了具体的版本,且运行时不变
          • 静态方法,私有方法,父方法,final方法,构造方法都是非虚方法
          • 其他方法为虚方法
        • 虚拟机提供了几种方法调用指令:
          • 普通调用指令
            • invokestatic:调用static方法,编译期间确定唯一方法版本
            • invokespecial:调用方法,父类方法,私有方法,编译期间确定唯一方法版本
            • invokevirtual:调用虚方法
            • invokeinterface:调用接口方法
          • 动态调用指令:
            • invokedynamic:动态解析需要调用的方法,然后执行
              • Java7修改了虚拟机规范,在指令集加了这个指令。但是没有直接生成这个指令的方式。Java8的lambal表达式出来之后,才有了直接的生成方式
          • 前面四个指令不能干预,最后一个用户可以人为确定方法版本。 invokestatic+invokespecial+final方法(被分到invokevirtual里了)为非虚方法
      • 方法的重写
        • 方法重写的本质:
          • 1.找到操作数栈栈顶的第一个元素所执行的对象的实际类型,记作C
          • 2.如果在类型C中找到和常量中的描述和简单名称都相符的方法,则进行访问权限校验。如果校验通过则返回这个方法的直接引用,查找过程结束。校验不通过,抛出 java.lang.IllegalAccessErrorr 异常
          • 3.如果没有找到相符的方法,按继承关系从下往上对C的各个父类进行第二步的查找和校验过程
          • 4.如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常
          • 【IllegalAccessErrorr 介绍:当程序试图访问或修改一个属性或调用一个方法,如果你没有权限访问,就会引起编译器异常。如果这个错误发生在运行时,就说明这个类发生了不兼容的变化】
      • 虚方法表
        • 作用:减少调用虚方法时查找目标方法的次数,用索引表代替查找【非虚方法不用查找,因为编译期就确定了】
        • 每个类都有一个,表中存放着各个方法的实际入口
        • 创建时间:虚方法表在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完后,JVM会把该类的方法表也初始化完毕

面试题:

  • 举例栈溢出的情况:一直自己调用自己 或者 一直递归
  • 调整栈的大小,能保证不出现溢出吗:不能。如果一直递归不退出,只能推迟出现异常的时间
  • 垃圾回收是否会涉及到虚拟机栈:不会,栈会有OOM,但是没有GC
  • 分配的栈内存越大越好吗:不是。要合理分配,过大会导致线程数过少,而且也会影响jvm其他部分使用的空间
  • 方法中定义的局部变量是否线程安全:如果没有逃逸,只在方法内起作用,是线程安全的。但是如果逃逸了,比如传参进来,return出去,不仅仅在方法内起作用,就不是线程安全

请添加图片描述

请添加图片描述

请添加图片描述

3.本地方法栈

本地方法接口

  • 本地方法:一个Java调用非Java代码的接口,方法体是非Java方法在外面实现的
  • 使用本地方法的原因:
    • 与Java环境外交互
    • 与操作系统交互
    • Sun’s java:sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互
  • 现在使用本地方法越来越少了,除法是和硬件相关的应用,比如通过Java程序驱动打印机或Java系统管理生产设备。因为现在的异构邻域间的通信很发达,比如用socket通信,用web service
  • native可以与除了 abstract 的所有修饰符一起用

本地方法栈

  • 也是线程私有的
  • 允许被实现成固定或动态大小(溢出和 虚拟机栈是一样的)
  • 本地方法是用C写的
  • 具体做法是在本地方法栈 登记本地方法,执行引擎执行时加载本地方法库
  • 当线程调用一个本地方法时,它就不受虚拟机限制,和虚拟机有一样的权限
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 它可以直接使用本机的寄存器
    • 直接从本地内存的堆中分配任意数量的内存
  • 不是所有虚拟机支持本地方法
  • 在hotspot jvm中,直接将本地方法栈和虚拟机栈合二为一
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沛权

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值