JVM栈

    技术2022-07-13  87

    返回主博客

    返回上一层

    目录

    概述

    栈帧

    局部变量表(local variables)

    操作数栈

    动态链接

    方法返回地址(return address)


    概述

    由于跨平台的设计,java的指令是根据栈来设计的,不同CPU架构不同,不能设计成基于寄存器的。

    优点,跨平台,简单实现,指令小。

    缺点,指令多。

    栈是运行时单位(只要是管理运行的,都是线程分开线程管理的),而堆是存储单位,

    栈的读和写都是由规律的,

    但是堆,就好比一个大仓库,里面的东西都是有大有小的,取的时候也是从中随机取的,取多了就会存在这一个碎片,那一个碎片,所以垃圾回收为了最大利用这个大仓库,减少碎片,搞了一套GC机制。

    每个线程都会创建一个虚拟机栈,内部保存一个个栈帧(Stack Frame),一个栈帧对应一个方法的调用。

     

    作用:

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

     

    补充,你遇到过哪些异常

    RuntimeException:

    空指针

    类型转化异常,

    OutOfMemoryError (堆溢出,或者动态扩展的栈在太大情况下无法获取到足够内存时)

    StackOrverFLowError(如果-Xss固定栈大小而溢出)

    数组越界

    NoClassDefFoundError(运行时根据aaa.bbb.XXX获取类定义时失败,一般都是包版本不一致导致,或者AOP失败导致)

    非RuntimeException:

    ClassNotFindException     这是一个检查型异常,需要显式捕获。     当程序试图通过字符串名加载类时,抛出该异常:          Class 类中的 forName 方法。          ClassLoader 类中的 findSystemClass 方法。          ClassLoader 类中的 loadClass 方法。

    以及那些数据库链接异常等。

     

    栈的存储单位栈帧

    每个线程都有一个自己的栈,栈中的数据是以栈帧为基本单位进行存储的。

    线程中对应的正在执行的方法对应一个栈帧, 当前方法return或者异常,该方法对应的栈帧出栈,

    栈帧维系运行过程需要的数据。

    栈帧

    包含

    局部变量表,操作数栈,动态链接(指向运行时常量池的方法引用),方法返回地址(或者方法正常返回和异常的信息),其他附加信息。

     

    局部变量表(local variables)

    一个数组,存放参数和方法体的局部变量

    线程私有,没有线程安全问题

    大小在编译期间就确定

    基本单位32位的Slot (包括returnAddress)

    32位以内的占一个Slot

    byte,short,char,boolean 强转int

    long,double占俩Slot,使用其起始下标

    局部变量是有作用域的,过了作用域,该其占用的槽,会被重复利用。

    按声明顺序放置

    this引用放置在slots[0]中

     

    在栈帧中,与性能调优最为密切的就是局部变量表,方法执行过程中,VM使用局部变量表完成方法的参数传递。

    局部变量表也是垃圾回收重要节点,被局部变量表中的引用指向的不会被回收。

     

    操作数栈

    可以用数组和链表来实现。(数组实现最简单,下标前移表示出栈pop,后移并修改值就是入栈push)

    32位和占一个深度,64位的占两个深度。

    局部变量表和栈一样,基本单位大小固定32位(一个int的大小)由此可见,其实定义short和int其实区别不大。

    如果调用方法有返回值,返回值会直接压入上一个栈帧的操作数栈,如果该返回值没有store则会调用Pop

    ++i和i++ ,单独的++i和i++其实是没有区别的,都是在局部变量表中自增,

    如果配合a = ++i; a = i++;  前者表示先自增再load到操作数栈,后者表示先load的操作数栈,接着局部变量表中的值自增;

    只要不写i = i++(当然这种是不符合编码风格规范的), 其实运行的结果都是一样的。

     

    栈顶缓存技术(Top of Stack Caching):

    HotSpot VM团队提出的一个技术,将栈顶元素缓存到物理寄存器中,降低对内存读写次数,提升效率。

     

    动态链接

    指向运行时常量池中栈帧所属方法的引用(程序运行时,内存分为数据区和指令区,物理寄存器就是取指令区的指令进行运行的

    class文件符号和方法的引用都是符号引用,动态链接就是将这些符号引用转化为方法的直接引用。

    将动态链接,方法返回地址,附加信息合称为帧数据

    多态其实就是源于这个动态链接

     

    其实运行时常量池中也不过存储的都是方法和类的指针,其实这些真正信息还是在堆中。

     

     方法的调用

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

    静态链接:(早期绑定)invokeSpecial(特定的方法,如构造方法, private 方法, super.xxx方法),invokeStatic

    目标方法在编译期可知。这种情况的符号引用转化为直接引用称静态引用。

    动态链接:(晚期绑定)invokeVirtual, invokeInterface,invokedynamic

    目标方法在运行时确定下来的。

    案例1:

    public static void main(String[] args) { Human human = new Son(); if (human instanceof Father) { //其实还是调用Son ((Father)human).say(); } if (human instanceof Son) { ((Son)human).say(); } }

    案例2:

    ublic class Father implements Human { public static void staticFun() { } public void publicFun() { } private void privateFun() { } public void testDynamicLink() { staticFun(); //INVOKESTATIC com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.staticFun ()V publicFun(); //INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.publicFun ()V privateFun(); //INVOKESPECIAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.privateFun ()V String s = super.toString(); //INVOKESPECIAL java/lang/Object.toString ()Ljava/lang/String; Daughter daughter = new Daughter(); // INVOKESPECIAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.<init> ()V Daughter.staticFun(); //INVOKESTATIC com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.staticFun ()V daughter.publicFun(); //INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.publicFun ()V daughter.fianlFun(); //INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.fianlFun ()V say(); //INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.say ()V Human human = new Daughter(); human.say(); //为什么final修饰的方法也是invokeVirtual } @Override public void say() { System.out.println("I am father"); } }

    留个问题,

    1、为什么final修饰的方法也是invokeVirtual?我也不理解,我觉得应该是invokeSpecial

    2、为什么super是调用invokeSpecial,他的直接父类也有可能没有实现这个方法,比如toString通常在Object里面。

    (这个很好解释,既然是super.aaa() 了,那必定是父类以上,肯定不是子类,既然肯定不是子类,至于调用哪个方法,编译器在编译的时候就可以找到最近的哪个父类有该方法)

    多态:

    运行时多态:泛型,重写(接口参数类型时基类,调用可传子类)。

    编译器多态:函数的重载。

     

    四种调用

    invokestatic 非虚方法

    invokespecial 非虚方法

    invokevirtual 虚方法

    invokeinterface 虚方法

    java的多态和c++不一样

    java认为所有普通成员方法(非private非super调用的成员方法)都是invokevitual或invokeinterce,也就是编译的时候,java就认为他是一个虚方法。

    而C++在编译的时候,编译器以为这个方法就是父类调用的,但是在运行的才知道原来是子类调用。

    java中的接口就类似C++的虚类,但是其实思想不一样,C++的虚类也是继承,就好比继承了一个假的父类,他是为了多态而设计,而java虽然也是为了多态而设计,他是他认为interface就好像是一种能力,而继承就好比是模板。(人类是一个父类,超人就是继承了人类,并且实现了“飞”这个接口)

     

    invokedynamic

    1.7增加,为了实现动态类型语言而设计。

    在1.7中并没有提供直接的调用invokedynamic的方法,需要借助ASM来产生invokedynamic指令。

    知道1.8出现lambda表达式的出现,才出现直接的调用invokedynamic。

    java是静态语言,但是可以用invokedynamic执行动态语言支持

    动态语言和静态语言:区别在于类型检查是在编译期间进行还是运行期间进行

    静态类型语言是判断变量自身的类型信息,动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息

    动态语言:python (str = 'aaa')scala (val str = 'aaa') js (var str = 'aaa')

    静态语言:  java , C, C++, C# (String str = "aaa")

    比如,在python中我们调用a.fun(); 传入对象如果有这个方法就没事,  但是如果传入对象没有这个方法,编译不会报错,运行才会报错。但是如果这个传入的不是期望的对象,但是也会有fun方法,那么他的返回信息,可能在运行时报错,比如你期望获得一个变量,他是他返回了元组。

    /** INVOKEDYNAMIC func()Lcom/jack/ascp/purchase/app/test/vm/dynamiclink/invokedynamic/Fun; [ // handle kind 0x6 : INVOKESTATIC java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; // arguments: (Ljava/lang/String;)Z, // handle kind 0x6 : INVOKESTATIC com/jack/ascp/purchase/app/test/vm/dynamiclink/invokedynamic/LambdaTest.lambda$main$0(Ljava/lang/String;)Z, (Ljava/lang/String;)Z ] */ Fun fun1 = s -> { return s.equals("true"); };

     

    java动态语言的支持,是对java虚拟机规范的修改,而不是对java规范的修改,设计复杂,并且增加了方法的调用。使java可以传递函数,但是效率低,所以stream流效率低主要因为开了很多栈帧。

    stream流可以尽量不用,比如map写着好看,filter好看,开发快,可以用。foreach就算了。或者map的时候我还得加代码而不是传方法,那也就算了,写个for循环吧,何必多次一举。其实有时候stream一串下来,看起来也很难看。java是静态语言,有时候没必要用就不用装逼了。

     

    方法重写机制:

    1、找到操作数栈的第一个元素所指向对象的实际类型,记为C

    2、如果在C中找到方法与常量池中的方法描述相同,则进行权限校验,如果通过直接执行,查找过程结束,如果不通过则返回java.lang.AccessIllegalAccessError,查找过程结束。

    3、如果找不到,则从下到上对父类进行查找。并执行验证。

    4、如果都没有找到,则返回AbstractMethodError。

    这个查找过程漫长,为了提高性能,JVM在类方法区建立了一个虚方法表(virtual method table),是用索引表代替查找。就可以直接找到具体方法,这个virtual method table在链接(的解析)阶段就建立了

    AccessIllegalAccessError:一般情况编译器会校验。一般运行时报的话是因为子包的jar包冲突或者AOP没写好。

     

    方法返回地址(return address)

     

    存放该方法返回到的PC寄存器的值

    返回方式:

    正常返回,异常返回

    不管哪种,都得回到被调用的位置,只不过异常退出没有返回值。

    返回指令根据返回类型决定:ireturn(byte, short,int,boolean,char),lreturn(long), freturn(float), dreturn(double), areturn(引用a表示address) return(void)

    异常处理表

     

    异常捕获从第4个指令到8个指令,如果捕获到异常则到11个指令开始处理。如果代码正常运行到8指令,则goto到16返回了。

     

    5.3.7 一些附加信息

    如对调试支持的一些信息。

     

    5.3.8 面试题

    举例StackOrverFLow的原因情况

      通过-Xss设置栈的大小,如果溢出整个内存则会OOM

    通过调整大小就能保住不会溢出吗?

      可能,但不能保证,如果是递归深度太大,可以调整,但是如果死循环,就一定溢出

    分配栈内存越大越好吗?

      按需分配,栈越大会挤占其他内存空间。

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

      不会,栈是一个有规则(Push 和Pop)的内存空间,在用就在栈中,不需要了边Pop。怎么会有这么傻逼的问题。

    局部变量是否数据安全

      何为线程安全:当多个线程操作同一个数据时,不会违背我们的期望。

      如果每个线程只读不写,或者只有一个线程读且写, 可以认为肯定是线程安全的。

      JVM中所有内存结构都是线程安全的,如果不安全就是JVM的debug,JVM不会因为java程序员菜,导致由线程不安全引发的JVM中的数据结构混乱,从而使JVM无法运行。

     

    参数:

    -XX:ThreadStackSize

    设置为0代表使用不同的操作系统默认值

     

    Processed: 0.018, SQL: 9