一篇文章带你搞懂JVM的内存结构(运行时数据区)

JVM 运行时数据区 简介

在这里插入图片描述

  • 当字节码通过前面的:类的加载-> 验证 -> 准备 -> 解析 -> 初始化 这几个阶段完成后,就会用到执行引擎对类进行使用,同时执行引擎将会使用到我们运行时数据区
    在这里插入图片描述

  • 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行 JVM 内存布局规定了Java在运行过程中内存申请分配管理的策略,保证了 JVM 的高效稳定运行。

  • 不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。

我们通过磁盘或者网络 IO 得到的数据,都需要先加载到内存中,然后CPU从内存中获取数据进行读取,也就是说内存充当了CPU和磁盘之间的桥梁

在这里插入图片描述

  • JVM 的运行时数据区包括:堆区、方法区(上图的元数据区和JIT编译产物)、程序计数器、本地方法栈、虚拟机栈
  • 我们常说的JVM调优,90%都是在对堆区进行调优
  • Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,即生命周期和虚拟机一致。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁,即生命周期与线程一致。
  • 其中,每个线程独享的有:独立包括程序计数器、栈、本地栈。(图中灰色块)
  • 线程之间共享的有:堆、堆外内存(永久代或元空间、代码缓存)。(图中红色块)
    在这里插入图片描述

线程简介

  • 线程是一个程序里的运行单元。JVM 允许一个应用有多个线程并行的执行
  • 在 Hotspot JVM 里,每个线程都与操作系统的本地线程直接映射
  • 当一个 Java线程 准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收,资源也会被释放。
  • 操作系统负责所有线程的安排调度到任何一个可用的 CPU 上。一旦本地线程初始化成功,它就会调用 Java 线程中的run()方法。
  • 如果run()方法能够正常执行完,以及对产生的异常能够进行处理,就算是正常的执行完毕;否则就终止 Java线程
  • 当终止 Java线程之后,本地线程还需要判断需不需要终止 JVM,要判断当前的线程是不是最后一个非守护线程,如果当前程序只剩下守护线程了,就可以终止退出了。

JVM 系统线程

  • 如果你使用 jconsole 或者是任何一个调试工具,都能看到在后台有许多线程在运行。
  • 这些后台线程不包括调用public static void main(String[])main线程以及所有这个main线程自己创建的线程。
  • 这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
    • 虚拟机线程:这种线程的操作是需要 JVM 达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要 JVM 达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
    • 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
    • GC线程:这种线程对在 JVM 里不同种类的垃圾收集行为提供了支持。
    • 编译线程:这种线程在运行时会将字节码编译成到本地代码。
    • 信号调度线程:这种线程接收信号并发送给 JVM,在它内部通过调用适当的方法进行处理。

程序计数器(Program Counter 寄存器)

在这里插入图片描述

  • JVM中的程序计数寄存器Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。
  • CPU只有把数据装载到寄存器才能够运行。
  • 这里的寄存器,并非是广义上所指的物理寄存器,或许将其翻译为 PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。
  • JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟

作用

  • PC寄存器存储指向下一条指令的地址,也就是将要执行的指令代码,再由执行引擎读取下一条指令。
    在这里插入图片描述

介绍

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

举例演示

public class PCRegisterTest {
    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;
    }
}

将代码编译成字节码文件,使用 jclasslib 查看;在字节码的左边有一个行号标识,它其实就是指令地址,用于指向当前执行到哪里;右边就是操作指令

// 拿到 10
0: bipush        10
// 存入索引为 1 的地方
2: istore_1
// 拿到 20
3: bipush        20
// 存入索引为 2 的地方
5: istore_2
// 加载索引为 1 的值
6: iload_1
// 加载索引为 2 的值
7: iload_2
// 执行加法运算
8: iadd
// 放入索引为 3 的地方
9: istore_3
// 返回
10: return

在这里插入图片描述

可以看到有一些操作指令的后面有一个#2这样的字符,这就是对应的引用对象地址,表示下一步从常量池中的#2这个地方获取值

在这里插入图片描述

面试考点

使用PC寄存器存储字节码指令地址有什么用呢?

  1. 因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
  2. JVM的 字节码解释器 就需要通过改变 PC寄存器 的值来明确下一条应该执行什么样的字节码指令。

PC寄存器为什么被设定为私有的?

  1. 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法(并发和并行的区别),CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
  2. 由于 CPU时间片 限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
  3. 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

CPU 时间片

  1. CPU时间片 即 CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
  2. 在宏观上:俄们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
  3. 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
  4. 并发即一段时间内有多个任务进行;并行即这一时刻有多个任务同时在进行,对应串行.

在这里插入图片描述

虚拟机栈

  • 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
  • 基于栈结构的【优点】是跨平台,指令集小,编译器容易实现;【缺点】是性能下降,实现同样的功能需要更多的指令。
    在这里插入图片描述

栈和堆的区别

栈是运行时的单位,而堆是存储的单位

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
  • 堆解决的是数据存储的问题,即数据怎么放,放哪里

在这里插入图片描述

  • 当然这也不是绝对的,大部分的数据也是放在堆区(对象),但是栈区也会存放一些数据(基本数据类型的局部变量,引用数据类型的地址)

Java 虚拟机栈

  • Java虚拟机栈(Java Virtual Machine Stack),早期也叫 Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧Stack Frame),对应着一次次的 Java方法 调用。
  • 线程私有的,生命周期和线程一致

在这里插入图片描述

  • 作用】主管Java程序的运行,它保存方法的局部变量(8中基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

栈的特点,为什么要使用栈结构呢?

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM 直接对Java栈的操作只有两个:
    • 每个方法执行,伴随着进栈(入栈、压栈)
    • 执行结束后的出栈工作
  • 对于栈来说,不存在垃圾回收问题,但是存在OOM内存溢出问题。

在这里插入图片描述

开发过程中,在栈中可能出现的异常

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

  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。常见的就是递归:当递归层次非常多,或递归没有出口的时候,就会产生这个异常。
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常。

【拓展】怎么设置栈内存大小?

  • 官网介绍:https://docs.oracle.com/en/java/javase/11/tools/java.htmlMain Tools to Create and Build Applications -> java -> java Command-Line Argument Files -> 搜索 -Xss
    在这里插入图片描述

  • 我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

-Xss1m
-Xss1k

// 其他命令
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。

// 可以通过输出递归层数来查看是否设置成功
// count 记录层数
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count++);
        main(args);
    }
}

栈的存储单位:栈帧

  • 每个线程都有自己的栈,栈中的数据都是以栈帧Stack Frame)的格式存在。
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈运行原理

  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出 / 后进先出原则。

  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧Current Frame),与当前栈帧相对应的方法就是当前方法Current Method),定义这个方法的类就是当前类Current Class)。

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

在这里插入图片描述

代码演示

public class StackFrameTest {
    public static void main(String[] args) {
        method01();
    }

    private static int method01() {
        System.out.println("方法1的开始");
        int i = method02();
        System.out.println("方法1的结束");
        return i;
    }

    private static int method02() {
        System.out.println("方法2的开始");
        int i = method03();;
        System.out.println("方法2的结束");
        return i;
    }
    private static int method03() {
        System.out.println("方法3的开始");
        int i = 30;
        System.out.println("方法3的结束");
        return i;
    }
}
/* 输出结果 
方法1的开始
方法2的开始
方法3的开始
方法3的结束
方法2的结束
方法1的结束
*/
  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧

  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

  • Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出

栈帧的内部结构

每个栈帧中存储着:

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

并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由 局部变量表 和 操作数栈决定的,栈帧的大小也影响了栈能存放的栈帧的个数。

在这里插入图片描述

局部变量表

局部变量表:Local Variables,被称之为局部变量数组本地变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及ReturnAddress类型。
    【一些基本数据类型可以转换成数字,然后存入】

  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变 局部变量表的大小的。

在这里插入图片描述

  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁

理解 Slot(变量槽) ---- 局部变量表的最基本的存储单位

  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。

  • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。

  • 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(1ong和double)占用两个slot
    byte、short、char 在存储前被转换为intboolean也被转换为int0表示false非0表示truelongdouble则占据两个slot

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

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

  • 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)

  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index0slot,其余的参数按照参数表顺序继续排列。【这也就是普通实例方法中无法使用 this 关键字的原因(因为 this 变量不存在于当前方法的局部变量表中)】

在这里插入图片描述

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

在这里插入图片描述

举例:静态变量和局部变量的对比

变量的分类

  • 按照数据类型分 :基本数据类型引用数据类型
  • 按照在类中声明的位置分:成员变量(使用静态修饰的称为类变量,没有被静态修饰的称为实例变量)、局部变量

三者的区别

  • 类变量linkingprepare阶段,给类变量默认赋值 ====> init阶段给类变量显示赋值,即静态代码块赋值
  • 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
  • 局部变量:在使用前必须进行显式赋值,不然编译不通过。
  • 即:成员变量使用前都经过默认初始化赋值;局部变量在使用前则必须显示赋值
  • 【原因】:局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表直接或间接引用的对象都不会被回收,也就是如果对象在栈中的地址被清除掉,那么在堆中的对象就会被当成垃圾清除

操作数栈 (Operand Stack)

  • 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为 表达式栈(Expression Stack
  • 操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据提取数据,即入栈(push)和 出栈(pop)

在这里插入图片描述

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

  • 操作数栈就是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的,这个时候数组是有长度的,而且数组一旦创建,那么就是不可变

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

在这里插入图片描述

  • 栈中的任何一个元素都可以是任意的 Java数据类型【32bit的类型占用一个栈单位深度;64bit的类型占用两个栈单位深度】

  • 操作数栈尽管使用的是数组,但是并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问,只是通过数组实现的而已。

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

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

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

详细分析一下字节码每个步骤对应的局部变量表和操作数栈的变化过程

在这里插入图片描述

  • 上面案例中,所需要的局部变量表的长度是4,操作数栈的最大深度是2
  • bipush表示把byte类型转换成int类型,因为10、20、30都可以使用byte来存储,但是存入局部变量表的时候,就自动转换成int类型了,例如istore

拓展: 通过分析字节码文件了解 i++++i 的区别

在这里插入图片描述

栈顶缓存技术:Top Of Stack Cashing

  • 前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
  • 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(ToSTop-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的 读 / 写 次数提升执行引擎的执行效率
  • 【寄存器:指令更少,执行速度快】

帧数据区(动态链接、方法返回地址、附加信息)

动态链接(Dynamic Linking)

  • 动态链接,也称 指向运行时常量池的方法引用

  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令

  • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用Symbolic Reference)保存在 class文件 的常量池里。

  • 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

在这里插入图片描述

在这里插入图片描述

  • 之所以使用常量池,是因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间
  • 常量池的作用:就是为了提供一些符号和常量,便于指令的识别

方法调用(重点)

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

两种链接方式:静态链接和动态链接

静态链接

  • 当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接

动态链接

  • 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接

绑定机制

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

早期绑定

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

晚期绑定

  • 如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

早晚期绑定的发展历史

  • 随着高级语言的横空出世,类似于 Java 一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。

  • Java 中任何一个普通的方法其实都具备虚函数的特征(体现为运行期才能确定下来),它们相当于C++ 语言中的虚函数( C++ 中则需要使用关键字virtual来显式定义)。如果在 Java 程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

虚方法和非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法

  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。

其他方法称为虚方法。

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

普通调用指令:

  • invokestatic:调用静态方法,解析阶段确定唯一方法版本
  • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法

动态调用指令:

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

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。

在这里插入图片描述

关于 invokednamic 指令

  • JVM字节码指令集一直比较稳定,一直到 Java7 中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进。

  • 但是在 Java7 中并没有提供直接生成invokedynamic指令的方法,需要借助 ASM 这种底层字节码工具来产生invokedynamic指令。直到 Java8 的 Lambda 表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式

  • Java7 中增加的动态语言类型支持的本质是对 Java 虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在 Java 平台的动态语言的编译器

动态类型语言和静态类型语言

  • 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,如果是在编译期就是静态类型语言运行期则是动态类型语言

  • 说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

Java:String info = "abc"; (Java是静态类型语言的,会先编译就进行类型检查)

JS:var name = "abc"; var age = 10; (运行时才进行检查, Python 也类似)

方法重写

Java 语言中方法重写的本质:

  • 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
  • 如果在类型 C 中找到与常量中的描述符合、简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  • 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。
  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodsError异常。

IllegalAccessError介绍

  • 程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变

虚方法表

  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率
  • 因此,为了提高性能,JVM 采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
  • 虚方法表会在类加载的链接阶段(解析)创建开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕。

在这里插入图片描述

  • 如上图所示:如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,否则将会直接连接到Object的方法中。

方法返回地址(Return Address)

存放调用该方法的PC寄存器的值

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

  • 正常执行完成
  • 出现未处理的异常,非正常退出

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

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

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

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

  • 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定
  • 在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用。

(2)在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口

方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
在这里插入图片描述

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

附加信息

  • 栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。

栈的相关面试题

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

  • 通过 -Xss设置栈的大小

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

  • 不能保证不溢出

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

  • 不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。

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

  • 不会,因为栈结构只有出栈和入栈两个操作,如果用不到,那就出栈了,用不着回收

方法中定义的局部变量是否线程安全?

/**
 * 面试题
 * 方法中定义局部变量是否线程安全?具体情况具体分析
 * 何为线程安全?
 *    如果只有一个线程才可以操作此数据,则必是线程安全的
 *    如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全
 */
public class StringBuilderTest {

    // s1的声明方式是线程安全的
    public static void method01() {
        // 线程内部创建的,属于局部变量
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
    }

    // 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
    public static StringBuilder method04() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder;
    }

    // stringBuilder 是线程不安全的,操作的是共享数据
    public static void method02(StringBuilder stringBuilder) {
        stringBuilder.append("a");
        stringBuilder.append("b");
    }


    /**
     * 同时并发的执行,会出现线程不安全的问题
     */
    public static void method03() {
        StringBuilder stringBuilder = new StringBuilder();
        new Thread(() -> {
            stringBuilder.append("a");
            stringBuilder.append("b");
        }, "t1").start();

        method02(stringBuilder);
    }

    // 是线程安全的,因为 toString 会 new 一个 String 对象实例
    // 而且 String 是 final 修饰的类,也是线程安全的
    public static String method05() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder.toString();
    }
}
  • 总结一句话就是:如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。

运行时数据区,是否存在 Error 和 GC ?
在这里插入图片描述

本地方法接口

在这里插入图片描述

  • 简单地讲,一个Native Method是一个Java调用非Java代码的接囗。

  • 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern “C” 告知C++编译器去调用一个 C 的函数。

  • "A native method is a Java method whose implementation is provided by non-java code."(本地方法是一个非Java的方法,它的具体实现是非Java代码的实现)

  • 在定义一个native method时,并不提供实现体(有些像定义一个Java Interface),因为其实现体是由非java语言在外面实现的

  • 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序

代码演示如何编写 Native 方法

/* 标识符 native 可以与其它 java 标识符连用,但是 abstract 除外 */
public class IhaveNatives {
    public native void Native1(int x);
    native static public long Native2();
    native synchronized private float Native3(Object o);
    native void Natives(int[] ary) throws Exception;
}

为什么使用Native Method?

Java使用起来非常方便,然而有些层次的任务用 Java 实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

在 Java 刚发行的时候,在执行效率上远达不到 C/C++ 的水平,但是现在 Java 的运行效率大体上已经跟 C/C++ 差不多了。

Java环境的交互

  • 有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 Java 应用之外的繁琐的细节。

操作系统的交互

  • JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用 Java 实现了 JRE 的与底层系统的交互,甚至JVM的一些部分就是用 C 写的。还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

Sun’s Java

  • Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。JRE 大部分是用Java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.ThreadsetPriority()方法是用 Java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority()ApI。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库External Dynamic Link Library)提供,然后被JVM调用。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。

本地方法栈

  • Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法(本地方法是由C语言实现)的调用
  • 本地方法栈,也是线程私有的。
  • 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面与Java虚拟机栈是相同的)
    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
    • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常。
  • 它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

在这里插入图片描述

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

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

并不是所有的 JVM 都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。

Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一

堆区和方法区

由于堆区和方法区篇幅较长,放在另一篇文章中

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2019最新深入理解JVM内存结构及运行原理(JVM调优)高级核心课程视频教程下载。JVM是Java知识体系中的重要部分,对JVM底层的了解是每一位Java程序员深入Java技术领域的重要因素。本课程试图通过简单易懂的方式,系统的深入讲解JVM相关知识。包括JVM执行过程、虚拟机类加载机制、运行时数据、GC、类加载器、内存分配与回收策略等,全套视频加资料高清无密码  第1讲 说在前面的话 免费 00:05:07  第2讲 整个部分要讲的内容说明 免费 00:06:58  第3讲 环境搭建以及jdk,jre,jvm的关系 免费 00:20:48  第4讲 jvm初体验-内存溢出问题的分析与解决 免费 00:17:59  第5讲 jvm再体验-jvm可视化监控工具 免费 00:21:17  第6讲 杂谈 免费 00:12:37  第7讲 Java的发展历史 00:27:24  第8讲 Java的发展历史续 00:02:27  第9讲 Java技术体系 00:08:46  第10讲 jdk8的新特性 00:07:31  第11讲 lanmbda表达式简介 00:07:02  第12讲 Java虚拟机-classic vm 00:06:06  第13讲 Java虚拟机-ExactVM 00:03:35  第14讲 Java虚拟机-HotSpotVM 00:04:23  第15讲 Java虚拟机-kvm 00:03:04  第16讲 Java虚拟机-JRockit 00:04:12  第17讲 Java虚拟机-j9 00:04:23  第18讲 Java虚拟机-dalvik 00:02:20  第19讲 Java虚拟机-MicrosoftJVM 00:03:57  第20讲 Java虚拟机-高性能Java虚拟机 00:02:58  第21讲 Java虚拟机-TaobaoVM 00:03:06  第22讲 Java内存域-简介 00:07:56  第23讲 Java内存域-Java虚拟机栈 00:12:04  第24讲 Java内存域-程序计数器 00:12:54  第25讲 Java内存域-本地方法栈 00:02:39  第26讲 Java内存域-堆内存 00:05:08  第27讲 Java内存域-方法 00:06:32  第28讲 Java内存域-直接内存运行时常量池 00:15:53  第29讲 对象在内存中的布局-对象的创建 00:21:19  第30讲 探究对象的结构 00:13:47  第31讲 深入理解对象的访问定位 00:08:01  第32讲 垃圾回收-概述 00:06:20  第33讲 垃圾回收-判断对象是否存活算法-引用计数法详解 00:14:08  第34讲 垃圾回收-判断对象是否存活算法-可达性分析法详解 00:07:09  第35讲 垃圾回收算法-标记清除算法 00:04:36  第36讲 垃圾回收算法-复制算法 00:14:35  第37讲 垃圾回收算法-标记整理算法和分代收集算法 00:05:24  第38讲 垃圾收集器-serial收集器详解 00:09:45  第39讲 垃圾收集器-parnew收集器详解 00:04:53  第40讲 垃圾收集器-parallel收集器详解 00:11:02  第41讲 垃圾收集器-cms收集器详解 00:14:58  第42讲 最牛的垃圾收集器-g1收集器详解 00:18:04  第43讲 内存分配-概述 00:04:23  第44讲 内存分配-Eden域 00:22:51  第45讲 内存分配-大对象直接进老年代 00:06:42  第46讲 内存分配-长期存活的对象进入老年代 00:03:40  第47讲 内存分配-空间分配担保 00:04:54  第48讲 内存分配-逃逸分析与栈上分配 00:10:32  第49讲 虚拟机工具介绍 00:10:27  第50讲 虚拟机工具-jps详解 00:11:20  第51讲 虚拟机工具-jstat详解 00:09:20  第52讲 虚拟机工具-jinfo详解 00:05:03  第53讲 虚拟机工具-jmap详解 00:08:48  第54讲 虚拟机工具-jhat详解 00:08:10  第55讲 虚拟机工具-jstack详解 00:10:19  第56讲 可视化虚拟机工具-Jconsole内存监控 00:07:09  第57讲 可视化虚拟机工具-Jconsole线程监控 00:12:18  第58讲 死锁原理以及可视化虚拟机工具-Jconsole线程死锁监控 00:10:38  第59讲 VisualVM使用详解 00:08:03  第60讲 性能调优概述 00:11:22  第61讲 性能调优-案例1 00:23:28  第62讲 性能调优-案例2 00:10:05  第63讲 性能调优-案例3 00:12:41  第64讲 前半部分内容整体回顾 00:15:41  第65讲 Class文件简介和发展历史 免费 00:11:26  第66讲 Class文件结构概述 免费 00:16:50  第67讲 Class文件设计理念以及意义 免费 00:13:41  第68讲 文件结构-魔数 免费 00:09:49  第69讲 文件结构-常量池 免费 00:23:44  第70讲 文件结构-访问标志 免费 00:11:36  第71讲 文件结构-类索引 00:11:26  第72讲 文件结构-字段表集合 00:13:21  第73讲 文件结构-方法表集合 00:10:06  第74讲 文件结构-属性表集合 00:18:23  第75讲 字节码指令简介 00:09:18  第76讲 字节码与数据类型 00:09:34  第77讲 加载指令 00:09:33  第78讲 运算指令 00:10:24  第79讲 类型转换指令 00:13:42  第80讲 对象创建与访问指令 00:09:38  第81讲 操作树栈指令 00:03:27  第82讲 控制转移指令 00:11:58  第83讲 方法调用和返回指令 00:06:37  第84讲 异常处理指令 00:09:44  第85讲 同步指令 00:07:34  第86讲 类加载机制概述 00:07:26  第87讲 类加载时机 00:13:15  第88讲 类加载的过程-加载 00:15:15  第89讲 类加载的过程-验证 00:10:24  第90讲 类加载的过程-准备 00:05:40  第91讲 类加载的过程-解析 00:14:04  第92讲 类加载的过程-初始化 00:19:41  第93讲 类加载器 00:22:41  第94讲 双亲委派模型 00:17:03  第95讲 运行时栈帧结构 00:08:46  第96讲 局部变量表 00:20:48  第97讲 操作数栈 00:08:36  第98讲 动态连接 00:02:56  第99讲 方法返回地址和附加信息 00:03:24  第100讲 方法调用-解析调用 00:09:49  第101讲 方法调用-静态分派调用 00:16:21  第102讲 方法调用-动态分派调用 00:09:02  第103讲 动态类型语言支持 00:09:27  第104讲 字节码执行引擎小结 00:03:38  第105讲 总结与回顾 00:10:55  第106讲 happens-before简单概述 00:15:17  第107讲 重排序问题 00:23:19  第108讲 锁的内存语义 00:13:54  第109讲 volatile的内存语义 00:12:04  第110讲 final域内存语义

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值