JVM栈

返回主博客

返回上一层

目录

概述

栈帧

局部变量表(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或者异常,该方法对应的栈帧出栈,

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

栈帧

包含

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

 

局部变量表(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代表使用不同的操作系统默认值

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值