由于跨平台型的设计、Java的指令都是根据栈来设计的。不同平台CPU架构不同、所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降。实现同样的功能需要更多的指令。
5.2、内存中的栈与堆栈是运行时的单位、而堆是存储的单位
栈解决程序的运行问题、即程序如何执行、或者说如何处理数据,堆解决的是数据存储的问题,即数据怎么放、放在哪里.
5.3、简介- Java虚拟机栈是什么
Java虚拟机栈(Java Virtual Machine Stack) 早期叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame)。对应着一次次的Java方法调用。
它的生命周期和线程一致,作用是主管Java程序的运行,他保存方法的局部变量(8种基本数据变量、对象的引用地址)、部分结果。并参与方法的调用和返回。
- 栈的特点
- 栈是一种快速有效的分配存储的方式、访问速度仅次于程序计数器
- JVM直接堆Java栈操作只有两个
- 每个方法执行、伴随着进栈【push】
- 执行结束后出栈工作【pop】
- 对于栈来说不存在垃圾回收问题【但存在栈溢出】
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
- 如果采用固定大小的虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常。
- 如果Java虚拟机栈可以动态拓展。并且在尝试扩展的时候无法申请到足够的内存,挥着在创建新的线程时没有足够的内存区创建对应的虚拟机栈、那么Java虚拟机将会抛出一个OutOfMemoryError异常
我们可以使用参数 -Xss 选项来设置线程的最大栈空间。栈的大小直接决定函数调用的最大可达深度。【实际经验 开发分布式项目中如果哪个模块内存占用率比较低、可以进行设置,优化整个项目的启动速度】
5.6、栈的储存单位-
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
-
在这个线程上正在执行的每个方法都各自对应着一个栈帧(Stack Frame)
-
栈帧是一个内存区块、是一个数据集、维系着方法执行过程中的各类数据信息。
- JVM直接对Java栈的操作只有两个、就是对栈帧的压栈和出栈。遵循"先进后出的、后进先出"的原则。
- 在一条活动线程中、一个时间点上,只有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被成为当前栈帧(Current Frame)。与当前栈帧相对应的方法就是当前方法(Current Method) 定义这个方法的类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果在该方法中调用了其他方法、对应的新的栈帧会被创建出来,放在栈的顶端、成为新的当前帧。
- 不用线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
- 如果当前方法调用了其他方法、方法返回之际、当前栈帧会传回此方法的执行结果,给前一个栈帧。接着、虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- Java方法有两种返回函数的方式,一种是正常的函数返回。使用return指令。另外一种是抛出异常。不管使用那种方式、都会导致栈帧被弹出。
每个栈帧中存储着:
-
局部变量表【Local Variables】
-
操作数栈【Operand Stack】(或表达式栈)
-
动态链接【Dynamic Linking】(或指向运行时常量池的方法引用)
-
方法返回地址【Return Address】(或方法正常退出或者异常退出的定义)
-
一些附加信息。
- 局部变量表也被称为局部变量数组或者本地变量表
- 定义为一个数字数组【一维】、主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returenAddress类型。
- 由于局部变量表时建立在线程的栈上的,是线程的私有数据,因此不 存在数据的安全问题
- 局部变量表所需的容量大小是在编译器确定下来的、并保存在方法的Code属性的maximum local variables数据项中、在方法运行期间是不会改变局部变量表的大小的。
- **方法嵌套调用的次数由栈的大小决定、一般来说、栈越大、方法嵌套调用次数越多。**对一个函数而言,它的参数和局部变量越多、使得局部变量表膨胀,它的栈帧就会越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时、虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后、随着方法栈帧的销毁,局部变量表也会随之销毁。
【软件使用的是IDEA里面的插件 jclasslib Bytecode Viewer】在View 下 Show Bytecode With Jclasslib 即可查看。
2、关于Slot的理解- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
- 局部变量表,最基本的存储单元是 Slot【变量槽】
- 局部变量表中存放编译器可知的各种基本类型数据(8种)、引用类型、returnAddress类型的变量
- 在局部变量表里、32位以内的类型只占用一个Slot,64位的类型(long、double)占用两个slot 【byte、short、char在存储之前被转换为int,boolean也被转换为int ,0标识false、非0表示true。 long和double则占据两个Slot】
- JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
- 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照调序被复制到局部变量表中的每一个slot上
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
- 【Slot的重复利用】
- 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
3、操作数栈【Operand stack】在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
栈可以使用数组或者链表进行实现。
每个独立的栈帧中除了包含局部变量表外、还包含一个先进后出的操作数栈。
操作数栈、在方法执行过程中。根据字节码指令,往栈里面写入数据或者提取数据【出栈、入栈】
操作数栈、主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新Pc寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候、一个新的栈帧也会随之被创建出来。这个方法的操作数栈是空的。
每个操作数栈都会拥有一个明确的栈深度用于存储数值、其所需的最大深度在编译器就定义好了,保存在方法的Code属性中。为Max_stack的值。
栈中的任何一个元素都是可以任意的Java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈( pop)操作来完成一次数据访问。
4、代码追踪以简单的操作来大致介绍过程【以小见大】
简要分析:PC寄存器保存指令地址。操作指令中 b代表它的数据类型也就是byte,在后续操作过程中byte、short、char、boolean都会转化为int进行保存。 执行push操作将15压栈。不过在调用方法之前操作数栈是空的。
简要分析: 这时执行下一操作、将栈顶数据弹栈存入局部变量表中。【0的索引位置被this占】
简要分析:这时执行iload指令 【因为数据类型已经转换为int了 所以是i】 从局部变量表中拿出对应位置的数值放入栈中。
5、动态链接 【或指向运行时常量池的方法引用】简要分析:从栈中去除两个数据,出栈执行iadd操作。通过执行引擎翻译成机器指令给CPU进行处理运算。保存到索引为3的位置。 【这里存在一个点: 不管你方法有没有返回值最后的操作一定return。 所以我们Java方法返回值是void其实也是默认有返回的。】,从始至终、栈的最大使用度就是两个空间。所以最大深度就为2.
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
通俗的理解就是
下图为常量池。
【#2代表符号引用 那么通过#2与调用方法进行一个绑定。按地址调用。】
6、方法返回地址为什么需要常量池
常量池的作用、就是为了提供一些符号和常量。便于指令的识别。
方法返回地址、动态链接、附加一些有时被称为帧数据区
作用:存放调用该方法的PC寄存器的值。
一个方法的结束、有两种方式:
- 正常执行完成
- 出现未处理的异常、非正常退出。
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。一个方法在正常调用完成之后究竟需要使用那一返回指令还需要根据方法返回值的实际数据类型而定。
在字节码指令中,返回指令包含ireturn (当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
7、附加信息【不确定存在】栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
5.9、栈顶缓存技术【Top-Of-Stack-Cashing】技术。 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将栈顶元素全部缓存在物理CPu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。【寄存器的优势在于指定少、执行速度快。缓存到CPU寄存器中可跳过执行引擎翻译成机器指令的过程。】
5.10、方法的调用 在JVM中、将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
- 静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
- 动态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
- 晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于c++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
- 非虚方法:
- 如果方法在编译器就确定了具体的调用版本、这个版本在运行是不可变的。这样的方法称为非虚方法
- **静态方法、私有方法、final方法、**实例构造器、父类方法都是非虚方法。
- 其他方法称为虚方法。
虚拟机中提供了以下几条方法调用指令
- 普通调用指令
- invokestatic:调用静态方法、解析阶段确定唯一方法版本
- invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- 动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为千预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的( final修饰的除外)称为虚方法。
5.11、虚拟机栈的五道面试题 1、举例栈溢出的情况 【StackOverflowError】- 主函数中循环调用main函数。【详情见 5.5、设置栈内存大小】
不能保证。只能调整上下限。
3、分配的栈内存越大越好么?并不是。可能会避免过早栈溢出、但不能完全避免栈溢出。而且,对性能要求会有显著的提高。在分布式项目中有比较明显的感受。 所有模块都设置很大的栈内存。那么势必整个项目的启动会随之被影响。可能会挤占其他内存空间。【辩证唯物主义论】
4、垃圾回收是否涉及到虚拟机栈?不会的。
【分析运行时数据区】
1.程序计数器 既不涉及Error、也不涉及GC。只是单纯存储作用。
2.虚拟机栈 存在Error (StackOverflowError) 不存在GC。基本功能体现在入栈出栈。就算有垃圾也随着栈的操作消失。
3.本地方法栈 同虚拟机栈。
4.方法区 存在Error和GC (后续分析)
5.堆 存在Error和GC (后续分析)
5、方法中定义的局部变量是否线程安全?【先理解什么是线程安全】
如果只有一个线程才可以操作此数据、则线程是安全的。
如果有多个线程操作数据、则此数据是共享数据。不考虑同步机制的话,会存在线程安全问题 。
具体问题具体分析
如果只有一个线程才可以操作此数据、则线程是安全的。例如创建一个方法、方法的内部调用没有被其他线程调用或者抢占。那么是线程安全的。但如果作为参数传递进方法。那么则有可能线程不安全。因为声明在外部可能存在多次调用。



