💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 1 概述 javac 将语法树输出成字节码,通过虚拟机执行时,可以达到折叠语法树结点的目的。 ## 2 运行时栈帧结构 Java 虚拟机以方法作为最基本的执行单元,栈帧(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。 - 栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息,一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。 - 同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行执行状态,而在执行引擎中,在活动线程里,只有位于栈顶的方法才是在运行的,位于栈顶的栈帧被称为当前栈帧(Current Stack Frame),与这个栈帧所关联的方法被称为当前方法(Current Method) ### 2.1 局部变量表 局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。 局部变量表的容量以变量槽(variable Slot)为最小单位,Java虚拟机规范中说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。 reference表示对一个对象实例的引用,需要支持两件事: - 根据引用直接或间接查找到对象在Java堆中的数据存放的起始地址或索引 - 根据引用直接或间接地查找对象所属数据类型在方法区的存储的类型信息 returnAddress目前已经很少见了,是为字节码jsr、jsr_w、ret服务的,指向了一条字节码指令的地址,某些古老的虚拟机用来实现异常处理时的跳转,现在已经全部改为异常表了 当一个方法被调用时,虚拟机会使用局部变量表来完成实参到形参的传递 - 如果执行的是实例方法,那布局变量表中的第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字this来访问到,其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽 - 为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的 > 示例一 ```java byte[] placeholder = new byte[64 * 1024 * 1024]; System.gc(); // 未回收 ``` > 示例二 ```java { byte[] placeholder = new byte[64 * 1024 * 1024]; } System.gc(); // 未回收 ``` > 示例三 ```java { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; // 变量槽复用 System.gc(); // 回收 ``` 假设前面占用了很大内存,而后面的方法又比较耗时,可以手动将局部变量设置为null;但是,一般不推荐手动将局部变量设置为null ### 2.2 操作数栈 操作数栈(Operand Stack)也常被称为操作栈,32位数据元素所占的栈容量为1,64位的为2,javac的数据流分析工作保证了在方法执行时任何时候,操作数栈的深度都不会超过在max_stack数据项中设定的最大值。 ### 2.3 动态连接 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking),Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令时就以常量池里面指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。 ### 2.4 方法返回地址 一个方法被执行后,只有两种方式退出,一种是正常调用完成(Normal Method Invocation Completion),一种是遇到异常没有被catch,这种方式不会返回任何值,被称为异常调用完成(Abrupt Method Invocation Completion) ### 2.5 附加信息 附加信息包括与调试、性能相关的信息,完全取决于虚拟机实现,一般会把动态连接,方法返回地址附加信息归为一类,称为栈帧信息。 ## 3 方法调用 方法调用不等于方法被执行,其唯一的任务就是确定被调用的是哪一个方法。 ### 3.1 解析 所有方法调用的目标方法在Class文件中都是一个常量池的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的版本,并且这个方法的调用版本在运行期是不可改变的。这些方法的调用被称为解析(Resolution),符号`编译期可知,运行期不可变`的方法,主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们不可能通过继承或别的方式重写出其他版本(性能优化点),因此他们都适合在类加载阶段进行解析。 Java 虚拟机支持以下5种方法调用字节码指令: - invokestatic:用于调用静态方法 - invokespecial:用于调用实例构造器方法`<init>()`、私有方法和父类中的方法 - invokevirtual:用于调用所有的虚方法 - invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象 - invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法 能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有: - 静态方法 - 私有方法 - 实例构造器 - 父类方法 - final修饰的方法(尽管它使用invokevirtual指令调用) 统称为非虚方法(Non-Virtual Method),并且,Java语言规范中明确定义了被final修饰的方法是一种非虚方法 解析调用一定是一个静态的过程,在编译期完全确定;而另一种调用形式:分派(Dispatch)调用则要复杂许多,它可能是静态的也可能是动态的,一共有4种: - 静态单分派 - 静态多分派 - 动态单分派 - 动态多分派 ### 3.2 静态分派 > 重载方法静态分派演示 ```java /** * 静态分派的示例 */ public class Test { static abstract class A { } static class B extends A { } static class C extends A { } public void echo(A a) { System.out.println("A"); } public void echo(B b) { System.out.println("B"); } public void echo(C c) { System.out.println("C"); } public static void main(String[] args) { A b = new B(); A c = new C(); Test test = new Test(); test.echo(b); // A test.echo(c); // A } } ``` 在以上代码中,A被称为变量的静态类型(Static Type)或者外观类型(Apparent Type),B被称为变量的实际类型(Actual Type)或者运行时类型(Runtime Type), static 需要编译期确定,对于重载(Overload)方法,将选择静态分派,在方法调用时选择静态类型。例如以下方法,编译期不可知,因此定为A类型 ```java A d = new Random().nextBoolean() ? new B() : new C(); test.echo(d); // A ``` ---- 重载方法的优先级选择也是编译期通过静态分派完成的。 > 一个关于重载方法匹配优先级的例子 ```java import java.io.Serializable; /** * 静态分派的示例 */ public class Test { /** 0 优先级最高,直接匹配 */ static void echo(char arg) { System.out.println("char"); } /** 1 char > int */ static void echo(int arg) { System.out.println("int"); } /** 2 int > long */ static void echo(long arg) { System.out.println("long"); } /** 3 long > float */ static void echo(float arg) { System.out.println("float"); } /** 4 float > double */ static void echo(double arg) { System.out.println("double"); } /** 5 原始类型找尽,找char的包装类 */ static void echo(Character arg) { System.out.println("Character"); } /** 6 Character的接口,如果再写一个Comparable类型,由于优先级相同,会导致编译失败 */ static void echo(Serializable arg) { System.out.println("Serializable"); } /** 7 父类 */ static void echo(Object arg) { System.out.println("Object"); } /** 8 变长参数类型,优先级最低 */ static void echo(char... arg) { System.out.println("char..."); } /** 重载方法优先级选择的测试程序 */ public static void main(String[] args) { echo('a'); } } ``` ### 3.3 动态分派 动态分派的实现与重写(Override)有着密切的关联。 > 动态分派的示例 ```java package vm; import java.io.Serializable; /** * 静态分派的示例 */ public class Test { static abstract class A { protected abstract void echo(); } static class B extends A { @Override protected void echo() { System.out.println("B"); } } static class C extends A { @Override protected void echo() { System.out.println("C"); } } public static void main(String[] args) { A b = new B(); A c = new C(); b.echo(); // B c.echo(); // C b = new C(); b.echo(); // C } } /* public static void main(java.lang.String[]); Code: 0: new #2 // class vm/Test$B 3: dup 4: invokespecial #3 // Method vm/Test$B."<init>":()V 7: astore_1 8: new #4 // class vm/Test$C 11: dup 12: invokespecial #5 // Method vm/Test$C."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method vm/Test$A.echo:()V 20: aload_2 21: invokevirtual #6 // Method vm/Test$A.echo:()V 24: new #4 // class vm/Test$C 27: dup 28: invokespecial #5 // Method vm/Test$C."<init>":()V 31: astore_1 32: aload_1 33: invokevirtual #6 // Method vm/Test$A.echo:()V 36: return */ ``` 第0~15行是准备动作,建立了b和c的内存空间、调用了B和C类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表的变量槽中,对应了以下两行: ```java A b = new B(); A c = new C(); ``` 在第16~21行中,第16行和第20行的aload指令分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的echo方法的所有者,称为接受者(Receiver);第17行和第21行是方法调用指令,虽然相同,但是可以通过invokevirtual指令来分析,其运行时解析过程分为 : - 找到操作数栈顶的第一个元素所指向的对象的运行时类型(Runtime Type),记作C - 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则抛出java.lang.IllegalAccessError异常 - 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。 - 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常 这个过程就是Java方法重写(Override)的本质 ---- 字段没有多态性,虽然子类的内存中会存在父类中的同名字段,但是子类的字段会遮蔽父类中的同名字段。 **字段没有多态性** ```java package vm; /** * 字段没有多态性,子类会遮蔽父类的同名字段 */ public class Test { static class A { public int a = 1; public A() { a = 2; echo(); } void echo() { System.out.println("A#a " + a); } } static class B extends A { public int a = 3; public B() { a = 4; echo(); } @Override void echo() { System.out.println("B#a " + a); } } public static void main(String[] args) { A a = new B(); System.out.println("a.a " + a.a); // 父类初始化 虚方法调用,子类echo(子类的字段b)方法,访问子类的字段 输出 B#a 0 // 子类初始化 虚方法调用,子类echo(子类的字段b)方法,访问子类的字段 输出 B#a 4 // 静态调用 访问父类字段 输出 a.a 2 } } ```