深入理解JVM字节码(读书笔记)

1,202 阅读7分钟

第二章 字节码

  1. iinc
  2. for原理(ifxxx,goto)
  3. switch-case:lookupswitch(稀疏,O(logn)),tableswitch(紧凑,O(n)),String时使用hash值,hash冲突时如何处理,如何构造hash冲突(a-c=1, d-b=31,例如Aa,BB)
  4. try-catch-finally

方法的Code属性中有一个异常表(Exception table),每个异常表项表示一个异常处理器,由from指针,to指针,target指针,所捕获的异常类型type四部分组成。这些指针的值是字节码索引,用于定位字节码。其含义[from,to)字节码范围内,如果抛出了异常类型为type的异常,就会跳转到target指针表示的字节码处继续执行。

在finally语句中包含return语句时,会将返回值暂存到临时变量中,这个finally语句中的++i操作只会影响i的值,不会影响已经暂存的临时变量的值

  1. try-with-resources

    解决close用法在释放资源时出现的bug

    源代码中会添加Throwable.addSuppressed()把被抑制的异常记录下来,然后这些异常会出现在抛出的异常的堆栈信息中。

  2. 对象相关的字节码指令

    1. < init> 方法是对象初始化方法,类的构造方法,非静态变量的初始化,对象初始化代码都会被编译进这个方法中。所以如果我们在非静态变量中初始化一些东西可能抛出异常,就需要在构造器去处理或者代码块中处理。

    2. **一个对象创建需要三条指令,new、dup、<init> 方法的invokespecial调用。**在JVM中,类的实例初始化方法是<init>,调用new指令时,只是创建了一个类实例引用,将这个引用压入操作数栈顶,此时还没有调用初始化方法。使用invokespecial调用<init> 方法后才真正调用了构造器方法,那中间的dup指令的作用是什么?

      invokespecial会消耗操作数栈顶的类实例引用,如果想要在invokespecial调用以后栈顶还有指向新建类对象实例的引用,就需要在调用invokespecial之前复制一份类对象实例的引用,否则调用完<init> 方法以后,类实例引用出栈以后,就再也找不回刚刚创建的对象引用了。有了栈顶的新建对象的引用,就可以使用astore指令将对象引用存储到局部变量表。

      从本质上来理解导致必须要有dup指令的原因是<init> 方法没有返回值,如果<init>方法把新建的引用对象作为返回值,也不会存在这个问题。

    3. < clinit>方法

      < clinit>是类的静态初始化方法,类静态初始化块,静态变量初始化都会被编译进这个方法中。

      在四个指令触发时被调用(new,getstatic,putstatic和invokestatic),场景如下:

      创建类对象的实例,比如new,反射,反序列化等;

      访问静态变量或者静态方法;

      访问类的静态字段或者对静态字段赋值(final的字段除外);

      初始化某个类的子类。

第三章 字节码进阶

  1. 方法调用指令

​ invokestatic:用于调用静态方法。

​ invokespecial:用于调用私有实例方法,构造器方法以及使用super关键字调用父类的实例方法等。

​ invokevirtual:用于调用非私有实例方法。

​ invokeinterface:用于调用接口方法。

​ invokedynamic:用于调用动态方法。

  1. 为什么有了invokevirtual指令还需要invokespecial指令呢?

这是出于效率的考虑,invokespecial调用的方法可以在编译期间确定,在JDK 1.0.2之前,invokespecial指 令曾被命名为invokenonvirtual,以区别于invokevirtual。例如private方法不会因为继承被子类覆写,在编译 期间就可以确定,所以private方法的调用使用invokespecial指令。

  1. Java方法分派原理

    invokevirtual 单继承(类似C++单继承时的虚方法表,通过vtable加offset找到方法)

    invokeinterface 除了虚方法表vtable, JVM提供了名为itable(interface method table)的结构来支持多 接口实现,itable由偏移量表(offset table)和方法表(method table)两部分组成。

    vtable,itable小结:

    vtable、itable机制是实现Java多态的基础。

    子类会继承父类的vtable。因为Java类都会继承Object类,Object中有5个方法可以被继承,所以一个空Java类的vtable的大小也等于5。

    被final和static修饰的方法不会出现在vtable中,因为没有办法被继承重写,同理可以知道private修饰的方法也不会出现在vtable中。

    接口方法的调用使用invokeinterface指令,Java使用itable来支持多接口实现,itable由offsettable和method table两部分组成。在调用接口方法时,会先在offset table中查找method table的偏移量位置,随后在method table查找具体的接口实现。

  2. invokestatic

    invokedynamic之前需要先介绍一个核心的概念方法句柄(MethodHandle)。MethodHandle又称为方法句柄或方法指针,是java.lang.invoke包中的一个类,它的出现使得Java可以像其他语言一样把函数当作参数进行传递。MethodHandle类似于反射中的Method类,但它比Method类要更加灵活和轻量级。

    运行时才能知道的实例方法调用,一种方法动态分配的方式。

  3. lambda表达式的原理(待加强分析)

Lambda表达式声明的地方会生成一个invokedynamic指令,同时编译器生成一个对应的引导方法(Bootstrap Method)。

第一次执行invokedynamic指令时,会调用对应的引导方法(Bootstrap Method),该引导方法会调用LambdaMetafactory.metafactory方法动态生成内部类。

引导方法会返回一个动态调用CallSite对象,这个CallSite会最终调用实现了Runnable接口的内部类。

Lambda表达式中的内容会被编译成静态方法,前面动态生成的内部类会直接调用该静态方法。

真正执行lambda调用的还是用invokeinterface指令。

Lambda表达式采用的方式并不是在编译期间生成匿名内部类,而是提供一个稳定的字节码二进制表示规范,对用户而言看到的只有invokedynamic这样一个非常简单的指令。

  1. 泛型与字节码
  2. synchronized 原理,无论正常或异常都会释放锁,try-finally monitor(管程)
  3. 反射的实现原理,反射的inflation机制(0-15,16+)

第四章 Javac编译原理简介

javac编译过程 的七个阶段

  1. parse:读取.java源文件,做词法分析和语法分析
  2. enter:生成符号表
  3. process:处理注解
  4. attr:检查语义合法性,常量折叠
  5. flow:数据流分析
  6. desugar:去除语法糖
  7. generate:生成字节码

第五章 从字节码角度看Kotlin语言

  1. Metadata注解 为什么需要这样一个注解呢?Kotlin代码最终要编译为Class文件,但Kotlin语言独有的一些特性,比如lateinit,nullable,properties delegation等无法用单纯的字节码表示,这些信息都被写入Meatdata注解当中。

  2. 顶层方法 实际上就是生成XXXKt.kt,然后里面定义一个静态方法。

  3. Object单例 饿汉式单例

  4. 扩展方法 Kotlin就是在扩展函数代码所在的类新建了一个静态的方法,这个方法的第一个参数是扩展类的对象。

    当要扩展的类中已经存在一个方法签名相同的方法是,Kotlin会优先选择扩展类中定义的方法。

  5. 接口默认方法 接口中有个静态内部类来实现

  6. 默认参数 Kotlin默认参数的实现方式是使用了一个整型掩码(mask),记录了此次调用有哪些位置的参数没有赋值,没有赋值的参数就会被设置为预设的默认值。

  7. Kotlin协程实现原理,suspend关键字,Kotlin协程的原理是每个挂起点和初始起点对应的Continuation都会转化为状态机的一种状态,协程切换只是状态机切换到另外一种状态,使用CPS机制传递了协程上下文。

字节码的应用

JDK动态代理使用反射机制来调用被拦截的方法,效率较低。cglib使用了一种FastClass的机制来规避反射调用。它的原理是对被代理类的方法增加索引,通过索引值可以直接定位到具体的方法。

Cglib使用ASM生成目标代理类的一个子类,在子类中扩展父类方法,达到代理的功能,因此要求代理的类不能是final的。