Java 进阶之字节码剖析(1)

  • 返回值

  • 局部变量表(Local Variables):存储方法用到的本地变量

  • 动态链接:在字节码中,所有的变量和方法都是以符号引用的形式保存在 class 文件的常量池中的,比如一个方法调用另外的方法,是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用,这么说可能有人还是不理解,所以我们先执行一下 javap -verbose Demo.class 命令来查看一下字节码中的常量池是咋样的

注意:以上只列出了常量池中的 部分符号引用

可以看到 Object 的 init 方法是由 #4.#16 表示的,而 #4 又指向了 #19,#19 表示 Object,#16 又指向了 #7.#8,#7 指向了方法名,#8 指向了 ()V(表示方法的返回值为 void,且无方法参数),字节码加载后,会把类信息加载到元空间(Java 8 以后)中的方法区中,动态链接会把这些符号引用替换为调用方法的直接引用,如下图示

那为什么要提供动态链接呢,通过上面这种方式绕了好几个弯才定位到具体的执行方法,效率不是低了很多吗,其实 主要是为了支持 Java 的多态 ,比如我们声明一个 Father f = new Son() 这样的变量,但执行 f.method() 的时候会绑定到 son 的 method(如果有的话),这就是用到了动态链接的技术,在运行时才能定位到具体该调用哪个方法,动态链接也称为后期绑定,与之相对的是静态链接(也称为前期绑定),即在编译期和运行期对象的方法都保持不变,静态链接发生在编译期,也就是说在程序执行前方法就已经被绑定, java 当中的方法只有final、static、private和构造方法是前期绑定的 。而动态链接发生在运行时, 几乎所有的方法都是运行时绑定的

举个例子来看看两者的区别,一目了解

class Animal{

public void eat(){

System.out.println(“动物进食”);

}

}

class Cat extends Animal{

@Override

public void eat() {

super.eat();//表现为早期绑定(静态链接)

System.out.println(“猫进食”);

}

}

public class AnimalTest {

public void showAnimal(Animal animal){

animal.eat();//表现为晚期绑定(动态链接)

}

}

  • 操作数栈(Operand Stack) :程序主要由指令和操作数组成,指令用来说明这条操作做什么,比如是做加法还是乘法,操作数就是指令要执行的数据,那么指令怎么获取数据呢,指令集的架构模型分为 基于栈的指令集架构 和 基于寄存器的指令集架构 两种,JVM 中的指令集属于前者,也就是说任何操作都是用栈来管理,基于栈指令可以更好地实现跨平台,栈都是是在内存中分配的,而寄存器往往和硬件挂钩,不同的硬件架构是不一样的,不利于跨平台,当然基于栈的指令集架构缺点也很明显,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈),而寄存器是在CPU的高速缓存区,相较而言, 基于栈的速度要慢不少 ,这也是为了跨平台而做出的一点性能牺牲,毕竟鱼和熊掌不可兼得。

Java 字节码技术简介

注意线程中还有一个「PC 程序计数器」,是每个线程独有的,记录着当前线程所执行的字节码的行号指示器,也就是指向下一条指令的地址,也就是将执行的指令代码。由执行引擎读取下一条指令。我们先来看下看一下字节码长啥样。假设我们有以下 Java 代码

package com.mahai;

public class Demo {

private int a = 1;

public static void foo() {

int a = 1;

int b = 2;

int c = (a + b) * 5;

}

}

执行 javac Demo.java 后可以看到其字节码如下

字节码是给 JVM 看的,所以我们需要将其翻译成人能看懂的代码,好在 JDK 提供了反解析工具 javap ,可以根据字节码反解析出 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等信息。我们执行以下命令来看下根据字节码反解析的文件长啥样(更详细的信息可以执行 javap -verbose 命令,在本例中我们重点关注 Code 区是如何执行的,所以使用了 javap -c 来执行

javap -c Demo.class

转换成这种形式可读性强了很多,那么aload_0,invokespecial 这些表示什么含义呢, javap 是怎么根据字节码来解析出这些指令出来的呢

首先我们需要明白什么是指令, 指令=操作码+操作数 ,操作码表示这条指令要做什么,比如加减乘除,操作数即操作码操作的数,比如 1+ 2 这条指令,操作码其实是加法,1,2 为操作数,在 Java 中每个操作码都由一个字节表示,每个操作码都有对应类似 aload_0,invokespecial,iconst_1 这样的助记符,有些操作码本来就包含着操作数,比如字节码 0x04 对应的助记符为 iconst_1, 表示 将 int 型 1 推送至栈顶,这些操作码就相当于指令,而有些操作码需要配合操作数才能形成指令,如字节码 0x10 表示 bipush,后面需要跟着一个操作数,表示 将单字节的常量值(-128~127)推送至栈顶 。以下为列出的几个字节码与助记符示例

| 字节码 | 助记符 | 表示含义 |

| — | — | — |

| 0x04 | iconst_1 | 将int型1推送至栈顶 |

| 0xb7 | invokespecial | 调用超类构建方法, 实例初始化方法, 私有方法 |

| 0x1a | iload_0 | 将第一个int型本地变量推送至栈顶 |

| 0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶 |

至此我们不难明白 javap  的作用了,它主要就是找到字节码对应的的助记符然后再展示在我们面前的,我们简单看下上述的默认构造方法是如何根据字节码映射成助记符并最终呈现在我们面前的:

最左边的数字是 Code 区中每个字节的偏移量,这个是保存在 PC 的程序计数中的,比如如果当前指令指向 1,下一条就指向 4

另外大家不难发现,在源码中其实我们并没有定义默认构造函数,但在字节码中却生成了,而且你会发现我们在源码中定义了 private int a = 1; 但这个变量赋值的操作却是在构造方法中执行的(下文会分析到),这就是理解字节码的意义: 它可以反映 JVM 执行程序的真正逻辑 ,而源码只是表象,要深入分析还得看字节码!

接下来我们就来瞧一瞧构造方法对应的指令是如何执行的,首先我们来看一下在 JVM 中指令是怎么执行的。

  1. 首先 JVM 会为每个方法分配对应的局部变量表,可以认为它是一个数组,每个坑位(我们称为 slot)为方法中分配的变量,如果是实例方法,这些局部变量可以是 this, 方法参数,方法里分配的局部变量,这些局部变量的类型即我们熟知的 int,long 等八大基本,还有引用,返回地址,每个 slot 为 4 个字节,所以像 Long , Double 这种 8 个字节的要占用 2 个 slot, 如果这个方法为实例方法,则第一个 slot 为 this 指针, 如果是静态方法则没有 this 指针

  2. 分配好局部变量表后,方法里如果涉及到赋值,加减乘除等操作,那么这些指令的运算就需要依赖于操作数栈了,将这些指令对应的操作数通过压栈,弹栈来完成指令的执行

比如有 int i = 69 这样的指令,对应的字码节指令如下

0:bipush 69

2:istore_0

其在内存中的操作过程如下

可以看到主要分两步:第一步首先把 69 这个 int 值压栈,然后再弹栈,把 69 弹出放到局部变量表 i 对应的位置,istore_0 表示弹栈,将其从操作数栈中弹出整型数字存储到本地变量中,0 表示本地变量在局部变量表的第 0 个 slot

理解了上面这个操作,我们再来看一下默认构造函数对应的字节码指令是如何执行的

首先我们需要先来理解一下上面几个指令

  • aload_0:从局部变量表中加载第 0 个 slot 中的对象引用到操作数栈的栈顶,这里的 0 表示第 0 个位置,也就是 this

  • invokespecial:用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及 可见的超类方法,在此例中表示调用父类的构造器(因为 #1 符号引用指向对应的 init 方法)

  • iconst_1:将 int 型 1推送至栈顶

  • putfield:它接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,在这里这个字段是 a。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都会从操作数栈顶上 pop 出来。前面的 aload_0 指令已经把包含这个字段的对象(this)压到操作数栈上了,而后面的 iconst_1 又把 1 压到栈里。最后 putfield 指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的 a 这个字段的值更新成了 1。

接下来我们来详细解释以上以上助记符代表的含义

  • 第一条命令 aload_0,表示从局部变量表中加载第 0 个 slot 中的对象引用到操作数栈的栈顶,也就是将 this 加载到栈顶,如下

  • 第二步 invokespecial #1,表示弹栈并且执行 #1 对应的方法,#1 代表的含义可以从旁边的解释( # Method java/lang/Object." ":()V )看出,即调用父类的初始化方法,这也印证了那句话: 子类初始化时会从初始化父类

  • 之后的命令 aload_0 , iconst_1 , putfied #2 图解如下

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
-d2EantM3-1715707895721)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值