JVM学习——字节码(学习过程)

JVM——字节码

为什么要学字节码

字节码文件,有什么用?

  1. JVM虚拟机的特点:一处编译,多处运行。

  2. 多处运行,靠的是.class 字节码文件。

  3. JVM本身,并不是跨平台的。Java之所以跨平台,是因为JVM本身不夸平台。

  4. 二进制的文件,显然不是给人看的。是给机器看的。

  5. 从根源了解了之后,返回到语言层次 好多都会豁然开朗。

  6. 必须要学,学一个东西,还需要理由吗?

Java语言规范补充:

JVM虚拟机规范(相对底层的)Java,Groovy,kotlin,Scala。 编译后都是Class文件,所以就都能在JVM虚拟机上运行。

字节码:枯燥且重要

字节码文件解读

一个Java类,然后进行编译成字节码文件

package com.dawa.jvm.bytecode;

public class MyTest1 {
    private int a = 1;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

javap 编译后的结果:

➜  main javap com.dawa.jvm.bytecode.MyTest1   
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1 {
  public com.dawa.jvm.bytecode.MyTest1();
  public int getA();
  public void setA(int);
}

Java -c 编译后的结果:

➜  main javap -c com.dawa.jvm.bytecode.MyTest1 
Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1 {
  public com.dawa.jvm.bytecode.MyTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field a:I
       9: return

  public int getA();
    Code:
       0: aload_0
       1: getfield      #2                  // Field a:I
       4: ireturn

  public void setA(int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field a:I
       5: return
}

javap -verbose编译后的结果

 ➜   main javap -verbose com.dawa.jvm.bytecode.MyTest1 
Classfile /Users/shangyifeng/work/workspace/jvm_leature/build/classes/java/main/com/dawa/jvm/bytecode/MyTest1.class
  Last modified 2020-2-14; size 489 bytes
  MD5 checksum 952635139a8b5b42f0142d033929d8c2
  Compiled from "MyTest1.java"
public class com.dawa.jvm.bytecode.MyTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#21         // com/dawa/jvm/bytecode/MyTest1.a:I
   #3 = Class              #22            // com/dawa/jvm/bytecode/MyTest1
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/dawa/jvm/bytecode/MyTest1;
  #14 = Utf8               getA
  #15 = Utf8               ()I
  #16 = Utf8               setA
  #17 = Utf8               (I)V
  #18 = Utf8               SourceFile
  #19 = Utf8               MyTest1.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               com/dawa/jvm/bytecode/MyTest1
  #23 = Utf8               java/lang/Object
{
  public com.dawa.jvm.bytecode.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/dawa/jvm/bytecode/MyTest1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/dawa/jvm/bytecode/MyTest1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 11: 0
        line 12: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/dawa/jvm/bytecode/MyTest1;
            0       6     1     a   I
}
SourceFile: "MyTest1.java"

Java字节码常量池深入分析

  1. 字节码查看器(Hex_fiend)
  2. IDEA自带的字节码查看插件(jclasslib)

上述都不是真正存在磁盘上的字节码文件。

借助工具:Hex_fiend(mac) 查看16进制的文件

用工具打开的二进制文件:16进制

4字节展示格式

4byte展示

单字节展示格式

单字节16进制的class文件

CA:一个16进制占据4位,2个代表8位。一个8位字节代表一个字符

上述部分规则说明

  1. 使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。

  2. 魔数:所有.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE

    魔数是用来首次判断是否为合法且有效的字节码规范。(cafe babe 翻译为: 咖啡宝贝)

  3. 魔数之后的4个字节为版本信息,前两个字节表示minor versionS(次版本号),后面两个字节表示major version(主版本号)。这里的版本号为00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52。(52对应JDK8,51对应JDK7,以此类推)。所以该文件的版本号为:1.8.0。使用java -version查看。所以可以通过这个去判断你本地的JVM是否支持你的Class类文件的版本号。

    image-20200214073419042

  4. 常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的。可以将常量池看做是Class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,java中声明为final的常量等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名词和描述符等。

    常量池:里面的值不一定都是常量。变量也是放在常量池的。

  5. 常量池的总体架构:java类所对应的常量池主要由常量池数量与常量池数组(表结构)这两部分共同构成。常量池数量紧跟在主版本号后面,占据2个字节;而常量池数组则紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的, 长度当然也不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。 值得注意的是:常量池数组中元素的个数 = 常量池数 -1 (其中0暂时不适用),目的是满足某些常量池索引值的数据在特定情况下需要表达「不引用任何一个常量池」的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它部位与常量表中,这个常量对应null值;所以,常量池的索引从1开始而非从0开始。

Java字节码:Class文件结构中常量池中14种数据类型(3种为动态调用未显示)的结构总表

Java字节码表

  • CONSTANT_Utf8_info
    • tag: 标识位,u1类型
    • length: 字符长
    • bytes:长度为length的字符串
  • CONSTANT_NameAndType_info
    • tag:值是12,在16进制里面就是0C
    • 名字:方法名或者变量名
    • 描述符:类型,或者是返回值加参数类型的拼写
  1. 在JVM规范中,每个变量/字段都是由描述信息的,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示(.替换为/)。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示。

    B - byte

    C - char

    D - double

    F - float

    I - int

    J - long

    S - short

    Z - boolean

    V - void

    L - 对象类型。如:Ljava/lang/String

  1. 对于数组类型来说,每个维度使用一个前置的[来表示。如:

    int[] - 被记录为 [I

    String[][]被记录为[[Ljava/lang/String

  2. 用描述符描述方法时,按照先参数列表,后返回值的顺序为描述。参数列表按照参数的严格顺序放在一组()之内。

    如方法:String getRealnameByIdAndNickname(int id,String name)的描述符为

    (I,Ljava/lang/String)Ljava/lang/String

分析之后:所有的常量池的内容:如图中标识

image-20200214212613716

字节码16进制文件,是给机器看的,不是给人看的。

已经分析的4个部分:

  1. 魔数
  2. 版本号
  3. 常量池数-1
  4. 常量池数组(常量池中的每个常量的具体信息)

字节码整体结构分解

image-20200214212809818

  1. 当前类的访问控制权限(private,public , pro , 等几种 当前类的标识符)
  2. 当前类的名字
  3. 父类的名字
  4. 当前类的接口信息
  5. 当前类的成员变量的信息
  6. 当前类的方法的信息
  7. 当前类附加的属性

上面一张图说明了字节码所有的事情

image-20200214213356594

完整的java字节码结构

image-20200214213832887

  • Class文件中有两种数据类型 (在上面体现)
    • 字节数据直接量:u1/u2/u4/u8分别代表1个,2个,4个,8个连续的字节组成的整体数据
    • 表(数组):

image-20200214214124717

image-20200214214059170

访问标识符与字段表

  1. 访问标识

image-20200214214207818

image-20200214214238898

![image-20200214214533351](/Users/shangyifeng/Library/Application Support/typora-user-images/image-20200214214533351.png)

  1. 字段表

image-20200214214907055

image-20200214215017901

访问修饰符+名字+描述索引+数量+值:就构成了一个字段

image-20200214215059930

0002 访问修饰符,代表 private

名字描述符索引:指向了 a

描述符索引:指向了 I

接着:attributes_count 为0 , 那么 attribute_info 就不存在。字节码中就不体现。

image-20200214215655784

此二进制文件到此为止:字段也结束完毕了。敢不敢尝试自己写一个反编译的工具。去读一下这个二进制的字节码的文件。

方发表与属性表深度剖析

image-20200215203537894

如果方法中有属性:那么方法的属性结构

image-20200215203804238

Code:表示方法的执行的代码

image-20200215203934951

image-20200215204301065

image-20200215204357605

CODE结构详解:

每一个方法,里面都会有一个属性:Code attribute > 1

Code attribute的作用是保存该方法的结构。(对应的字节码的信息。)

对于方法来说:Code属性是统一存在的。并且,值都为“Code”

image-20200215204416954

行号表之后,是局部变量表(在PPT中没有体现,注意。)

StackMapTable也没有在表格中体现(不重要,在做校验功能的时候用的)

其他须知:对于Java里面每一个方法里面(包括构造方法)都是至少有一个局部变量this的。所以在每个方法里面都能用到this。但是静态方法里面是没有this的,直接编译就失败了。总的来说就是:字节码面前,没有秘密。

image-20200215204533110

  • 方法体的内容,是以助记符的方式体现出来的
  • stack:表示方法在运行的任何时刻能够达到的操作数栈的最大深度
  • locals:表示方法执行期间创建的局部变量的数目。包含传入的参数的局部变量。

image-20200215204621773

  • Code_length 表示该方法所包含的字节码的字节数以及具体的指令码(指令)
    • 这10个字节,代表了方法真正运行的内容。
    • image-20200215214356166
    • image-20200215214329740

16进制符号 和 助记符的对应关系

没有人知道。。。只有在官网设置的有对应关系。(java 虚拟机官网)

image-20200215214754817

从图中描述:将指定的XX值推到栈顶。这里认识到了数据结构的重要性。推送到操作栈的栈顶,就是要操作他了。

image-20200215215743451

  • 分析上述构造方法的Code的二进制字节码源码。得出结论:
  • 成员变量的赋值,是在构造方法里面完成的
  • 完成之后构造方法就结束了。

行号表之后,是局部变量表(在PPT中没有体现,注意。)

image-20200215205551038

image-20200215210902588

Debug的时候对于Class文件中的错误的行号都能找出来,用的就是这个属性

image-20200215210951770

详细深入了解 关于方法Code这一块的字节码。

  • 方法体的内容,是以助记符的形式体现出来的:
    • 16进制数字,1个或者多个,对应一个特定的助记符

![image-20200215213713665](/Users/shangyifeng/Library/Application Support/typora-user-images/image-20200215213713665.png)

  • 每一个方法,里面都会有一个属性:Code attribute > 1

    Code attribute的作用是保存该方法的结构。(对应的字节码的信息。)

    对于方法来说:Code属性是统一存在的。并且,值都为“Code”

  1. 源文件

    image-20200215222629761

  2. 字节码

    image-20200215214329740

  3. 翻译后的

    image-20200215222641215

IDEA里面查看字节码源码的工具-插件

image-20200215205950504

到现在为止。读完了一个非常简单的程序的字节码的二进制文件。

建议:自己一定要花时间去自己去尝试解读上述字节码文件,以后肯定不会一有空就去读这个源码的,机会甚少,如果上述用2小时,3小时,4小时,做一次。不会的去查,去百度。去官网看对应的助记符。这对于以后对于字节码的认知是由很大很大帮助的。

学完之后感觉受益匪浅!感受:

  1. 了解了规范,你就能读懂了。
  2. 老师的重要性(能带你进入这个领域)
  3. 别人讲,比自己第一遍读书,来的深刻
  4. 很多东西并不是泛泛而谈就可以了。比如说看网上的一篇文章,一个教程等。

复杂的字节码文件分析

源文件:

image-20200216063833438

java -p 字节码:

image-20200215222938582

image-20200215222956268

image-20200215223034116

image-20200215223105641

image-20200215223146752

先补充synchronize关键词的字节码讲解

  1. 如果把方法设置为private,方法在字节码中就没有了。如图

    (并不是不见了。是因为只是用javap 命令的话,是不显示私有变量的参数的)

    要使用javap -p 这个参数。

    image-20200215224618680

加了synchronized之后的字节码文件比较:只多了一个访问修饰符flag

image-20200216051529173

为什么只有一个访问修饰符呢?我们平时说的同步关键字和同步块,功能都是怎么实现的呢?对于moniterenter 和 moniterexit 这两个指令怎么执行的?是如何通过上述指令完成对锁的锁定和释放呢?

  • monitorenter的作用:进入到对象的监视器当中:

    image-20200216062417881

image-20200216053945648

  • monitorexit的作用:退出对象的监视器

    image-20200216062259433

  • synchronized可以用到的地方:实例方法上 或者 静态方法上,或者在方法内修饰锁对象。

  • 在方法名字上,用的是synchronized关键字。如果是在方法名字上,则只是多了一个标记符

  • 在方法里面,用的是synchronized方法,修饰锁对象,如果修饰在对象上,则字节码有其他体现

  • 如果在静态方法上,则代表给当前方法所在的Class类的对象进行上锁

  • 修饰实例方法的时候,synchronize修饰的是this对象。字节码中只在访问修饰符里面体现。(如果用在方法里面,就能够在字节码中看到具体的二进制代码的实现了。如下图所示,常量池中 13 和3 对应的两个关键词。)其中 13 是正常退出的操作。19 是异常退出的操作。确保锁在出异常的时候能够正常退出。

image-20200216053415793

该方法对应的字节码如下

image-20200216053316908

需要了解的:可重入锁,单线程执行

image-20200216061943561

  • (自己乱写的一些概念。别人可能看不懂。对上述概念的自我翻译。)当第一个线程使用锁对象的时候,状态有0变为1,在访问锁的同事,可以再次访问这个锁对象中的其他synchronize标记方法。当第二个线程尝试去获取这个锁的时候,是进不去的。因为这个对象就进入等待状态。在等待的时候处于类的自旋状态。老版本的synchronize是比较重量级的,所以之前要求能不用就不用,但是现在已经相对优化了。

  • 如果另外一个线程已经拥有了这个线程锁对象的标记,在状态由1变为0之前,另外一个线程会一直处于等待的阻塞状态。直到那边线程释放,这边再次尝试获得拥有权。

深入分析上述的复杂程序的字节码

字节码如下所示,当然不会像之前那么详细,是对整体有一个认知。

image-20200216054227798

当然,还是要通过结构表来进行查询操作:

image-20200216054318371

JClasslib对应的信息:

image-20200216054427655

70个常量池,70-1 = 69个常量

image-20200216054524329

  • 在字节码中,能看到字节装箱的操作:

从第9个字段定义中,赋值的是5.53 调用的是L类型的Integer。完成了自动装箱操作

image-20200216055012042

  • 是类中存在静态变量的时候才会出现的静态初始化块,对静态变量进行初始化

image-20200216055805127

  • 默认的访问标识符为 0000。 0009 = public static

    image-20200216060512022

  • 方法:6个

    image-20200216060707231

  • 初始化变量的赋值,是在构造方法<init>()中执行的 (不包括静态变量)

    image-20200216060912280

    问题1:如果我自己没有提供无参构造,系统会自动生成构造方法,然后在里面进行初始化。如果我们提供一个无参数构造方法呢?

    测试结果:结果一样。如下图所示

    image-20200216061120385

    为什么会这样呢?原来,因为在编译完之后,JVM会对指令,进行重新的排序。所以会是这样的现象。

    问题2:如果有两个构造方法呢?那么变量的赋值操作,是在哪里进行执行的呢?

    测试结果:有参构造和无参构造的字节码,完全相同,没有任何的变化。

    说明:JVM是把所有赋值,都给放在所有的构造方法中了。(并且是先赋值,然后再执行构造方法自定义的代码片段。)字节码面前,了无秘密。

    image-20200216061517841

  • 静态变量和静态代码块的内容都是在cinit()方法中执行的

    不管有多少个代码块,都只会生成一个cinit()方法

    image-20200216062922558

This关键词和异常表的详解

源文件代码:

package com.dawa.jvm;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;

public class MyTest3 {
    public void test(){
        try {
            InputStream is = new FileInputStream("test.txt");
            ServerSocket serverSocket = new ServerSocket(9999);
            serverSocket.accept();
        } catch (FileNotFoundException ex) {

        } catch (IOException ex) {

        } catch (Exception ex) {

        }finally {
            System.out.println("finally ");
        }
    }
}

异常

对应的字节码(java -p)

image-20200216064327499

image-20200216064451008

image-20200216064516162

image-20200216064540777

image-20200216064609609

  • 对于类中的每一个实例方法(非static方法),其在编译后说生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个(tiis),它位于方法的第一个参数位置处;这样我们就可以在java的实例方法中使用this来访问当前对象的属性以及其他方法

  • 这个操作是在编译期间完成的,即由javac编译器在编译的时候对this的访问转化为对一个普通实例方法参数的访问,接下来起的运行期间,由JVM在调用实例方法时,自动向实例方法传入this参数。所以,在实例方法的局部变量表中,至少会有一个指向对象的局部变量。

    如下所示:无参数的方法的参数个数为1

    image-20200216065233085

  • 为什么局部变量是4个?

    • 我的答案:“test”,“is”, "serverSokect" , "9999"

    • 正确答案:this / is / serverSocket / ex(执行的时候最多进入一个,所以4个)

      image-20200216065749656

    • 如果没有抛出异常,则只有3个变量。上面只是说最大本地变量数为4个。

    • 堆栈的执行方式。java调用的机制,都是基于堆栈的。所以有一个概念叫“栈帧”。后续会涉及。 表示最多压入3个元素。image-20200216070002638

  • 关于异常表的内容:

    image-20200216070125405

    image-20200216070211155

    • 这里涉及了goto指令。出异常的时候,就是使用goto跳转到指定的异常处理。会有一个默认 any , exceptiontype = 0,代表所有的异常。

      image-20200216070515826

      image-20200216073313437

  • 关于goto,我的想法:我觉得下面博客中的这种说的不对。 他没有深入理解JVM。 因为goto语句在字节码文件里面,异常处理的时候用到了。(网上有很多种说goto没有用的观点)

    信息图像(https://tva1.sinaimg.cn/large/0082zybply1gbxvq1ka3pj30yj0u0tiy.jpg)

Java字节码对于异常的处理方式:

  1. 统一采用异常表的方式来对异常进行处理的。(下图为异常表)

    image-20200216074256095

  2. 在JDK1.4.2 之前的版本,并不是使用异常表的方式来对异常进行处理的。而是采用特定的指令。

  3. 当异常处理存在你finally语句块时,现代化的JVM采取的处理方式是:将finally语句块的字节码拼接到每一个catch块后面,换句话说,冲虚中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。

异常处理有两种方式。1.catch 2. throws ,上述的是第一种方式。

如果是throws 抛异常,查看异常的字节码。(方法级别的异常)

注意:

  1. 两种异常方式,异常不是在同一个级别的

  2. 不论是运行时异常,还是其他异常,都是一样的。都能在这里throws

image-20200216075155726

字节码到底是如何执行的?

栈帧(stack frame):

  • 栈:先进后出的数据结构

  • 帧:小的单元

  • 栈帧是有一种用于帮助虚拟机执行方法调用与方法执行的数据结构。

  • 栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息

  • 符号引用,直接引用 (由动态链接,扩展出来的概念)

补充知识:动态链接,C语言的动态链接库。符号引用,直接引用的概念。入栈和出栈的过程。

image-20200216081155798

到这里该学学汇编语言了。

SLOT: 32位。 一个int,长度为32,占用1个Slot。Slot是可以复用的。

image-20200216081516019

对于slot可复用的解释,如上图:如b,c所占用的slot, 可能会被后来的d,e 占用。所以所占用的slot数量不能固定计算出来的。是动态的。

符号引用,如何转换为直接引用

直接引用:可以直接拿到方法或者变量的内存地址。在运行期间是拿不到的。

符号引用,是存在java的常量池中的。

方法调用:通过常量池,将符号引用,转换为直接引用

所以,符号引用的转换的两种方式:

  1. 静态解析。

    有些符号引用是在类加载阶段或者是第一次使用的时候就会转换为直接引用,这些转换叫做静态解析。

  2. 动态解析。

    另外一些符号引用则是在 每次运行期转换为直接引用,这种转换叫做动态链接。这体现为Java的多态性。

    通过伪代码来解释(下图):只有在处于运行期的时候,这些变量才会被动态的识别。这就是多塔ID一种体现,也是多态性的体现。在字节码中所能看到的,字节码的a所对应的都是Animal类的引用。叫做:invokevirtual:动态调用转发。

    333

    方法重载与invokevirtual字节码指令的关系**

  1. invokeinterface: 助记符:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
  2. invokestatic:调用静态方法
  3. invokespecial:调用自己的私有方法、构造方法(<init>)以及父类的方法
  4. invokevirtual:调用虚方法,运行期动态查找的过程。
  5. invokedynamic:动态调用方法。JDK1.7引入的

从代码去理解invokestatic 含义

image-20200216093704005

![image-20200216093757320](/Users/shangyifeng/Library/Application Support/typora-user-images/image-20200216093757320.png)

  • 能被invokestatic和in是invokespecial 描述的方法,在加载的时候,就会转换为直接引用。
  • 静态解析的四种情形(称为非虚方法,在类加载阶段就可以将符号引用转换为直接引用 )
    • 静态方法
    • 父类方法
    • 构造方法
    • 私有方法 (无法被重写,共有方法会被重写)

静态分派机制(方法重载)

方法重载,是一种静态的行为。

image-20200216094439812

输出结果是什么?

我的猜想:两个都是Grandpa

实际结果:(bingo)image-20200216094647752

原因:在编译阶段,并识别不出子类。

解释:涉及到方法的静态分派

  1. Grandpa g1 = new Father()

    以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father

    我们可以得出这样一个结论:变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期方可确定。

  2. 根本原因:方法重载,是一种静态的行为。编译期就可以完全确定。(通过查看字节码就能看出来。字节码中已经确定)。invokevirtual

    image-20200216095316252

动态分派机制(方法重写)

image-20200216095905190

运行结果是什么?

我的猜想:apple, orange,orange

运行结果:(bingo)

image-20200216100051742

从字节码角度去分析结果的原因。

image-20200216100329595

new 关键字的的三个作用

  1. 开辟空间
  2. 执行构造方法
  3. 将对应的引用值给返回

为什么字节码汇中:还是调用的Fruit类的test方法?这和看到的输出结果不符合吧?

因为,这是方法的动态分派。

方法的动态分派涉及到一个重要概念:方法接收者。

invokevirtual字节码指令的多态查找流程(方法重写)

  1. 换句话说:查找这个方法是谁调用的

  2. 找到操作树栈顶的第一个元素所指向的对象的实际类型。(找到实例方法接收者)

  3. 如果找到了方法描述符和方法名称都完全相同的方法,且访问权限也校验通过。则直接返回当前实际类型对象的调用。

  4. 如果没有找到,则从子类往父类,从下往上,依次查找。找到,则返回。

  5. 如果一直没找到,则抛出异常。

比较方法重载(overload)和方法重写(overwrite),我们可以得到这样的结论:

方法重载是静态的,是编译器行为。

方法重写是动态的,是运行期行为。

java方法:虚方法表与动态分派机制

案例:

image-20200216103200060

运行结果是什么?

我的猜测:animal str / animal date (第二个掺杂了蒙的成分。)

运行结果(猜错了):

image-20200216103335260

第二行是Dog的原因:

根据静态分派和动态分派区分。 (如何区分呢?根据调用者的类型吧,如果调用者调用方法,调用的是当前类的重载的方法,那就是静态分派,如果调用者是重写的方法,则认为是动态分派。)

虚表

  • 针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable)
  • 针对于invokeInterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(Interface method tabke)
  • 续表,其实就是标记了指定方法的方法入口。

**

图中,粉红色的线,代表父类的方法。子类没有对其重写,所以子类的vtable中,直接指导父类的方法索引。从而提升了查找效率,节省了内存空间。前面提到的类加载的连接阶段中将符号引用转换为直接引用,就是这个操作

在初学多态的时候,初学者经常犯这样的错误。

从字节码角度,看看下面这个程序是不行的? 就是 父类不能调用子类的引用对象。

image-20200216105645717

结论:不行, child调不到子类的test3().

分析:因为方法在编译器会在编译阶段就进行静态静态分派。指向的还是父类的类,父类里面没有test3()方法。

基于栈的指令集与基于寄存器的指令集详细对比

  1. 编译执行和解释执行的概念,不同于java的编译后,然后再执行的这种概念
  2. CPU,寄存器,机器码由物理机直接执行,效率肯定高。*(涉及到了计算机底层原理知识)

现在JVM在执行Java代码的时候,通常都会将解释执行和编译执行二者结合起来进行。

  1. 所谓的解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令。
  2. 所谓的编译执行,是通过及时编译器(Just In Time,JIT)将字节码转换为机器码来执行。
  3. 现在JVM会根据代码热点(频率)来生成相应的本地机器码。(两者都有使用)

基于栈的指令集与基于寄存器的指令集之前的关系:

  1. JVM执行指令所采用的方式是基于栈的指令集。
  2. 基于栈的指令集主要的操作有入栈与出栈两种。
  3. 基于栈的指令集的优势在于它可以在不同平台之间移植。而基于寄存器的指令集是与硬件架构密切相关的,无法做到可移植。
  4. 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU来执行的,它是在高速缓冲区中进行执行的,速度要快的很多。虽然虚拟机可以采取一些优化手段,但是总体来说,基于栈的指令集的执行速度要慢。

如,要完成一个2-1的操作。

  1. 栈指令集:

image-20200216113017760

  1. 寄存器,10-1=1.直接执行机器码。

栈指令集的实例剖析

分析一个计算类

image-20200216113815446

这个myCalculate()方法对应的二进制. 有22个指令需要执行。

image-20200216113930074

stack = 2 ;表示这个栈最多容下2个值

locals = 6 ; 表示最大变量数为6

args_size = 1 ; this 参数

对于这22个指令,进行逐行分析

image-20200216114225563

  1. iconst_1 : bipush <i>:表示将1推到操作数栈当中。

    image-20200216114649500

  2. istore_1:index of <i> :把索引1的变量,将栈顶弹出来的值赋值到这个索引。

  3. istore 4 : 不是简写了。 局部变量表的索引:4 ,把栈顶的值,放在这个索引对应的变量值的位置里

    此时:image-20200216120249687

  4. iload_0:从局部变量中加载一个int类型的值 1, 2, 3,4

    iload_1和iload2执行完之后,左边的栈就有值了,为 2 、 1

  5. iadd: Add int,从操作数栈中弹出两个。 value1+ value2 = value。 然后把value再压回栈顶。

    此时这个指令执行完之后,是这样的:

  6. isub: 相减 (同 iadd)

  7. imul:相✖️乘 (同iadd)

  8. 全部执行完之后: 就算完了。

  9. 结论:JVM是基于操作数栈的实现

从字节码角度审视动态代理运作机制

Spring的动态代理

  1. 动态代理接口

    image-20200217071244972

  2. 接口的实现类

    image-20200217071310012

  3. 动态代理对象的实现 (实现了InvocationHandler)

    image-20200217071334108

  4. 客户端实现类

    image-20200217071434058

  5. 执行结果:

    image-20200217071500050

  6. 结论:对Subject的操作,全部由动态代理对象来代理操作。

  7. 如果打印subject对象的类,是 com.sun.proxy.$Proxy0,在程序运行期动态生成出来的,那么Proxy的父类为:java.lang.reflect.Proxy

image-20200217071632976

那么:com.sun.proxy.$Proxy0 。动态生成这个类的字节码长什么样?

生成代理类的源码:

image-20200217071935196

跟入:generateProxyClass()方法,是由JVM实现的。 再跟入这个方法

image-20200217073637198

image-20200217073750638

通过设置属性值sun.misc.ProxyGenerator.saveGeneratedFiles将值保存在磁盘上左边目录里面就已经有了

image-20200217074021077

generateClassFile()生成动态代理类的二进制文件的字节流。

谈谈你对动态代理的理解?(important)

我现在的理解:无非就是在不知道子类是什么的时候,在运行期间,就能够动态的生成Class文件。这就是动态代理。

字节码总结

对字节码有了整体的认知。

  • java字节码的整体结构(10个)
    • 魔数(CAFFBABE)
    • 版本 (向下兼容,向上不兼容,在这里就能判断)
    • 常量池 (索引从1开始的)
    • 访问标识符
    • 类名
    • 父类名
    • 接口
    • 成员变量
    • 方法(CODE) - init - clinit -method
    • 属性(sourcefile)

关于method里面是否可以不包含任何方法?甚至是init。 是可以的。之前说的关于java的构造方法是必须会有的,那是站在java层次和源码层次去考虑的。字节码规范里面并没有规范必须要有方法。java语言规范和JVM规范并不一是一个东西,不要混为一谈。

  • Class字节码中有两种数据类型

    • 字节数据直接量
    • 表(数组)
  • 字节码指令

  • 异常表

  • 动态代码的原理

把东西,变为自己的,才是自己的

原文地址:https://www.cnblogs.com/bigbaby/p/12348956.html