Java字节码浅析(—)

简介:

英文原文链接译文链接,原文作者:James Bloom,译者:有孚

明白Java代码是如何编译成字节码并在JVM上运行的非常重要,这有助于理解程序运行的时候究竟发生了些什么。理解这点不仅能搞清语言特性是如何实现的,并且在做方案讨论的时候能清楚相应的副作用及权衡利弊。

本文介绍了Java代码是如何编译成字节码并在JVM上执行的。想了解JVM的内部结构以及字节码运行时用到的各个内存区域,可以看下我前面的一篇关于JVM内部细节的文章

本文分为三部分,每一部分都分成几个小节。每个小节都可以单独阅读,不过由于一些概念是逐步建立起来的,如果你依次阅读完所有章节会更简单一些。每一节都会覆盖到Java代码中的不同结构,并详细介绍了它们是如何编译并执行的。

1. 第一部分, 基础概念

变量

局部变量

JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。

局部变量可以是以下这些类型:

* char
* long
* short
* int
* float
* double
* 引用
* 返回地址

除了long和double类型外,每个变量都只占局部变量区中的一个变量槽(slot),而long及double会占用两个连续的变量槽,因为这些类型是64位的。

当一个新的变量创建的时候,操作数栈(operand stack)会用来存储这个新变量的值。然后这个变量会存储到局部变量区中对应的位置上。如果这个变量不是基础类型的话,本地变量槽上存的就只是一个引用。这个引用指向堆的里一个对象。

比如:


1 int i = 5;

编译后就成了


1 0: bipush      5
2 2: istore_0
bipush 用来将一个字节作为整型数字压入操作数栈中,在这里5就会被压入操作数栈上。
istore_0 这是istore_ 这组指令集(译注:严格来说,这个应该叫做操作码,opcode ,指令是指操作码加上对应的操作数,oprand。不过操作码一般作为指令的助记符,这里统称为指令)中的一条,这组指令是将一个整型数字存储到本地变量中。n代表的是局部变量区中的位置,并且只能是0,1,2,3。再多的话只能用另一条指令istore了,这条指令会接受一个操作数,对应的是局部变量区中的位置信息。

这条指令执行的时候,内存布局是这样的:

class文件中的每一个方法都会包含一个局部变量表,如果这段代码在一个方法里面的话,你会在类文件的局部变量表中发现如下的一条记录。


1 LocalVariableTable:
2         Start  Length  Slot  Name   Signature
3           0      1      1     i         I
字段

Java类里面的字段是作为类对象实例的一部分,存储在堆里面的(类变量对应存储在类对象里面)。关于字段的信息会添加到类文件里的field_info数组里,像下面这样:


01 ClassFile {
02     u4 magic;
03     u2 minor_version;
04     u2 major_version;
05     u2 constant_pool_count;
06     cp_info contant_pool[constant_pool_count – 1];
07     u2 access_flags;
08     u2 this_class;
09     u2 super_class;
10     u2 interfaces_count;
11     u2 interfaces[interfaces_count];
12     u2 fields_count;
13     field_info fields[fields_count];
14     u2 methods_count;
15     method_info methods[methods_count];
16     u2 attributes_count;
17     attribute_info attributes[attributes_count];
18 }

另外,如果变量被初始化了,那么初始化的字节码会加到构造方法里。

下面这段代码编译了之后:


1 public class SimpleClass {
2  
3     public int simpleField = 100;
4  
5 }

如果你用javap进行反编译,这个被添加到了field_info数组里的字段会多出一段描述信息。


1 public int simpleField;
2     Signature: I
3     flags: ACC_PUBLIC

初始化变量的字节码会被加到构造方法里,像下面这样:


01 public SimpleClass();
02   Signature: ()V
03   flags: ACC_PUBLIC
04   Code:
05     stack=2, locals=1, args_size=1
06        0: aload_0
07        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
08        4: aload_0
09        5: bipush        100
10        7: putfield      #2                  // Field simpleField:I
11       10: return
aload_0 从局部变量数组中加载一个对象引用到操作数栈的栈顶。尽管这段代码看起来没有构造方法,但是在编译器生成的默认的构造方法里,就会包含这段初始化的代码。第一个局部变量正好是this引用,于是aload_0把this引用压到操作数栈中。aload_0是aload_指令集中的一条,这组指令会将引用加载到操作数栈中。n对应的是局部变量数组中的位置,并且也只能是0,1,2,3。还有类似的加载指令,它们加载的并不是对象引用,比如iload_,lload_,fload_,和dload_, 这里i代表int,l代表long,f代表float,d代表double。局部变量的在数组中的位置大于3的,得通过iload,lload,fload,dload,和aload进行加载,这些指令都接受一个操作数,它代表的是要加载的局部变量的在数组中的位置。
invokespecial 这条指令可以用来调用对象实例的构造方法,私有方法和父类中的方法。它是方法调用指令集中的一条,其它的还有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.这里的invokespecial指令调用的是父类也就是java.lang.Object的构造方法。
bipush 它是用来把一个字节作为整型压到操作数栈中的,在这里100会被压到操作数栈里。
putfield 它接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,在这里这个字段是simpleField。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都 会从操作数栈顶上pop出来。前面的aload_0指令已经把包含这个字段的对象压到操作数栈上了,而后面的bipush又把100压到栈里。最后putfield指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的simpleField这个字段的值更新成了100。

上述代码执行的时候内存里面是这样的:

这里的putfield指令的操作数引用的是常量池里的第二个位置。JVM会为每个类型维护一个常量池,运行时的数据结构有点类似一个符号表,尽管它包含的信息更多。Java中的字节码操作需要对应的数据,但通常这些数据都太大了,存储在字节码里不适合,它们会被存储在常量池里面,而字节码包含一个常量池里的引用 。当类文件生成的时候,其中的一块就是常量池:


01 Constant pool:
02    #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()V
03    #2 = Fieldref           #3.#17         //  SimpleClass.simpleField:I
04    #3 = Class              #13            //  SimpleClass
05    #4 = Class              #19            //  java/lang/Object
06    #5 = Utf8               simpleField
07    #6 = Utf8               I
08    #7 = Utf8               <init>
09    #8 = Utf8               ()V
10    #9 = Utf8               Code
11   #10 = Utf8               LineNumberTable
12   #11 = Utf8               LocalVariableTable
13   #12 = Utf8               this
14   #13 = Utf8               SimpleClass
15   #14 = Utf8               SourceFile
16   #15 = Utf8               SimpleClass.java
17   #16 = NameAndType        #7:#8          //  "<init>":()V
18   #17 = NameAndType        #5:#6          //  simpleField:I
19   #18 = Utf8               LSimpleClass;
20   #19 = Utf8               java/lang/Object
常量字段(类常量)

带有final标记的常量字段在class文件里会被标记成ACC_FINAL.

比如


1 public class SimpleClass {
2  
3     public final int simpleField = 100;
4  
5 }

字段的描述信息会标记成ACC_FINAL:


1 public static final int simpleField = 100;
2     Signature: I
3     flags: ACC_PUBLIC, ACC_FINAL
4     ConstantValue: int 100

对应的初始化代码并不变:


1 4: aload_0
2 5: bipush        100
3 7: putfield      #2                  // Field simpleField:I
静态变量

带有static修饰符的静态变量则会被标记成ACC_STATIC:


1 public static int simpleField;
2     Signature: I
3     flags: ACC_PUBLIC, ACC_STATIC

不过在实例的构造方法中却再也找不到对应的初始化代码了。因为static变量会在类的构造方法中进行初始化,并且它用的是putstatic指令而不是putfiled。


1 static {};
2   Signature: ()V
3   flags: ACC_STATIC
4   Code:
5     stack=1, locals=0, args_size=0
6        0: bipush         100
7        2: putstatic      #2                  // Field simpleField:I
8        5: return

未完待续。

目录
相关文章
|
消息中间件 存储 Java
最经典的消息中间件:RabbitMQ
最经典的消息中间件:RabbitMQ
1703 1
|
1月前
|
缓存 运维 搜索推荐
KKCE网站测速与性能优化实战:快速排查网站卡顿提升访问速度
网站加载速度是用户体验与SEO排名的关键。KKCE提供零基础、低成本的测速优化方案,涵盖服务器响应、页面加载、交互体验三大维度,支持多终端、全地域精准诊断,并指导常态化性能巡检,助站长科学提速、长效稳质。(239字)
179 2
|
分布式计算 Hadoop 分布式数据库
Hadoop生态系统介绍(二)大数据技术Hadoop入门理论系列之一----hadoop生态圈介绍
Hadoop生态系统介绍(二)大数据技术Hadoop入门理论系列之一----hadoop生态圈介绍
1105 2
|
开发工具 Android开发
rk平台Android12屏幕永不休眠
rk平台Android12屏幕永不休眠
719 1
|
存储 监控 数据可视化
在Linux中,如何查看系统日志?
在Linux中,如何查看系统日志?
|
存储 编解码 网络协议
阿里云服务器计算型和通用型四代云服务器实例区别及选择参考
目前阿里云在售的云服务器中,计算型和通用型实例规格都包含了第5代、第6代、第7代和最新第八代倚天云服务器产品,例如计算型实例中有c5、c6、c7、c8y实例,而通用型实例有g5、g6、g7、g8y等实例,有的新手用户并不清楚这四代产品之间的差别,本文为大家展示这四代云服务器实例在规格、CPU(核)、内存(G)、计算、存储、内存以及不同配置的指标数据等方面为大家做个对比,让大家了解一下他们之间的不同,以供参考和选择。
阿里云服务器计算型和通用型四代云服务器实例区别及选择参考
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其独特的“不重复性”要求,彻底改变了处理唯一性约束数据的方式。
【10月更文挑战第14天】从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其独特的“不重复性”要求,彻底改变了处理唯一性约束数据的方式。本文深入探讨Set的核心理念,并通过示例代码展示了HashSet和TreeSet的特点和应用场景。
211 2
|
人工智能 Java 语音技术
开源上新|FunASR离线文件转写GPU软件包1.0
开源上新|FunASR离线文件转写GPU软件包1.0
|
SQL 关系型数据库 MySQL
Mysql中from多表跟join表的区别
Mysql中from多表跟join表的区别
1330 0
|
Linux 编译器 开发者
Linux嵌入式系统之交叉编译
Linux嵌入式系统之交叉编译
615 0