精华内容
下载资源
问答
  • 学习JVM的第一天,首先先了解下JVM。...1.一次编译,到处运行(平台) 2.自动内存管理,垃圾回收功能 3.数组下标越界检查 4.多态 比较: JVM,JRE,JDK JVM<JRE(JVM+基础类库)<JDK(JVM+基础类库+编译工具)...

    学习JVM的第一天,首先先了解下JVM。

    什么是JVM?

    定义: JVM全称Java Virtual Machine(Java程序的运行环境(Java二进制字节码的运行环境))
    好处:
    1.一次编译,到处运行(跨平台)
    2.自动内存管理,垃圾回收功能
    3.数组下标越界检查
    4.多态
    比较: JVM,JRE,JDK
    JVM<JRE(JVM+基础类库)<JDK(JVM+基础类库+编译工具)

    程序计数器

    在这里插入图片描述
    定义: Program Counter Register 程序计数器(寄存器)
    程序计数器是Java里虚拟抽象的名词,实际是用cpu的寄存器来当做程序计数器。
    程序执行流程:java源代码---->Jvm指令---->解释器---->机器码---->CPU
    作用: 记住下一条jvm指令的执行地址
    特点:
    1.是线程私有的,每条线程都有自己的程序计数器
    2.根本不会存在内存溢出的情况

    在这里插入图片描述

    虚拟机栈

    在这里插入图片描述
    定义: Java Virtual Machine Stacks (Java 虚拟机栈)
    每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
    栈: 线程运行需要的内存空间(一个栈里可以有多个栈帧)
    栈帧: 每个方法运行时需要的内存(包含参数,局部变量,返回地址)
    在这里插入图片描述
    代码演示:

    /**
     * 演示栈帧
     */
    public class Demo1_1 {
        public static void main(String[] args) throws InterruptedException {
            method1();
        }
        private static void method1() {
            method2(1, 2);
        }
        private static int method2(int a, int b) {
            int c =  a + b;
            return c;
        }
    }
    

    程序debug运行见:
    在这里插入图片描述
    如图:进栈顺序:main方法先压栈,之后method1方法进栈,最后method2进栈并携带两个参数a=1和b=2。
    出栈顺序:method2出栈–>method1出栈–>最后main方法出栈–>程序结束

    问题

    1.垃圾回收是否涉及栈内存?

    答:不涉及。因为栈内存无非就是一次次的方法调用所产生的栈帧内存,而栈帧在每一次方法调用结束后都会被弹出栈,根本不需要垃圾回收来管理。(垃圾回收只回收堆内存中无用的对象)

    2.栈内存分配的越大越好吗?

    答:肯定不是,栈内存分配越大,反而会让线程数变少。因为物理内存大小是一定的,比如说,一个线程使用的是栈内存吧,假设使用1M的内存,物理内存500M,理论上就可以有500个线程同时运行。如果每个线程设置2M,那么只能同时有250个线程运行。所以栈内存分配越大并不是越好,它分配大了通常只是能够进行多次的方法递归调用,而不会增快程序的运行效率,反而会影响线程数目的变少。一般采用默认的就可以,不必在程序启动的时候手动修改。

    3.方法内的局部变量是否线程安全?

    答:分两种情况:
    1.如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
    2.如果是局部变量引用了对象,或逃离方法的作用范围,需要考虑线程安全。
    *逃离方法:*在方法结束的时候把这个变量return出去
    演示代码:

    /**
     * 局部变量的线程安全问题
     */
    public class Demo1_17 {
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            sb.append(4);
            sb.append(5);
            sb.append(6);
            new Thread(()->{
            	//m1();
                m2(sb);
               	//m3(); 
            }).start();
        }
        public static void m1() {
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb.toString());
        }
        public static void m2(StringBuilder sb) {
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb.toString());
        }
        public static StringBuilder m3() {
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            return sb;
        }
    }
    

    如上代码:调用m1方法时局部变量是线程安全的,而m2和m3是不安全的。

    栈内存溢出

    导致栈内存溢出有两种情况:
    1.栈帧过多导致栈内存溢出(递归操作,大量方法进栈)
    如下代码:

    /**
     * 演示栈内存溢出 java.lang.StackOverflowError
     * -Xss256k
     */
    public class Demo1_2 {
        private static int count;
        public static void main(String[] args) {
            try {
                method1();
            } catch (Throwable e) {
                e.printStackTrace();
                System.out.println(count);
            }
        }
        private static void method1() {
            count++;
            method1();
        }
    }
    

    运行结果:java.lang.StackOverflowError 并打印了2736。可见方法递归了2736次,也就是栈里有2736个方法时最终导致栈内存溢出。
    2.栈帧过大导致栈内存溢出(一般不会出现,一个int变量才4个字节,而默认栈大小都1M左右)
    一般不会出现,这里就不做演示了。

    本地方法栈

    在这里插入图片描述
    定义: Java虚拟机调用本地方法时分配的内存空间
    本地方法: 不是由Java代码编写的方法的方法。因为Java代码是有限制的,不能直接跟操作系统底层打交道,所以就需要用C,C++等语言编写的本地方法与底层打交道,Java代码可以间接的通过本地方法来调用到底层的功能。
    如图:带native的方法就是本地方法

    在这里插入图片描述

    本篇为JVM开篇

    >>下一篇:内存结构-堆(Heap)

    展开全文
  • 由于平台性的设计,Java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点: 平台,指令集小,编译器容易实现,缺点: 性能下降,实现同样的功能需要更多的指令。 内存中的...

    第 5 章 虚拟机栈

    1、虚拟机栈概述

    • -Xss512k 设置栈内存大小为512k

    1.1、虚拟机栈的出现背景

    文档网址

    虚拟机栈出现的背景

    1. 由于跨平台性的设计,Java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
    2. 优点: 跨平台,指令集小,编译器容易实现,缺点: 性能下降,实现同样的功能需要更多的指令

    内存中的栈与堆

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

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

    image-20200705163928652

    1.2、虚拟机栈的存储内容

    虚拟机栈的基本内容

    Java虚拟机栈是什么

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

    虚拟机栈的生命周期

    • 生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了

    虚拟机栈的作用

    负责Java程序运行, 保存局部变量。 (Java程序运行, 实际也就是运行的功能函数, 即方法!)

    • 负责Java程序的运行它保存方法的 局部变量(8 种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回
    1. 局部变量,它是相比于成员变量来说的(或属性/字段)
    2. 基本数据类型变量 VS 引用类型变量(类、数组、接口)

    1.3、虚拟机栈的特点

    栈的特点

    • 先进后出的数据结构
    • 不存在垃圾回收的概念, 也就是说不会内存溢出,发生OOM, 但是会出现栈溢出 (StackOverFlow)
    • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对虚拟机栈的操作只有两个:
      • 每个方法执行,伴随着 进栈(入栈、压栈)
      • 执行结束后的 出栈 工作
    • 对于栈来说不存在垃圾回收问题(栈存在溢出的情况)
      image-20200705165025382

    1.4、虚拟机栈的异常 (重点)

    栈中可能出现的异常 : 栈溢出

    面试题:栈中可能出现的异常

    1. Java 虚拟机规范允许Java栈的大小动态的或者是固定不变
    2. 如果采用固定大小的Java虚拟机栈那每一个线程的Java虚拟机栈可以在线程创建的时候独立设置该线程的栈容量。
    3. 如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常。
    4. 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutofMemoryError 异常。(该OOM是由于扩展栈容量时, 没有足够的内存使用)
      • 因为虚拟机栈的容量可以动态增加栈的大小(占的更多的内存), 当别的线程创建的时候, 没有足够的内存去创建对应的虚拟机栈, 此时就会报OOM

    栈异常演示

    • 代码
    public class StackErrorTest {
        private static int count = 1;
        public static void main(String[] args) {
            System.out.println(count);
            count++;
            main(args);
        }
    }
    
    • 递归调用 11418 次后,出现栈内存溢出

    image-20200727220541524

    1.5、设置栈内存大小

    设置栈内存的大小

    • -Xss设置栈空间大小
    • 我们可以使用参数 -Xss 选项来设置线程的最大栈空间栈的大小直接决定了函数调用的最大可达深度。
    -Xss1024m		// 栈内存为 1024MBS
    -Xss1024k		// 栈内存为 1024KB
    
    • 设置线程的最大栈空间:256KB
      image-20200727220826190
    • 代码测试
    public class StackErrorTest {
        private static int count = 1;
        public static void main(String[] args) {
            System.out.println(count);
            count++;
            main(args);
        }
    }
    
    • 递归 2471 次,栈内存溢出

    image-20200727220915116

    2、栈的存储单位

    2.1、栈的运行原理 (重点)

    栈存储什么?

    1. 每个线程都有自己的栈 (虚拟机栈是线程独有的资源),栈中的数据都是以栈帧(Stack Frame)的格式存在
    2. 在这个线程上 正在执行的每个方法都各自对应一个栈帧(Stack Frame)
    3. 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

    栈的运行原理

    • 虚拟机栈就只能有入栈, 出栈两种操作
    • 在虚拟机栈里面, 同一时间点上只能有一个活动的栈帧, 即栈顶的栈帧(当前正在执行的方法)
    • 执行引擎只对运行当前栈帧的指令
    • 在栈顶栈帧(methodA)中调用了methodB, (走到调用B方法的那一行)当前栈顶就换成了methodB, methodB执行完就会弹出栈, methodA就会重新回到栈顶, 如果methodB有返回值, 会将返回值返回给methodA.
    1. JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出(后进先出)原则
    2. 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的
      • 这个栈帧被称为当前栈帧(Current Frame)
      • 当前栈帧相对应的方法就是当前方法(Current Method)
      • 定义这个方法的类就是当前类(Current Class)
    3. 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
    4. 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
    5. 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
    6. 如果当前方法调用了其他方法方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
    7. Java方法有两种返回函数的方式,但不管使用哪种方式,都会导致栈帧被弹出
      • 一种是正常的函数返回,使用return指令
      • 另外一种是抛出异常
        image-20200705203142545

    代码示例:字节码方式分析

    • 代码
    public class StackFrameTest {
        public static void main(String[] args) {
            StackFrameTest test = new StackFrameTest();
            test.method1();
        }
    
        public void method1() {
            System.out.println("method1()开始执行...");
            method2();
            System.out.println("method1()执行结束...");
        }
    
        public int method2() {
            System.out.println("method2()开始执行...");
            int i = 10;
            int m = (int) method3();
            System.out.println("method2()即将结束...");
            return i + m;
        }
    
        public double method3() {
            System.out.println("method3()开始执行...");
            double j = 20.0;
            System.out.println("method3()即将结束...");
            return j;
        }
    }
    
    • 先执行的函数,最后执行结束
    method1()开始执行...
    method2()开始执行...
    method3()开始执行...
    method3()即将结束...
    method2()即将结束...
    method1()执行结束...
    
    • 反编译,可以看到每个方法后面都带有return 语句或者 ireturn 语句
      public void method1();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #6                  // String method1()开始执行...
             5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: aload_0
             9: invokevirtual #8                  // Method method2:()I
            12: pop
            13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
            16: ldc           #9                  // String method1()执行结束...
            18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            21: return
          LineNumberTable:
            line 16: 0
            line 17: 8
            line 18: 13
            line 19: 21
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      22     0  this   Lcom/atguigu/java1/StackFrameTest;
    
      public int method2();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #10                 // String method2()开始执行...
             5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: bipush        10
            10: istore_1
            11: aload_0
            12: invokevirtual #11                 // Method method3:()D
            15: d2i
            16: istore_2
            17: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
            20: ldc           #12                 // String method2()即将结束...
            22: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            25: iload_1
            26: iload_2
            27: iadd
            28: ireturn
          LineNumberTable:
            line 22: 0
            line 23: 8
            line 24: 11
            line 25: 17
            line 26: 25
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      29     0  this   Lcom/atguigu/java1/StackFrameTest;
               11      18     1     i   I
               17      12     2     m   I
    
      public double method3();
        descriptor: ()D
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #13                 // String method3()开始执行...
             5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: ldc2_w        #14                 // double 20.0d
            11: dstore_1
            12: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
            15: ldc           #16                 // String method3()即将结束...
            17: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            20: dload_1
            21: dreturn
          LineNumberTable:
            line 30: 0
            line 31: 8
            line 32: 12
            line 33: 20
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      22     0  this   Lcom/atguigu/java1/StackFrameTest;
               12      10     1     j   D
    

    代码示例:画图方式分析
    在这里插入图片描述

    在这里插入图片描述在这里插入图片描述在这里插入图片描述


    2.2、栈的内部结构 (重点)

    栈帧内部结构

    • 局部变量表
    • 操作数栈
    • 动态链接
    • 方法返回地址
    • 一些附加信息 锁记录

    每个栈帧中存储着:

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

    image-20200705204836977

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

    image-20200705205443993

    3、局部变量表 (重点)

    3.1、认识局部变量表

    认识局部变量表

    • 用来存储方法的形参和方法体的局部变量
    • 局部变量表不存在安全问题, 因为局部变量表是在栈帧中, 栈帧又在虚拟机栈中,虚拟机栈是线程独有的资源.
    • 局部变量表的容量大小, 在编译阶段就确定了(因为在编译器就能确定形参,局部变量的个数), 运行期不会改变容量大小;
    • 对于一个方法, 它的参数和局部变量越多, 局部变量表就越大, 栈帧就越大. 栈帧的大小: 主要由局部变量表、操作数栈来控制
    • 当方法调用结束, 栈帧就销毁了, 所以栈帧里的局部变量表也就销毁了
    1. 局部变量表:Local Variables,被称之为局部变量数组或本地变量表
    2. 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型对象引用(reference),以及returnAddress类型。
    3. 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
    4. 局部变量表所需的容量大小在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
    5. 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多
      • 对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。
      • 进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
    6. 局部变量表中的变量只在当前方法调用中有效。
      • 在方法执行时虚拟机通过使用局部变量表完成参数值参数变量列表传递过程
      • 当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

    局部变量表所需的容量大小是在编译期确定下来的 (因为在编译器就能确定形参,局部变量的个数)

    • 代码
    public class LocalVariablesTest {
        private int count = 0;
    
        public static void main(String[] args) {
            LocalVariablesTest test = new LocalVariablesTest();
            int num = 10;
            test.test1();
        }
    
        public void test1() {
            Date date = new Date();
            String name1 = "atguigu.com";
            test2(date, name1);
            System.out.println(date + name1);
        }
    
        public String test2(Date dateP, String name2) {
            dateP = null;
            name2 = "songhongkang";
            double weight = 130.5;//占据两个slot
            char gender = '男';
            return dateP + name2;
        }
    }
    
    • 反编译后,可得结论:
      • 在编译期间,局部变量的个数、每个局部变量的大小都已经被记录下来; 编译就确定了
      • 所以局部变量表所需的容量大小是在编译期确定下来的

    image-20200728094057286

    • 利用JClassLib也可以查看局部变量的个数

    image-20200728094210198

    思考:

    • 代码
    public static void main(String[] args) {
        if(args == null){
            LocalVariablesTest test = new LocalVariablesTest();
        }
        int num = 10;
    }
    
    • 反编译后,提出问题:上面代码中的 test 变量跑哪儿哪了呢?
    • test 变量和 num 变量共用一个 slot(槽)
      • 当test在if结束, 就销毁了, 此时num就占用了之前test的slot
        image-20200728094439892

    字节码中方法内部结构的剖析

    • [Ljava/lang/String] :
      • [] 表示数组
      • L 表示引用类型
      • java/lang/String 表示 java.lang.String
    • 合起来就是:main() 方法的形参类型为 String[]

    image-20200728100032128

    • 字节码,字节码长度为 16(0~15)
      image-20200728100232360

    • 方法异常信息表(存放方法的异常)
      image-20200728100332466

    • 杂项(Misc)
      image-20200728100423166

    • 字节码指令行号和原始 java 代码行号的对应关系
      image-20200728100757147

    • 注意:生效行数和剩余有效行数都是针对于字节码文件的行数
      image-20200728101044588

    3.2、关于 Slot 的理解 (了解)

    关于 Slot 的理解

    • 将局部变量表分为一个个的变量槽, 每个槽用来放变量
    • 64bit的数据类型的变量, 占两个slot
    • 如果当前方法(栈帧)是实例方法, 局部变量表的第一个slot为this
    1. 参数值的存放总是从局部变量数组索引 0 的位置开始,到数组长度-1的索引结束
    2. 局部变量表,最基本的存储单元是Slot(变量槽),局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
    3. 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型占用两个slot(1ong和double)。
    4. JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
    5. 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
    6. 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)
    7. 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列

    image-20200705212454445

    Slot 代码示例

    this 存放在 index = 0 的位置:

    • 代码
    // 实例方法
    public void test3() {
        this.count++;
    }
    
    • 局部变量表:this 存放在 index = 0 的位置

    image-20200728102851509


    64位的类型(1ong和double)占用两个slot

    • 代码
    public String test2(Date dateP, String name2) {
        dateP = null;
        name2 = "songhongkang";
        double weight = 130.5;//占据两个slot
        char gender = '男';
        return dateP + name2;
    }
    
    • weight 为 double 类型,index 直接从 3 蹦到了 5

    image-20200728103053250


    static方法无法调用 this的原因: (重要)
    • this 不存在与 static 方法的局部变量表中,所以无法调用, 在静态方法中, 不能够调用this
    //练习:
    public static void testStatic(){
        LocalVariablesTest test = new LocalVariablesTest();
        Date date = new Date();
        int count = 10;
        System.out.println(count);
        //因为 this 变量不存在于当前方法的局部变量表中!!
        //System.out.println(this.count);
    }
    

    3.3、Slot 的重复利用

    Slot 的重复利用

    • 栈帧中的局部变量表中的槽位可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后声明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
    • 代码
    public void test4() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        //变量c使用之前已经销毁的变量b占据的slot的位置
        int c = a + 1;
    }
    
    • 局部变量 c 重用了局部变量 b 的 slot 位置

    image-20200728103342525

    静态变量与局部变量的对比

    变量的分类 (重要)
    • 按照数据类型分:
      • 基本数据类型
      • 引用数据类型
    • 按照在类中声明的方式分:
      • 类变量(静态变量) :
        • linkingprepare阶段:给类变量默认赋值即零值
        • initial阶段:给类变量显式赋值静态代码块赋值
      • 实例变量: 随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
      • 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过,应该是栈中数据弹出后,不会清除上次的值,再次使用时,如果不显示初始化,就会出现脏数据
      • final static 变量 : 在编译时就赋上初始值, 在linking的prepare阶段 显示赋值
    • 参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
    • 类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值
    • 和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

    代码示例

    • 报错:局部变量未初始化

    image-20200728103943968

    补充说明

    1. 在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表在方法执行时,虚拟机使用局部变量表完成方法的传递。
    2. 局部变量表中的变量也是重要的垃圾回收根节点只要被局部变量表中直接或间接引用的对象都不会被回收。

    4、操作数栈

    4.1、操作数栈 (虚拟机栈中栈帧中的部分)

    操作数栈的特点

    • 根据字节码指令往操作数栈中, 进行运算操作

    操作数栈:Operand Stack

    1. 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)操作数栈,也可以称之为表达式栈(Expression Stack)
    2. 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
    3. 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈,比如:执行复制、交换、求和等操作

    image-20200706090618332

    代码举例

    • 左边为 java 源代码,右边为 java 代码编译生成的字节码指令

    image-20200706090833697

    0: iconst_2		// 将常量 15 压入操作数栈中
    1: istore_1		// 将常量 15 保存至变量 i 中
    2: iconst_3		// 将常量 8 压入操作数栈中
    3: istore_2		// 将常量 8 保存至变量 j 中
    4: iload_1		// 加载变量 i, 并将 i 变量的值15放到操作数栈中
    5: iload_2		// 加载变量 j, 并将 j 变量的值8放到操作数栈中
    6: iadd			// 将操作数栈的数据进行 累加操作
    7: istore_3		// 加法结果保存在变量 k 中
    8: return
    

    4.2、操作数栈的作用

    操作数栈的作用

    • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
    1. 操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
    2. 操作数栈就是JVM执行引擎的一个工作区当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这时方法的操作数栈是空的(这个时候数组是有长度的,只是操作数栈为空)
    3. 每一个操作数栈都大小都确定了最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。
    4. 栈中的任何一个元素都是可以任意的Java数据类型
      • 32bit的类型占用一个栈单位深度
      • 64bit的类型占用两个栈单位深度
    5. 操作数栈并非采用访问索引的方式进行数据访问的,而是只能通过标准的入栈出栈操作来完成一次数据访问
    6. 如果被调用的方法带有返回值的话,其 返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
    7. 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
    8. 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的指的就是操作数栈

    操作数栈的深度

    通过反编译生成的字节码指令查看操作数栈的深度
    image-20200728105905469

    5、代码追踪

    操作数栈代码追踪

    • 代码
    public void testAddOperation() {
        //byte、short、char、boolean:都以int型来保存
        byte i = 15;
        int j = 8;
        int k = i + j;
    }
    
    • 反编译得到的字节码指令
     0 bipush 15	// 操作数15入操作数栈中
     2 istore_1		// 将操作数栈中的数15, 放到局部变量表1的位置, 也就是将15放到变量i中
     3 bipush 8     // 操作数8放入操作数栈中
     5 istore_2	    // 将操作数栈中的数8, 放到局部变量表2的位置, 也就是将8放到变量j中
     6 iload_1		// 将局部变量表中i变量的值, 放到操作数栈中
     7 iload_2 	    // 将局部变量表中j变量的值, 放到操作数栈中
     8 iadd			// 将操作数栈中的两个数, 进行相加操作, 得到一个结果
     9 istore_3     // 再将操作数栈中的这个结果, 放到局部变量表3的位置, 即赋值给变量k
    10 return
    

    程序执行流程如下

    • 首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为0,然后使用bipush让操作数15入操作数栈

    image-20200706093131621

    • 执行完后,让PC + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表1的位置,我们可以看到局部变量表的已经增加了一个元素
    • 解释为什么局部变量表索引从 1 开始,因为该方法为实例方法局部变量表索引为 0 的位置存放的是 this

    image-20200706093251302

    • 然后PC+1,指向的是下一行。让操作数8也入栈,同时执行istore操作,存入局部变量表中
      image-20200706093646406

      image-20200706093751711

    • 然后从局部变量表中,依次将数据放在操作数栈中,等待执行 add 操作

    image-20200706093859191

    image-20200706093921573

    • 然后将操作数栈中的两个元素执行相加操作,并存储在局部变量表3的位置

    image-20200706094046782

    image-20200706094109629

    关于 int j = 8; 的说明

    • 我们反编译得到的字节码指令如下
      • 因为 8 可以存放在 byte 类型中,所以压入操作数栈的类型为 byte ,而不是 int ,所以执行的字节码指令为 bipush 8
      • 然后执行将数值 8 存放在 int 类型的变量中:istore_2
        image-20200728112222199

    关于调用方法,返回值入操作数栈的说明

    • 代码
    public int getSum(){
        int m = 10;
        int n = 20;
        int k = m + n;
        return k;
    }
    
    public void testGetSum(){
        //获取上一个栈桢返回的结果,并保存在操作数栈中
        int i = getSum();
        int j = 10;
    }
    
    • getSum() 方法字节码指令:最后带着个 ireturn

    image-20200728112603621

    • testGetSum() 方法字节码指令:一上来就加载 getSum() 方法的返回值

    image-20200728112631597

    ++i 与 i++ 的区别

    并不是原子操作, i++分了4个原子指令, 所以在多线程并发的时候, i++操作可能导致线程不安全问题!

    • 代码
    // 程序员面试过程中, 常见的i++和++i 的区别,放到字节码篇章时再介绍。
    public void add(){
        //第1类问题:
        int i1 = 10;
        i1++;
    
        int i2 = 10;
        ++i2;
    
        //第2类问题:
        int i3 = 10;
        int i4 = i3++;
    
        int i5 = 10;
        int i6 = ++i5;
    
        //第3类问题:
        int i7 = 10;
        i7 = i7++;
    
        int i8 = 10;
        i8 = ++i8;
    
        //第4类问题:
        int i9 = 10;
        int i10 = i9++ + ++i9;
    }
    
    • 下面,我根据字节码指令,简单说下 i++ 和 ++i 的区别
     0 bipush 10
     2 istore_1
     3 iinc 1 by 1
     6 bipush 10
     8 istore_2
     9 iinc 2 by 1
    12 bipush 10
    14 istore_3
    15 iload_3
    16 iinc 3 by 1
    19 istore 4
    21 bipush 10
    23 istore 5
    25 iinc 5 by 1
    28 iload 5
    30 istore 6
    32 bipush 10
    34 istore 7
    36 iload 7
    38 iinc 7 by 1
    41 istore 7
    43 bipush 10
    45 istore 8
    47 iinc 8 by 1
    50 iload 8
    52 istore 8
    54 bipush 10
    56 istore 9
    58 iload 9
    60 iinc 9 by 1
    63 iinc 9 by 1
    66 iload 9
    68 iadd
    69 istore 10
    71 return
    

    i++

    • java 源代码
    //第2类问题:
    int i3 = 10;
    int i4 = i3++;
    
    • 字节码指令
      • bipush 10 :将 10 压入操作数栈
      • istore_3 :将操作数栈中的 10 保存到变量 i3 中
      • iload_3 :将变量 i3 的值(10)加载至操作数栈中
      • iinc 3 by 1:变量 i3 执行 +1 操作
      • istore 4:将操作数栈中的值保存至变量 i4 中(10)
    12 bipush 10
    14 istore_3
    15 iload_3
    16 iinc 3 by 1
    19 istore 4
    

    ++i

    • java 源代码
    int i5 = 10;
    int i6 = ++i5;
    
    • 字节码指令
      • bipush 10 :将 10 压入操作数栈
      • istore 5 :将操作数栈中的 10 保存到变量 i5 中
      • iinc 5 by 1:变量 i5 执行 +1 操作
      • iload 5 :将变量 i5 的值(11)加载至操作数栈中
      • istore 6:将操作数栈中的值保存至变量 i6 中(11)
    21 bipush 10
    23 istore 5
    25 iinc 5 by 1
    28 iload 5
    30 istore 6
    

    总结:

    • i++:先将 i 的值加载到操作数栈,再将 i 的值加 1
    • ++i:先将 i 的值加 1,在将 i 的值加载到操作数栈

    在这里插入图片描述

    6、栈顶缓存技术

    栈顶缓存技术:Top Of Stack Cashing

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

    7、动态链接 (指向运行时常量池的方法引用) (重要)

    动态链接(或 指向运行时常量池的方法引用

    动态链接:Dynamic Linking

    image-20200706100311886

    1. 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用
    2. 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令
    3. Java源文件被编译到字节码文件中时,所有的变量方法引用都作为符号引用(Symbolic Reference)保存在class文件常量池
    4. 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
      image-20200706101251847

    代码示例

    • 代码
    public class DynamicLinkingTest {
    
        int num = 10;
    
        public void methodA(){
            System.out.println("methodA()....");
        }
    
        public void methodB(){
            System.out.println("methodB()....");
            methodA();
            num++;
        }
    }
    
    • 字节码指令中,methodB() 方法中通过 invokevirtual #7指令调用了方法 A
    • 那么 #7是个啥呢?
      public void methodB();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #6                  // String methodB()....
             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: aload_0
             9: invokevirtual #7                  // Method methodA:()V
            12: aload_0
            13: dup
            14: getfield      #2                  // Field num:I
            17: iconst_1
            18: iadd
            19: putfield      #2                  // Field num:I
            22: return
          LineNumberTable:
            line 16: 0
            line 18: 8
            line 20: 12
            line 21: 22
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      23     0  this   Lcom/atguigu/java1/DynamicLinkingTest;
    
    • 往上面翻,找到常量池的定义:
      #7 = Methodref #8.#31
    
    • 先找 #8 :

      • #8 = Class #32 :去找 #32
      • #32 = Utf8 com/atguigu/java1/DynamicLinkingTest
      • 结论:通过 #8 我们找到了 DynamicLinkingTest 这个类
    • 再来找 #31:

      • #31 = NameAndType #19:#13 :去找 #19 和 #13
      • #19 = Utf8 methodA :方法名为 methodA
      • #13 = Utf8 ()V :方法没有形参,返回值为 void
    • 结论:通过 #7 我们就能找到需要调用的 methodA() 方法,并进行调用

    Constant pool:
       #1 = Methodref          #9.#23         // java/lang/Object."<init>":()V
       #2 = Fieldref           #8.#24         // com/atguigu/java1/DynamicLinkingTest.num:I
       #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
       #4 = String             #27            // methodA()....
       #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #6 = String             #30            // methodB()....
       #7 = Methodref          #8.#31         // com/atguigu/java1/DynamicLinkingTest.methodA:()V
       #8 = Class              #32            // com/atguigu/java1/DynamicLinkingTest
       #9 = Class              #33            // java/lang/Object
      #10 = Utf8               num
      #11 = Utf8               I
      #12 = Utf8               <init>
      #13 = Utf8               ()V
      #14 = Utf8               Code
      #15 = Utf8               LineNumberTable
      #16 = Utf8               LocalVariableTable
      #17 = Utf8               this
      #18 = Utf8               Lcom/atguigu/java1/DynamicLinkingTest;
      #19 = Utf8               methodA
      #20 = Utf8               methodB
      #21 = Utf8               SourceFile
      #22 = Utf8               DynamicLinkingTest.java
      #23 = NameAndType        #12:#13        // "<init>":()V
      #24 = NameAndType        #10:#11        // num:I
      #25 = Class              #34            // java/lang/System
      #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
      #27 = Utf8               methodA()....
      #28 = Class              #37            // java/io/PrintStream
      #29 = NameAndType        #38:#39        // println:(Ljava/lang/String;)V
      #30 = Utf8               methodB()....
      #31 = NameAndType        #19:#13        // methodA:()V
      #32 = Utf8               com/atguigu/java1/DynamicLinkingTest
      #33 = Utf8               java/lang/Object
      #34 = Utf8               java/lang/System
      #35 = Utf8               out
      #36 = Utf8               Ljava/io/PrintStream;
      #37 = Utf8               java/io/PrintStream
      #38 = Utf8               println
      #39 = Utf8               (Ljava/lang/String;)V
    
    • 在上面,其实还有很多符号引用,比如 Object、System、PrintStream 等等

    为什么要用常量池呢?

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

    8、解析和分派 (重要)

    8.1、静态链接与动态链接

    静态链接机制与动态链接机制:

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

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

    2. 动态链接

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

    8.2、早期绑定与晚期绑定

    方法的绑定机制:

    • invokevirtual 体现为晚期绑定
    • invokeinterface 也体现为晚期绑定
    • invokespecial 体现为早期绑定

    四条普通指令:

    • invokestatic:调用静态方法(非虚方法),解析阶段确定唯一方法版本
    • invokespecial:调用非虚方法(早起绑定<编译器可确定>)、私有及父类方法,解析阶段确定唯一方法版本
    • invokevirtual:调用虚方法(晚期绑定<运行期才能确定>)
    • invokeinterface:调用接口方法(晚期绑定)
    • 静态链接动态链接对应的方法的绑定机制为:早期绑定(Early Binding)晚期绑定(Late Binding)绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
    1. 早期绑定

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

    2. 晚期绑定

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

    代码示例

    • 代码
    /**
     * 说明早期绑定和晚期绑定的例子
     *
     * @author shkstart
     * @create 2020 上午 11:59
     */
    class Animal {
        public void eat() {
            System.out.println("动物进食");
        }
    }
    
    interface Huntable {
        void hunt();
    }
    
    class Dog extends Animal implements Huntable {
        @Override
        public void eat() {
            System.out.println("狗吃骨头");
        }
    
        @Override
        public void hunt() {
            System.out.println("捕食耗子,多管闲事");
        }
    }
    
    class Cat extends Animal implements Huntable {
        public Cat() {
            super();//表现为:早期绑定
        }
    
        public Cat(String name) {
            this();//表现为:早期绑定
        }
    
        @Override
        public void eat() {
            super.eat();//表现为:早期绑定
            System.out.println("猫吃鱼");
        }
    
        @Override
        public void hunt() {
            System.out.println("捕食耗子,天经地义");
        }
    }
    
    public class AnimalTest {
        public void showAnimal(Animal animal) {
            animal.eat();//表现为:晚期绑定
        }
    
        public void showHunt(Huntable h) {
            h.hunt();//表现为:晚期绑定
        }
    }
    
    • invokevirtual 体现为晚期绑定

    image-20200728150755367

    • invokeinterface 也体现为晚期绑定

    image-20200728150903827

    • invokespecial 体现为早期绑定

    image-20200728150955826

    8.3、多态性方法绑定

    多态性与方法绑定机制:

    1. 随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定晚期绑定两种绑定方式。
    2. Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

    虚方法与非虚方法

    虚方法与非虚方法的区别

    • 如果方法编译期确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。----> 对应早期绑定
      • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
    • 其他方法称为虚方法。----> 对应晚期绑定

    子类对象的多态的使用前提:

    • 类的继承关系
    • 方法的重写

    虚拟机中调用方法的指令:

    四条普通指令

    1. invokestatic:调用静态方法(非虚方法),解析阶段确定唯一方法版本
    2. invokespecial:调用非虚方法(早起绑定<编译器可确定>)、私有及父类方法,解析阶段确定唯一方法版本
    3. invokevirtual:调用虚方法(晚期绑定<运行期才能确定>)
    4. invokeinterface:调用接口方法(晚期绑定)

    一条动态调用指令

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

    区别:

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

    代码示例:

    • 代码
    /**
     * 解析调用中非虚方法、虚方法的测试
     *
     * invokestatic指令和invokespecial指令调用的方法称为非虚方法
     * @author shkstart
     * @create 2020 下午 12:07
     */
    class Father {
        public Father() {
            System.out.println("father的构造器");
        }
    
        public static void showStatic(String str) {
            System.out.println("father " + str);
        }
    
        public final void showFinal() {
            System.out.println("father show final");
        }
    
        public void showCommon() {
            System.out.println("father 普通方法");
        }
    }
    
    public class Son extends Father {
        public Son() {
            //invokespecial
            super();
        }
    
        public Son(int age) {
            //invokespecial
            this();
        }
    
        //不是重写的父类的静态方法,因为静态方法不能被重写!
        public static void showStatic(String str) {
            System.out.println("son " + str);
        }
    
        private void showPrivate(String str) {
            System.out.println("son private" + str);
        }
    
        public void show() {
            //invokestatic
            showStatic("atguigu.com");
    
            //invokestatic
            super.showStatic("good!");
    
            //invokespecial
            showPrivate("hello!");
    
            //invokevirtual
            //虽然字节码指令中显示为invokevirtual,但因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
            showFinal();
    
            //invokespecial
            super.showCommon();
    
            //invokevirtual
            //有可能子类会重写父类的showCommon()方法
            showCommon();
            info();
    
            MethodInterface in = null;
            //invokeinterface
            in.methodA();
        }
    
        public void info() {
    
        }
    
        public void display(Father f) {
            f.showCommon();
        }
    
        public static void main(String[] args) {
            Son so = new Son();
            so.show();
        }
    }
    
    interface MethodInterface {
        void methodA();
    }
    
    • Son 类中 show() 方法的字节码指令如下

    image-20200728152731214


    关于 invokedynamic 指令

    1. JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进。
    2. 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
    3. Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。

    代码示例

    • 代码
    @FunctionalInterface
    interface Func {
        public boolean func(String str);
    }
    
    public class Lambda {
        public void lambda(Func func) {
            return;
        }
    
        public static void main(String[] args) {
            Lambda lambda = new Lambda();
    
            Func func = s -> {
                return true;
            };
    
            lambda.lambda(func);
    
            lambda.lambda(s -> {
                return true;
            });
        }
    }
    
    • 字节码指令

    image-20200728154236568

    8.4、方法重写的本质 (重点)

    动态语言和静态语言:

    1. 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期满足前者就是静态类型语言,反之是动态类型语言。
    • 静态类型语言 : int a = 20, 在编译期就确定了a的类型为整形。
    • 动态类型语言 : var b = “20”, 在运行期才能确定b的类型为字符串

    方法重写的本质:

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

    1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。

    2. 如果在类型C中找到与常量中的描述符合简单名称都相符的方法, 则进行访问权限校验

      • 如果通过则返回这个方法的直接引用,查找过程结束
      • 如果不通过,则返回java.1ang.IllegalAccessError 异常
    3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程

    4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

    IllegalAccessError介绍

    1. 程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。
    2. 一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
    3. 比如,你把应该有的jar包放从工程中拿走了,或者Maven中存在jar包冲突

    回看解析阶段

    1. 解析阶段就是将常量池内的符号引用转换为直接引用的过程
    2. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

    8.5、多态与虚方法表

    浅谈动态分派和静态分派

    在Java多态的两种常见用法中,方法重载使用的是静态分派机制,而方法重写使用的是动态分派机制。 这也就导致了,方法重载调用的时候是根据变量的静态类型来决定调用哪个方法。而方法重写的时候,则是根据变量的实际类型来决定调用哪个方法。

    虚方法表

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

    image-20200706144954070


    9、方法返回地址

    方法返回地址(return address)

    1. 存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
      • 正常执行完成
      • 出现未处理的异常,非正常退出
    2. 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
    3. 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
    4. 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

    方法退出的两种方式

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

    正常退出:

    1. 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
    2. 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。
    3. 在字节码指令中,返回指令包含:
      1. ireturn:当返回值是boolean,byte,char,short和int类型时使用
      2. lreturn:Long类型
      3. freturn:Float类型
      4. dreturn:Double类型
      5. areturn:引用类型
      6. return:返回值类型为void的方法、实例初始化方法、类和接口的初始化方法

    异常退出:

    1. 在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
    2. 方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码

    image-20200706154554604

    代码举例

    • 代码
    public class ReturnAddressTest {
        public boolean methodBoolean() {
            return false;
        }
    
        public byte methodByte() {
            return 0;
        }
    
        public short methodShort() {
            return 0;
        }
    
        public char methodChar() {
            return 'a';
        }
    
        public int methodInt() {
            return 0;
        }
    
        public long methodLong() {
            return 0L;
        }
    
        public float methodFloat() {
            return 0.0f;
        }
    
        public double methodDouble() {
            return 0.0;
        }
    
        public String methodString() {
            return null;
        }
    
        public Date methodDate() {
            return null;
        }
    
        public void methodVoid() {
    
        }
    
        static {
            int i = 10;
        }
    
        public void method2() {
            methodVoid();
            try {
                method1();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public void method1() throws IOException {
            FileReader fis = new FileReader("atguigu.txt");
            char[] cBuffer = new char[1024];
            int len;
            while ((len = fis.read(cBuffer)) != -1) {
                String str = new String(cBuffer, 0, len);
                System.out.println(str);
            }
            fis.close();
        }
    }
    
    • 方法正常返回

      • ireturn

        image-20200728184852735

      • dreturn

        image-20200728184921891

      • areturn

        image-20200728184954040

    • 异常处理表:

      • 反编译字节码文件,可得到 Exception table
      • from :字节码指令起始地址
      • to :字节码指令结束地址
      • target :出现异常跳转至地址为 11 的指令执行
      • type :捕获异常的类型

    image-20200728185306587

    10、一些附加信息

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

    11、栈相关面试题 (关注)

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

    • 栈空间分配的内存太少
    • 通过-Xss 设置栈的大小

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

    • 不能保证不溢出

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

    • 不是,一定时间内降低了StackOverFlowError概率,但是会挤占其它的线程空间,因为整个虚拟机的内存空间是有限的, 当其他的线程分配栈内存的时候, 发现内存不足, 就会报OutofMenaryError

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

    • 不涉及, 虚拟机栈只会发生栈溢出, 栈管运行, 堆管存储, 垃圾回收一般只针对于堆空间

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

    何为线程安全?

    1. 如果只有一个线程才可以操作此数据,则必是线程安全的。
    2. 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。

    具体问题具体分析:

    • 如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的

      • 直接在方法中销毁掉对象的引用, 此时就是安全的.
      • 当通过参数, 返回值, 将对象的引用返回到了外部, 此时就是线程不安全的。
    • 看代码

    /**
     * 面试题:
     * 方法中定义的局部变量是否线程安全?具体情况具体分析
     *
     *   何为线程安全?
     *      如果只有一个线程才可以操作此数据,则必是线程安全的。
     *      如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
     * @author shkstart
     * @create 2020 下午 7:48
     */
    public class StringBuilderTest {
        //s1的声明方式是线程安全的
        public static void method1(){
            //StringBuilder:线程不安全
            StringBuilder s1 = new StringBuilder();
            s1.append("a");
            s1.append("b");
            //...
        }
        //sBuilder通过参数传递方法内,存在线程不安全的问题
        public static void method2(StringBuilder sBuilder){
            sBuilder.append("a");
            sBuilder.append("b");
            //...
        }
        //操作s1之后,将s1作为返回值返回,存在线程不安全的问题
        public static StringBuilder method3(){
            StringBuilder s1 = new StringBuilder();
            s1.append("a");
            s1.append("b");
            return s1;
        }
        //s1的操作:是线程安全的
        public static String method4(){
            StringBuilder s1 = new StringBuilder();
            s1.append("a");
            s1.append("b");
            return s1.toString();
        }
    
        public static void main(String[] args) {
            StringBuilder s = new StringBuilder();
            new Thread(() -> {
                s.append("a");
                s.append("b");
            }).start();
            method2(s);
        }
    }
    

    运行时数据区,哪些部分存在Error和GC?

    运行时数据区是否存在Error是否存在GC
    程序计数器
    虚拟机栈是(StackOverFlowError)
    本地方法栈
    方法区是(OOM)
    是(OOM)
    展开全文
  • 局部变量和成员变量变量的分类※操作数1.操作数概述2.栈顶缓存技术先看n-TOS caching再看n-state TOS caching3.i++和++i问题※动态链接※方法返回地址三、方法的调用1.早起绑定和晚期绑定⭕️早期绑定⭕️晚期...
      
    


    前言

    语雀地址:https://www.yuque.com/yangxiaofei-vquku/wmp1zm/mggrlr


    一、虚拟机栈的概述

    1、虚拟机栈出现的背景

    由于jvm跨平台的设计,java的指令都是基于栈来设计的。不同平台的CUP架构不同,所以不能设计为基于寄存器的。基于栈的指令级架构相对于基于寄存器的指令集架构优点是跨平台,指令集小,编译器容易实现,每个指令都是相对于栈顶栈帧和栈顶栈帧操作数栈的操作,缺点是性能下降,实现同样的功能需要更多指令(jvm的指令集架构)。

    2、虚拟机栈的概述

    • 栈是线程私有的运行时数据区,在jvm中每个线程都会包涵一个虚拟机栈
    • 每调用一个方法就在栈中创建一个栈桢,标准的栈结构,先进后出,方法执行时压栈,方法结束后出栈
    • 每个栈桢中包涵局部变量表、操作数占、动态链接、方法返回地址
    • 栈是一种快速有效的分配储存方式,访问速度仅次于程序计数器
    • 对于栈来说不存在垃圾回收问题,因为栈桢会随着方法的结束出栈并销毁

    3、虚拟机栈中的常见异常

    由于java虚拟机规范允许java栈的大小是动态扩展的或者固定不变的所以除了StackOverflowError之外栈中也可能出现OutOfMemoryError。

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

    二、栈的存储单位

    栈中的存储单位是栈桢,在这个线程上正在执行的每个方法都各自对应一个栈桢。

    • 在一条活动的线程中,一个时间点上,只会有一个活动的栈桢,即只有当前正在执行的方法的栈桢是有效的,这个栈桢被称为当前栈桢,与当前栈桢对应的方法称为当前方法,定义这个方法的类被称为当前类
    • 程序计数器保存和执行引擎运行的所有字节码指令都是只针对当前栈桢
    • 如果在当前方法里调用了其他方法,对应的新的栈桢会被创建出来放在栈顶,称为新的当前栈桢
    • 不同线程中包含的栈桢是不允许相互引用的,即不能从一个栈桢中引用另一个线程中的栈桢
    • 如果当前方法调用了其他方法,方法返回之际,当前栈桢会传回此方法的执行结果给前一个栈桢,接着,虚拟机会丢弃当前栈桢,使得前一个栈桢重新成为当前栈桢
    • Java方法有两种返回函数的方式,一种是使用return指令正常返回(void方法也会默认执行return指令);另外一种是抛出异常。不管使用哪种方式,都会导致方法结束栈桢被弹出
      在这里插入图片描述

    ※局部变量表

    1.局部变量表概况

    • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用(reference),以及returnAddress类型。(基础数据类型正常保存,boolean、char等转换成对于数字,引用类型保存地址值也是二进制数字)
    • 局部变量表是建立在线程栈上的,是线程私有数据,因此不会存在数据安全问题
    • 局部变量表所需的容量大小是在编译期间就确定下来的,并保存在方法的Code属性maxmun local variables中,在方法运行期间是不会改变局部变量表大小的。
      在这里插入图片描述

    用jclasslib查看更明显
    在这里插入图片描述
    在这里插入图片描述

    • 方法嵌套的次数受栈的大小的限制(-Xss可配置),一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,局部变量表就膨胀,它的栈桢就越大,进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。(所以在栈大小固定的情况下适当减少局部变量以减小栈桢大小可以增加栈的深度)
    • 局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着栈桢的销毁,局部变量表也会随之销毁
    • 局部变量表占用了一个栈桢的绝大部分空间
    • 局部变量表中的变量时垃圾回收的根节点GCROOT,只有被任意栈桢的局部变量表直接或间接引用的对象都不会被回收

    2. 变量槽Slot

    • 变量槽Slot是局部变量表的基本存储单位,在局部变量表里32位以内(小于4字节)的类型值占用一个slot包括(byte、short、int、float、char、boolean、引用类型指针reference),64位的(8字节)占用两个slot(long和double)
    • 当一个实例方法被调用时候,他的方法参数和方法体内部定义的局部变量都会按照顺序被赋值到局部变量表的没一个slot上
    • 如果需要访问局部变量表中一个64bit的局部变量值是只需要使用前一个索引即可,例如:一个double类型变量a占用了变量槽4、5,在获取是该变量时访问变量槽索引4即可
    • 如果是非静态方法,那该方法栈桢的局部变量表index为0的solt会存放该对象的引用this

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    • 局部变量表中的变量槽是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很可能会复用过期局部变量的槽位,从而达到节省资源的目的(一个方法里并不是局部变量多就一定局部变量表大,槽位可以重复利用,例如:if、for等语法块中声明的变量,作用域仅在当前代码块并不是整个方法)。
      在这里插入图片描述

    3.局部变量和成员变量

    变量的分类

    变量按照类型分:基本数据类型变量、引用数据类型变量
    变量按照类中位置分:成员变量(类变量、实例变量)、局部变量
    这里着重对比成员变量和局部变量的区别:
    成员变量的类变量在类装载的链接-准备进行赋默认值,实例变量在对象创建时会进行赋默认值,所以不用进行显式赋值也可以使用,但是成员变量不会被赋默认值,所以声明时必须显式赋值才可以使用。

    ※操作数栈

    1.操作数栈概述

    操作数栈,主要用于保存计算过程的临时变量和中间结果
    操作数栈的深度在编译器即可确定,保存在方法的code属性中,为max_stack的值

    在这里插入图片描述

    在这里插入图片描述

    • 操作数栈中一个槽位也是4字节32bit,所以同局部变量表一样byte、short、int、float、char、boolean、引用类型指针reference占用一个栈深度,double和long占用两个栈深度
    • 操作数栈是栈结构不能像局部变量表一样通过下表索引访问数据,只能通过入栈push和出栈pop操作来完成数据访问
    • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈桢的操作数栈
      首先store_n命令的意思是指将操作数类型为的栈顶元素出栈并放入局部变量表的第n个变量槽中,pop是将操作数栈的栈顶元素出栈销毁。
      method1()字节码:整个过程操作数栈最大深度为1
      在这里插入图片描述

    0 sipush 1024 将1024压入操作数栈顶,此时操作数栈中只有1024
    3 istore_1 操作数栈顶元素出栈放入局部变量表第1个变量槽,此时操作数栈为空

    4 aload_0 将局部变量表0号变量槽的元素压如操作数栈顶,此时操作数栈中只有个this(0号位置是this,压栈目的是为了调用this.returnInt)

    5 invokevirtual #2 调用this.returnInt方法并将操作数栈中的this出栈,把returnInt的返回值10压如操作数栈顶,此时操作数栈中只有10

    8 istore_2 将操作数栈的栈顶元素出栈并放入局部变量表的2号槽位,此时操作数栈为空

    9 return 返回结果方法当前栈桢被弹出销毁

    method2字节码:

    在这里插入图片描述

    0 sipush 1024 将1024压入操作数栈顶,此时操作数栈中只有1024

    3 istore_1 操作数栈顶元素出栈放入局部变量表第1个变量槽,此时操作数栈为空

    4 aload_0 将局部变量表0号变量槽的元素压如操作数栈顶,此时操作数栈中只有个this(0号位置是this,压栈目的是为了调用this.returnInt)

    5 invokevirtual #2 调用this.returnInt方法并将操作数栈中的this出栈,把returnInt的返回值10压如操作数栈顶,此时操作数栈中只有10

    8 pop 将操作数栈的栈顶元素出栈销毁

    9 return 返回结果方法当前栈桢被弹出销毁
    由上图method1和method2可以看出调用又返回值的方法时会将方法的返回值压如当前栈桢的操作数栈

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

    2.栈顶缓存技术

    基于栈式的架构的虚拟机所使用的零地址指令更加紧凑,单完成一项操作的时候必然需要使用更多的入栈和出栈指令,虚拟机栈也是存在于内存中,这就意味着将需要更多的指令分派次数和内存读/写次数,频繁的执行内存读/写操作必然会影响执行速度,为了解决这一问题HotSpot JVM的设计者们提出了栈顶缓存(Top-of-Stack-Caching)技术,将栈顶元素(或栈顶周边)元素缓存到物理CUP的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
    有关栈顶缓存技术需要关注两个核心问题:

    • 缓存了栈顶附近的多少个元素?如果缓存了n个元素,那么就叫n-TOS caching;
    • 缓存带有多少种“状态”?如果有n种状态那么就叫n-state TOS caching。
    先看n-TOS caching

    从抽象数据结构来举例操作数栈的实现:可以想像把Java标准库自带的那个java.util.Stack包装一下,假如实现栈顶缓存技术的逻辑如下:压如操作数栈的栈顶元素优先存到CUP的寄存器中。(Stack为原始操作数栈,StackWith1TOSCA为支持栈顶缓存的操作数栈)

    import java.util.EmptyStackException;  
    import java.util.Stack;  
      
    public class StackWith1TOSCA<E> {  
      
      private enum TosState {  
        NOT_CACHED,  
        CACHED;  
      }  
      // 内存中的操作数栈(内存)
      private Stack<E> theStack = new Stack<E>();  
      // 栈顶元素(寄存器)
      private E        topOfStackElement; // the cache  
      // 是否有缓存,在1-TOS中即表示栈顶是否有数据
      private TosState state = TosState.NOT_CACHED;  
       
       // 向操作数栈中压如元素
      public void push(E elem) { 
          // 如果栈顶已有元素,
        if (state == TosState.CACHED) { 
            // 将栈顶元素压入内存中真正的操作数栈
          theStack.push(topOfStackElement);  
        }  
          // 将寄存器中栈顶数据替换为新压人的栈顶元素
        topOfStackElement = elem; 
          // 设置state为CACHE代表栈顶有元素
        state = TosState.CACHED;  
      }  
       
        // 操作数栈的栈顶元素出栈
      public E pop() {  
          // 如果状态为NOT_CACHE也就是无栈顶元素,此时操作数栈为空无法出栈
        if (state == TosState.NOT_CACHED) throw new EmptyStackException();  
         // 将CPU寄存器中的栈顶元素取出 
        E result = topOfStackElement;  
          // 内存中的栈桢操作数栈是否为空
        if (theStack.isEmpty()) { 
            // 此时操作数栈为空,栈顶元素弹出为空
          state = TosState.NOT_CACHED;  
          topOfStackElement = null;  
        } else { 
            // 将内存中操作数栈的栈顶元素去除放入寄存器
          topOfStackElement = theStack.pop();  
        }  
          
        return result;  
      }  
    }  
    

    那么如果有这样的Java代码:

    static void foo(Object o) {  
      Object temp = o;  
    }
    

    对于字节码指令为
    在这里插入图片描述

    用不支持栈顶缓存的操作数栈Stack执行上面字节码指令,执行了2次内存读取两次内存写入,jvm操作为

    // 从局部变量表读取写入操作数栈——》一次内存读取,一次内存写入
    stack.push(locals[0]);  
    // 从操作数栈中读取写入局部变量表——》一次内存读取,一次内存写入
    locals[1] = stack.pop(); 
    

    用支持栈顶缓存技术的操作数栈StackWith1TOSCA执行上面字节码指令,执行了一次内存读取,一次寄存器写入,一次寄存器读取,一次内存写入,jvm操作为

    // 从局部变量表读取,写入寄存器——》一次内存读取,一次寄存器写入
    topOfStackElement = locals[0]; 
    // 从寄存器读取,写入局部变量表——》一次寄存器读取,一次内存写入
    locals[1] = topOfStackElement;
    

    从上述例子可以看出将栈顶元素缓存寄存器有效的减少了内存的读写次数,而如果选择把栈顶附近的若干个元素缓存在寄存器里的话,在频繁的计算逻辑中将大幅度提升性能。

    再看n-state TOS caching

    前面的StackWith1TOSCA例子里可以看到已经有“state”的概念出现:我们必须要知道当前在缓存里到底有没有值,不然就无从判断压栈/出栈时数据该何去何从了。这个例子用了两种状态,NOT_CACHED和CACHED;对于不关心栈里元素类型的stack caching来说,1-TOS用两种状态就够用了。
    实际上“状态”可以记录许多东西,取决于到底要实现怎样的TOSCA。
    一个例子:如果我们现在不用1-TOS,而用3-TOS caching的话,那很明显我们的“状态”不但要记录“有没有缓存栈顶元素”,还得记录“到底栈顶附近的三个元素到底放在哪个变量里了”。
    另一个例子:如果我们的栈需要跟踪栈里的元素的类型,同时我们使用1-TOS caching的话,那就意味着要记录的“状态”里必须记住栈顶元素是什么类型的。HotSpot VM的解释器就是这样的例子,它虽然只用了1-TOS caching,但它的TosState却有9种有效值。也就是说这个解释器的TOSCA可以描述为1-TOS, 9-state caching。
    大家可以想像一个n > 1的n-TOS如果跟带类型的TOSCA结合起来状态数量的膨胀速度会有多快。
    实际上多数虚拟机就算用了stack caching也只会用1-TOS,因为简单高效;大不了1-TOS外带类型。
    也有复杂一些的例子,例如Sun JDK 1.1.x里的解释器在x86上的实现,它用的是2-TOS, 3-state caching。

    3.i++和++i问题

    public static void method3(){
            int i=100;
            i=i++;
            System.out.println(i);//100
    }
    

    上面一段代码输出值为100,这个很令人费解,下面将在字节码指令层面分析输出100的原因
    在这里插入图片描述

    0 bipush 100 将100压入操作数栈栈顶

    2 istore_0 将操作数栈栈顶元素取出放入局部变量表0号变量槽,此时操作数栈为空

    3 iload_0 将局部变量表0号槽位元素压入操作数栈栈顶

    4 iinc 0 by 1 将局部变量表0号槽位元素加1,此时局部变量表为101

    7 istore_0 将操作数栈栈顶元素取出放入局部变量表0号变量槽,此时操作数栈为空(这步很关键,覆盖率原先的101)

    所以最终打印的i=100。

    ※动态链接

    动态链接保存的是:当前类运行时常量池引用
    在这里插入图片描述

    如图所示栈桢中保存了当前方法的字节码指令,只要用到常量池的指令后面都会带有#n,这是在编译时确定的,与字节码中常量池相对应,字节码常量池在类加载后变成运行时常量池保存在方法区。当该栈桢运行这些字节码时就需要访问当前类的常量池,在动态链接中存储的就是当前类的运行时常量池的引用,在指令首次执行时常量池中保存的还是符合引用,需要被转换成符合引用对于的直接引用。

    ※方法返回地址

    存放调用该方法的程序计数器的值,即当前栈桢的上一个栈桢调用当前栈桢的那行指令的地址。
    作用就是当方法正常执行结束时,能够顺利回到上一个方法调用该方法的位置继续执行。如果是异常退出返回地址要通过异常表来确定,栈桢中不会保存这部分信息。

    三、方法的调用

    1.早起绑定和晚期绑定

    ⭕️早期绑定

    早期绑定就是指被调用的目标方法如果在编译器可知,且运行期保持不变时,即可将这个方法与其所属的类型进行绑定,早期绑定的方法也被称为非虚方法。
    早期绑定的好处是编译期触发,能够提早知道代码错误、提高程序运行效率。

    ⭕️晚期绑定

    如果被调用的方法在编译期无法被确定下来,只能够在程序运行期间确定实际的类型,这种绑定方式被称为晚期绑定,晚期绑定的方法都是虚方法。
    晚期绑定的好处是,晚期绑定是多态是设计模式的基础,能够降低耦合性,提升程序的复用性。

    2.虚方法和非虚方法

    ⭕️非虚方法&虚方法定义

    静态方法、私有方法、final方法、构造方法、父类方法都是非虚方法,其中前四种是无法被子类及其派生类重写的,父类方法在由于java只能允许单继承所以这五种方法在编译器就能确定具体的调用版本,且在运行期保持不变。其余方法均为虚方法。

    ⭕️方法调用字节码指令

    1. invokestatic:调用静态方法,编译阶段确定唯一方法版本.
    2. invokespecial:构造方法、私有方法、父类方法,编译阶段确定唯一方法版本
    3. invokevirtual :调用所有虚方法(除去final修饰的方法)
    4. invokeinterface:调用接口方法
      invokestatic、invokespecial再加上final修饰的invokevirtual为字节码指令代表调用非虚方法;invokevirtual(除去final修饰的方法)、invokeinterface代表调用虚方法
    package jvm.memory.stack;
    
    /**
     * 方法调用:前期绑定后期绑定,虚函数非虚函数指令
     */
    public class MethodCall {
    
        public static class Father{
            /**
             * 静态方法 非虚方法
             */
            public static void staticMethod(){
    
            }
    
            /**
             * 常量方法 非虚方法
             */
            public final void finalMethod(){
    
            }
    
            /**
             * 正常方法可以被继承重新,虚方法
             */
            public void method(){
    
            }
    
            /**
             * 私有方法,非虚方法
             */
            private void privateMethod(){
    
            }
    
    
        }
    
    
    
        public static class Son extends Father{
    
            public Son() {
                // 非虚方法
                super();
            }
    
            public Son(String name){
                // 非虚方法
                this();
            }
    
            /**
             * 正常方法可以被继承重新,虚方法
             * 子类已经重新
             */
            public void method(){
                System.out.println("子类重写");
            }
    
            /**
             * 私有方法,非虚方法
             */
            private void privateMethod(){
    
            }
    
    
            public void demo(){
                // 非虚
                staticMethod();// invokestatic
                finalMethod();// invokevirtual
                privateMethod();// invokespecial
                // 虚
                method();// invokevirtual
    
    
            }
    
        }
    
    
    
    }
    

    在这里插入图片描述

    还有一个invokedynamic的动态调用指令,代表动态解析出需要调用的方法然后执行,在JDK8之前需要ASM这种字节码工具才能触发,JDK8以后在调用Lambda表达式会触发

    package jvm.memory.stack;
    
    /**
     * Invokedynamic 动态调用指令
     */
    public class InvokedynamicDemo {
    
        private String name;
    
        public InvokedynamicDemo(String name){
            this.name=name;
        }
    
        public void method(Fun fun){
            System.out.println(fun.execute(name));
        }
    
    public interface Fun{
        public String execute(String name);
    }
    
    
        public static void main(String[] args) {
            InvokedynamicDemo invokedynamic=new InvokedynamicDemo("小明");
    
            // invokedynamic
            Fun fun=name -> {return name+"来自与一个函数接口0";};
    
            // fun1声明没用到函数表达式所以不是invokedynamic
            Fun fun1=new Fun() {
                @Override
                public String execute(String name) {
                    return name+"来自与一个函数接口1";
                }
            };
    
            invokedynamic.method(fun);
            invokedynamic.method(fun1);
            // invokedynamic
            invokedynamic.method(name -> {
                return name+"来自与一个函数接口2";
            });
    
        }
    
    
    
    
    }
    
    
    最后输出结果:
    小明来自与一个函数接口0
    小明来自与一个函数接口1
    小明来自与一个函数接口2
    

    在这里插入图片描述

    3.虚方法表

    ⭕️为什么要有虚方法表

    要想了解为什么需要虚方法表需要先明确虚方法调用时需要做那些事情,由于虚方法时运行被重写的再多态的情景下,只有运行时才能确认被调用方法是父类定义还是子类定义的。在方法调用前首先把调用方法的对象实例压入栈顶,具体流程如下:

    • 找到操作数栈顶的第一个元素记作类型C
    • 如果在C类型中找到与该方法名称与方法签名都符合且访问权限允许的方法则直接返回该方法直接引用,结束查找
    • 如果为找到,则按照继承关系依次查找C的各个父类,有则返回
    • 如果始终没找到则抛出java.lang.AbstractMethodError
      在面向对象的开发中会频繁使用虚方法的调用,如果每次调用都这样查找一遍势必影响性能,为了提高性能,JVM为每一个类在解析阶段在方法区中创建一个虚方法表,记录了类中各虚方法真实的直接引用。(不包括非虚方法,因为非虚方法编译期间即可明确方法版本归属)

    ⭕️虚方法表举例

    package jvm.memory.stack;
    
    /**
     * 虚方法表
     */
    public class VirtualMethodTable {
    
        public  class Person{
    
            public void love(){
                System.out.println("爱好世界和平");
            }
    
            @Override
            public String toString() {
                return "我是人类";
            }
        }
    
        public  class Father extends Person{
    
            @Override
            public void love() {
                System.out.println("爱好读书");
            }
    
    
            public void work(){
                System.out.println("努力工作");
            }
    
        }
    
    
    
        public  class Son extends Father{
    
            @Override
            public void love() {
                System.out.println("爱好编程");
            }
    
    
            @Override
            public void work() {
                System.out.println("努力学习");
            }
    
        }
    
    
    
    }
    

    在这里插入图片描述

    展开全文
  • 关于虚拟机的理解

    2020-11-04 22:01:37
    由于平台性的设计,Java的指令都是根据来设计的. 不同平台CPU架构不同,所以不能设置为基于寄存器的 优点是平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令. 有不少Java开发人员一...

    虚拟机栈

    虚拟机栈出现的背景

    由于跨平台性的设计,Java的指令都是根据栈来设计的. 不同平台CPU架构不同,所以不能设置为基于寄存器的

    优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令.


    有不少Java开发人员一提到Java内存结构,就会非常粗粒度地将JVM中的内存区理解为仅有Java堆(Heap)和Java栈(stack)? 为什么

    内存中的栈与堆

    栈是运行时的单位,而堆是存储的单位
    即: 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据.
    堆: 堆解决的是数据的存储的问题,即数据怎么放,放在哪儿.


    比喻

    java 虚拟机栈是什么?

    java虚拟机栈(java virtual machine stack)早期,也叫java栈
    每个线程在创建是都会创建一个虚拟机栈, 其 内部保存一个个的栈桢(stack Frame)对应着一次次的Java方法调用.

    是线程私有的

    声明周期

    生命周期和线程一致.
    

    作用

    • 主管java程序的运行,它保存的方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回.
    • 局部变量 vs 成员变量(或属性)
    • 基本数据变量 vs 引用类型变量(类,数组,接口)

    实例

    package com.atguigu.java5;
    
    /**
     * ClassName: StackTest <br/>
     * Description: StackTest <br/>
     * Date: 2020-10-23 10:36 <br/>
     * <br/>
     *
     * @author 
     * @version 产品版本信息 2020年10月23日10:36分 新建<br/>
     * <p>
     * 修改记录
     * @email 
     * @project study_note_01
     * @package com.atguigu.java5
     */
    public class StackTest {
        public static void main(String[] args) {
            StackTest test = new StackTest();
            test.methodA();
        }
        public void methodA(){
            int i = 10;
            int j = 20;
            methodB();
        }
        public void methodB(){
            int k = 30;
            int m = 40;
        }
    }
    
    

    分析一波

    栈的特点(优点)

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

    栈: 先入后出 子弹夹 FILO

    队列: 先入先出 FIFO

    面试题: 开发中遇到的异常有哪些

    栈可能出现的异常

    • java 虚拟机规范允许java栈的大小是动态的或者是固定不变的
      • 如果采用固定大小的java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定.如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常.
      • 如果Java虚拟机可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存区创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常

    设置内存中的栈的大小

    package com.atguigu.java5;
    
    /**
     * ClassName: StackErrorTest <br/>
     * Description: StackErrorTest <br/>
     * Date: 2020-10-23 11:01 <br/>
     * <br/>
     *
     * @author yufengming
     * @version 产品版本信息 2020年10月23日11:01分 
     *  新建<br/>
     * <p>
     * 修改记录
     * @email 
     * @project study_note_01
     * @package com.atguigu.java5
     * 默认情况下: count:11420
     * 设置栈的大小: -Xss 256k:
     */
    public class StackErrorTest {
        private static int count = 1;
        public static void main(String[] args) {
            System.out.println(count);
            count++;
            main(args);
        }
    }
    
    

    11410
    11411
    11412
    Exception in thread "main" java.lang.StackOverflowError
        at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
        at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
        at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
        at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
        at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
        at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
        at java.io.PrintStream.write(PrintStream.java:526)
        at java.io.PrintStream.print(PrintStream.java:597)
        at java.io.PrintStream.println(PrintStream.java:736)
        at com.atguigu.java5.StackErrorTest.main(StackErrorTest.java:20)
    

    栈的存储单位

    栈中存储什么?

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

    复习

    • OOP的基本概念: 类,对象
    • 类中基本结构: field(属性,字段,域),method

    栈运行原理

    • JVM 直接对Java栈的操作只有两个,就是对栈桢的压栈出栈,遵循"先进后出"/"后进先出"原则
    • 在一条活动线程中,一个时间点上,只会有一个活动的栈桢.即只有当前正在执行的方法的栈桢(栈顶栈桢)是有效的,这个栈桢被称为当前栈桢(current frame),与当前栈桢相对应的方法就是当前方法(current method),定义这个方法的类就是当前类(current class)
    • 执行引擎运行的所有字节码指令只针对当前栈桢进行操作
    • 如果在该方法中调用了其他方法,对应的新的栈桢会被创建出来,放在栈的顶端,成为新的当前帧.

    当前栈桢,一直在移动

    依次执行

    • 不同线程中所包含的栈桢是不允许存在相互引用的,即不可能在一个栈桢之中引用另外一个线程的栈桢.
    • 如果当前方法调用了其他方法,方法返回之际,当前栈桢会传回此方法的执行结果给前一个栈桢,接着,虚拟机会丢弃当前栈桢,使得前一个栈桢重新成为当前栈桢.
    • Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常.不管使用哪种方式,都会导致栈桢被弹出.

    栈桢内部结构

    在虚拟机这块儿,有错误的帖子很多,你不能全信

    垃圾回收算法

    局部变量表

    • 局部变量表也被称之为局部变量数组或本地变量表
    • **定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,**这些数据类型包括各类基本数据类型,对象引用(reference),以及returnAddress类型.
    • 由于局部变量表示建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
    • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法Code属性的maximum local variables 数据项中.在方法运行期间是不会改变局部变量表的大小的.

    javap 命令 解析,这个idea具有反编译功能

    idea中jclasslib 插件

    • 方法嵌套调用的次数由栈的大小决定.一般来说,**栈越大,方法嵌套调用次数越多.**对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈桢就越大,以满足方法调用所需传递的信息增大的需求.进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少.
    • **局部变量表中的
    • **.在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程.**当方法调用结束后,随着方法栈桢的销毁,局部变量表也会随之销毁.

    关于Slot的理解

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

    • 局部变量表,最基本的存储单元是Slot(变量槽)

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

    • 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot.

    • byte,short,char 在存储前被转换为int,boolean也被转换为int,0 表示 false,非0 表示 true.

    • long和double 则占据两个Slot.

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

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

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

    • 如果当前帧是由构造方法或者实例方法创建的,name该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列

    4分钟

    构造器中的this,表示当前创建的变量


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

    变量的分类

    按照数据类型:

    1. 基本数据类型
    2. 引用数据类型

    按照在类中声明的位置:

    1. 成员变量: 在使用前都经历过初始化赋值
    • 使用静态来修饰的(类变量):
      • linking的prepare阶段:给类变量默认赋值 —>
      • initial阶段: 给类变量显示赋值即静态代码
    • 实例变量(归具体的对象所有)
      :随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
    1. 局部变量: 在使用前,比如要进行显示赋值,否则,编译不通过
    public void test5Temp(){
        int num;
        System.out.println(num);
        // 错误信息,变量num没有进行赋值(未初始化错误)
    }
    

    补充说明

    • 在栈桢当中,与性能调优关系最为密切的部分就是前面提到的局部变量表.
      在方法的执行时,虚拟机使用局部变量表完成方法的传递.
    • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收.

    在静态的方法当中是不可以引用这个this的

    Slot的重复利用

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

    52 操作数栈(Operate Stack)

    这个是数据结构中的一种,先进后出的特点

    栈: 可以通过数组或链表来实现
    栈可以看成满足特殊条件的数组或者链表.


    • 每一个独立的栈桢当中除了包含局部变量表以外,还包含一个后进献出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)

    • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop).

      • 某些字节码指令将值亚入操作数栈,其余的字节码指令将操作数取出栈.使用它们后再把结果压入栈.
      • 比如:执行复制交换,求和等操作.
    • 操组数栈,主要用于保存计算过程的中间计算结果,同时作为计算过程中变量临时的存储空间

    • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈桢也会随之被创建出来,这个方法的操作数栈是空的

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

    • 栈中的任何一个元素都是可以任意Java数据类型

      • 32bit的类型占用一个栈单位深度
      • 64bit的类型占用两个栈单位深度
    • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问.

    • 如果被调用的方法带有返回值的话,其返回值将会被压入当期栈桢的操作数栈中,并更新PC寄存器中吓一跳需要执行的字节码指令.

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

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

    动态链接(或指向运行时常量池的方法引用)

    动态链接


    方法的调用

    在java虚拟机当中,将符号引用转换为调用方法的直接引用于方法的绑定机制相关

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

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

    方法的调用

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

    • 早期绑定:
      早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用.
    • 晚期绑定:
      如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定
      随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装\继承,和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式.

    Java 中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字Virtual来进行显示的定义).如果在Java程序汇总不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法.

    虚方法和非虚方法

    非虚方法

    • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的.这样的方法称为非虚方法
    • 静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法.
    • 其他方法称为虚方法.

    多态的使用前提:
    1 类的继承关系
    2 方法的重写

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

    • 普通调用指令:
      1. invokestatic: 调用静态方法,解析阶段确定唯一方法版本
      2. invokespecial: 调用方法,私有及父类方法,解析阶段确定唯一方法版本
      3. invokevirtual: 调用所有虚方法
      4. invokeinterface: 调用接口方法
    • 动态调用指令:
      5. invokedynamic: 动态解析出需要调用的方法,然后执行

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

    练习一下

    package com.atguigu.java6;
    
    
    /**\
     *解析
     */
    class Father{
        public Father() {
            System.out.println("father的构造器~");
        }
    
        public static void showStatic(String str) {
            System.out.println("Father"+str);
        }
    
        public final void showFinal() {
            System.out.println("father show final!");
        }
    
        public void showCommon() {
            System.out.println("father 普通方法!");
        }
    
    }
    public class Son extends Father{
        public Son() {
            super();
        }
    
        public Son(int age) {
            this();
        }
    
        /**
         * 不是重写的父类方法
         * @param str
         */
        public static void showStatic(String str) {
            System.out.println(str);
        }
    
        private void showPrivate(String str) {
            System.out.println("son private"+str);
        }
    
        public void show() {
            showStatic("autaied.com");
            super.showStatic("ggoaset");
            showPrivate("hellow!");
            super.showCommon();
            showFinal();
            showCommon();
            info();
    
    //        MethodInterface in = null;
    //        in.methodA();
    
    
        }
        public void info() {
    
        }
    
        public static void main(String[] args) {
            new Son().show();
        }
    }
    
    展开全文
  • (二)虚拟机 笔记

    2020-02-24 17:15:57
    栈桢的内部结构 字节码中方法内部结构的剖析 mian方法解析 局部变量表 变量槽slot的理解与演示 操作数栈的特点 涉及操作数栈的字节码指令执行分析 动态链接的理解与常量池的作用 虚拟机栈概述 每个线程会...
  • Java之所以能够平台,JVM功不可没。Java程序“一次编写到处运行”正是基于不同平台上底层JVM的不同。不同的JVM对于内存的划分方式和管理机制存在着部分差异 一个JVM就是一个进程,一个Java进程对应一个运行时数据...
  • 第五章虚拟机

    2021-03-23 10:25:46
    由于平台的设计,Java的指令都死根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的 优点:平台、指令集小、编译器容易实现 缺点:性能下降、实现统一的功能需要更多的指令 内存中的和堆 ...
  • 第 5 章 虚拟机

    千次阅读 多人点赞 2020-08-12 16:23:14
    由于平台性的设计,Java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 内存中的...
  • jvm之虚拟机

    2021-08-03 21:29:22
    2.优点:是平台、指令集小、编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 3.是运行时的单位,而堆是存储单位。即解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是...
  • 五、虚拟机

    千次阅读 2020-06-06 15:28:39
    由于平台性的设计, Java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 初步印象 有...
  • 前言上篇文章介绍了运行时数据区的概述、以及PC寄存器,这篇文章介绍的是虚拟机一、虚拟机的概述虚拟机出现的背景================================我们知道Java虚拟机是基于的一种设计架构,优点是平台...
  • 由于平台性的设计,java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令 1.2 内存中...
  • JVM系列-第4章-虚拟机

    千次阅读 2020-11-24 12:29:58
    文章目录虚拟机简介虚拟机的出现背景内存中的与堆虚拟机基本内容虚拟机的特点虚拟机的异常设置内存大小概念举例的存储单位中存储什么?运行原理栈帧的内部结构局部变量表认识局部变量表概念举例...
  • 由于平台性的设计,Java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 有不少Java...
  • 目标主机内核网络会发现这个数据帧有VXLAN Header,并且VNI为1,Linux内核会对它进行拆包,拿到内部数据帧,根据VNI的值,交给本机flannel.1设备处理,flannel.1拆包,根据路由表发往cni网桥,最后到达目标容器。...
  • 栈运行原理栈桢的内部结构三、栈桢的局部变量表局部变量表介绍局部变量表存储单位关于Slot的重复利用静态变量与局部变量的对比补充说明四、栈桢的操作数栈操作数栈的代码追踪四、栈顶缓存技术五、栈桢的动态链接六、...
  • 提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录一、内存1.1 运行时数据区内存划分1.2...3.4.3 PC寄存器为什么被设定为线程私有四、虚拟机4.1 虚拟机概述4.1.1 虚拟机出现的背景4.1
  • 是一种快速有效的分配存储方式,访问速度仅次于程序计数器。程序计数器只存储指令的地址,所以速度快 JVM直接对Java的操作只有两个: ①每个方法执行,伴随着进栈(入栈、压栈)执行结束后的出栈工作 ②对于来说...
  • JVM(一)

    2017-10-05 15:08:42
    JVM JAVA的虚拟机,包含PC寄存器、本地方法、JAVA,JAVA堆、方法区,常量池,JAV之所以能平台、系统无障碍运行,是因为JAVA虚拟机将JAVA的字节码翻译成机器识别的机器码。包含PC寄存器 本地方法 JAVA *...
  • java笔记(1)

    2021-04-20 11:02:40
    但是要类调用的话,不能省略类名. (3)怎么接受返回值? 1. 使用变量来接受,变量的定义需要指定变量的数据类型 2. 方法调用可以写在 "system.out.println()"的小括号里来精简代码或者方法写在判断语句的小括号里.(这...
  • 第五章 - 虚拟机

    千次阅读 多人点赞 2020-11-22 10:53:51
    虚拟机概述1.1 虚拟机出现的背景1.2 内存中的与堆1.3 虚拟机的基本内容1.3.1 虚拟机的特点1.3.2 虚拟机的异常1.3.3 设置内存大小2.的存储单位2.1 的运行原理2.2 栈帧的内部结构3.局部变量表3.1 ...
  • Java虚拟机一、虚拟机概述二、的存储单位三、局部变量表四、操作处五、代码追踪六、栈顶缓存技术七、动态链接八、方法的调用:解析和分派九、方法返回地址十、一些附加信息十一、的相关面试题 一、虚拟机...
  • 35.并行和并发有什么区别? 并行:系统中有多个任务同时执行,同一时刻做多件事情。 并发:系统中有多个任务同时存在,同一时间间隔做多件事情。因此,单核系统不存在并行。...同时线程能够访问其隶属的...
  • 目录1、虚拟机的概述1.1、背景1.2、内存中的与堆1.3、虚拟机的基本内容1.3.1、特点1.3.2、异常1.3.3、设置的内存大小2、的存储单位2.1、的运行原理2.2、栈帧的内部结构3、局部变量表3.1、局部变量表介绍...
  • jvm-第五章:虚拟机

    2020-12-18 17:07:23
    由于平台性的设计,Java的指令都是根据来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 内存中的与...
  • 尽可能在分配内存 func test() []int { s := make([]int, 3) s[0] = 0x100 s[1] = 0x200 s[2] = 0x300 return s } func main() { _ = test() } $ go build && go tool objdump -s "main...
  • JVM一、什么是JVM二、内存结构1、程序计数器 PC Register2、虚拟机 JVM Stacks3、本地方法 Native Method Stacks4、堆 Heap 一、什么是JVM 定义 Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的...
  • JVM 核心知识点

    2021-03-19 11:36:42
    1.3.2、虚拟机栈 方法的出入栈:调用的方法会被打包成 栈桢 ,一个栈桢至少需要包含一个局部变量表、操作数栈、桢数据区、动态链接。 动态链接: 当栈帧内部包含一个指向运行时常量池引用前提下,类加载时候会进行...
  • 以上我们知道了Java类如何加载到虚拟机中,而且知道java方法会被转化为代码指令,另外对方法的调用采用栈桢这种结构。而JVM由C++开发,C语言如何如何能够调用到汇编指令的呢?这是函数指针起到的作用。 C语言中...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 449
精华内容 179
关键字:

跨栈桢访问

友情链接: StreamThreadSample.zip