8.4 方法重写的本质
动态语言和静态语言
1.动态类型语言和静态类型语言两者的区别就在于 对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
2.说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java:String info = "mogu blog"; (Java是静态类型语言的,会先编译就进行类型检查) JS:var name = "shkstart"; var name = 10; (运行时才进行检查)
方法重写的本质
Java 语言中方法重写的本质:
1.找到操作数栈顶的第一个元素所执行的对象的 实际类型,记作C。
2.如果 在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验
1.如果通过则返回这个方法的直接引用,查找过程结束
2.如果不通过,则返回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 多态与虚方法表
虚方法表
1.在面向对象的编程中,会很频繁的使用到 动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能 影响到执行效率。
2.因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,非虚方法不会出现在表中。使用索引表来代替查找。
3.每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
4.虚方法表是什么时候被创建的呢?虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。
5.如图所示:如果类中重写了方法,那么调用的时候,就会直接在该类的虚方法表中查找
九、方法返回地址
方法返回地址(return address)
1.存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
1.正常执行完成
2.出现未处理的异常,非正常退出
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.方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
代码举例
代码
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
dreturn
areturn
异常处理表:
反编译字节码文件,可得到 Exception table
from :字节码指令起始地址
to :字节码指令结束地址
target :出现异常跳转至地址为 11 的指令执行
type :捕获异常的类型
十、相关面试题
1.举例栈溢出的情况?(StackOverflowError)
递归调用等,通过-Xss设置栈的大小;
2.调整栈的大小,就能保证不出现溢出么?
不能 如递归无限次数肯定会溢出,调整栈大小只能保证溢出的时间晚一些,极限情况会导致OOM内存溢出(Out Of Memery Error)注意是Error
3.分配的栈内存越大越好么?
不是 会挤占其他线程的空间
4.垃圾回收是否会涉及到虚拟机栈?
不会
关于Error我们再多说一点,上面的讨论不涉及Exception
首先Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception和Error体现了JAVA这门语言对于异常处理的两种方式。
Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。
其中的Exception又分为检查性异常和非检查性异常。两个根本的区别在于,检查性异常 必须在编写代码时,使用try catch捕获(比如:IOException异常)。非检查性异常 在代码编写使,可以忽略捕获操作(比如:ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。
5.方法中定义的局部变量是否线程安全?
要具体情况具体分析
/** * 面试题: * 方法中定义的局部变量是否线程安全?具体情况具体分析 * * 何为线程安全? * 如果只有一个线程可以操作此数据,则必定是线程安全的。 * 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题 * * 我们知道StringBuffer是线程安全的源码中实现synchronized,StringBuilder源码未实现synchronized,在多线程情况下是不安全的 * 二者均继承自AbstractStringBuilder * */ public class StringBuilderTest { //s1的声明方式是线程安全的,s1在方法method1内部消亡了 public static void method1(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); } //stringBuilder的操作过程:是不安全的,因为method2可以被多个线程调用 public static void method2(StringBuilder stringBuilder){ stringBuilder.append("a"); stringBuilder.append("b"); } //s1的操作:是线程不安全的 有返回值,可能被其他线程共享 public static StringBuilder method3(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1; } //s1的操作:是线程安全的 ,StringBuilder的toString方法是创建了一个新的String,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); } }