在 Java 的面试过程中,不可避免的一个面试题那就是 JVM,而 JVM 的面试题中,有各种,比如在堆中会被问到的关于垃圾回收机制的相关问题,在栈中会被问到入栈以及出栈的过程,来聊一下关于栈的相关问题,比如,栈帧和动态链接指的是什么?
JVM
JVM(Java Virtual Machine,Java虚拟机)是Java平台的核心组成部分,它是一个可以执行Java字节码的虚拟计算机。JVM的主要职责是加载Java类文件,并且根据这些类文件中的定义来执行相应的操作。
JVM(Java Virtual Machine,Java虚拟机)主要包含以下几个组成部分:
类加载器(Class Loader):负责加载字节码文件到内存,将.class文件中的类信息加载到JVM中,以便JVM能够识别和使用这些类。
运行时数据区(Runtime Data Area):JVM的核心内存空间结构模型,主要包括以下子区域:
- 方法区(Method Area):用于存储虚拟机加载的类信息、常量、静态变量,以及即时编译器编译后的代码等数据。
- 堆(Heap):存储Java程序创建的类实例(对象引用)。这是所有线程共享的内存区域,用于存放对象实例。
- Java栈(JVM Stacks):每个虚拟机线程都有一个私有的栈,用于存储局部变量、方法参数以及方法调用的相关信息。每个方法执行时,都会创建一个栈帧来存储这些信息。
- 程序计数器(Program Counter Register):一块较小的内存空间,作为当前线程所执行的字节码的行号指示器。它记录了线程执行的当前位置。
- 本地方法栈(Native Method Stack):与Java栈非常相似,但用于支持native方法的执行。当JVM调用native方法时,会在这个栈中创建栈帧。
执行引擎(Execution Engine):对JVM指令进行解析,翻译成机器码,然后提交到操作系统中执行。它负责读取JVM指令并驱动其执行。
本地库接口(Native Interface):允许Java代码与其他语言写的代码进行交互。它提供了Java调用其他语言的原生库的能力,使得Java程序能够使用其他语言的库和函数。
本地方法库(Native Method Library):实现了Java本地方法的具体功能,这些方法是使用其他语言(如C或C++)编写的,并通过本地库接口与Java代码进行交互。
JVM中的栈帧
在Java虚拟机(JVM)中,栈帧(Stack Frame)是用于支持方法调用和执行的数据结构,是方法执行时的内存模型。每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧存储了方法的局部变量表、操作数栈、动态链接、方法出口等信息。当一个方法被调用时,一个新的栈帧就会被创建并压入到虚拟机栈中,这个栈帧中保存了方法的局部变量、实际参数、操作数栈、常量池引用等信息。当方法执行完毕后,这个栈帧就会从虚拟机栈中弹出,接着执行上一个方法栈帧中的操作。
栈帧的结构主要包括以下几个部分:
局部变量表(Local Variable Table):存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
操作数栈(Operand Stack):也称为表达式栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
动态链接(Dynamic Linking):指向运行时常量池的方法引用,使得方法中的符号引用在运行时可以直接定位到引用的目标,比如某个类的成员或者方法。
方法返回地址(Return Address):存放着调用该方法的PC寄存器的值。当一个方法执行完毕后,会依赖这个方法出口来恢复上层方法的执行。
就像上图这样,但是看图的时候,又会有人发出疑问,既然动态链接都属于栈帧了,那么为什么还会再标题上把他区分出来,就来说一下这个动态链接的问题。
栈帧当中的动态链接
动态链接是为了支持动态方法的调用过程,这句话看起来好像也没什么毛病,但是总感觉很空,对着面试官如果说这句,那肯定还有下文,所以换成能理解的方式来解读一下。
动态链接实际上就是符号引用转变为直接引用。
符号引用转为直接引用是类加载过程中的一个关键步骤,它发生在解析阶段。符号引用是编译原理中的概念,可以包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
这些符号引用在Java字节码中以CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等类型的常量来表示。
而直接引用则是与内存布局相关的,比如直接指向目标代码的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局紧密相关的,同一个符号引用在不同虚拟机实例上甚至在同一虚拟机实例的不同类加载过程中可能都会转换为不同的直接引用。
在类加载的解析阶段,虚拟机将常量池内的符号引用替换为直接引用的过程称为解析。解析是类加载过程中必不可少的一个环节。如果符号引用无法进行解析,那么将会抛出一个异常,比如常见的java.lang.NoClassDefFoundError或java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。对于这7类符号引用,未必一定能在解析阶段或第一次使用时就完成解析,有些符号引用是在真正使用的时候才进行解析的,这种解析方式称为惰性解析。
总的来说,符号引用转为直接引用是Java类加载过程中解析阶段的一个重要步骤,它确保了符号引用能够被正确地解析为内存中的直接引用,从而实现Java程序的正常运行。