英文原文链接,译文链接,原文作者: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)会用来存储这个新变量的值。然后这个变量会存储到局部变量区中对应的位置上。如果这个变量不是基础类型的话,本地变量槽上存的就只是一个引用。这个引用指向堆的里一个对象。

比如:

int i = 5;

编译后就成了

0: bipush      5
2: istore_0
 bipush  用来将一个字节作为整型数字压入操作数栈中,在这里5就会被压入操作数栈上。
 istore_0 

这是istore_这组指令集(译注:严格来说,这个应该叫做操作码,opcode ,指令是指操作码加上对应的操作数,oprand。

不过操作码一般作为指令的助记符,这里统称为指令)中的一条,这组指令是将一个整型数字存储到本地变量中。

n代表的是局部变量区中的位置,并且只能是0,1,2,3。再多的话只能用另一条指令istore了,这条指令会接受一个操作数,对应的是局部变量区中的位置信息。

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

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

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

Java类里面的字段是作为类对象实例的一部分,存储在堆里面的(类变量对应存储在类对象里面)。

关于字段的信息会添加到类文件里的field_info数组里,像下面这样:

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_pool[constant_pool_count – 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

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

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

public class SimpleClass {

    public int simpleField = 100;

}

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

 public int simpleField;
Signature: I
flags: ACC_PUBLIC

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

 public SimpleClass();
Signature: ()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: bipush 100
7: putfield #2 // Field simpleField:I
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中的字节码操作需要对应的数据,

但通常这些数据都太大了,存储在字节码里不适合,它们会被存储在常量池里面,

而字节码包含一个常量池里的引用 。当类文件生成的时候,其中的一块就是常量池:

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

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

比如

 public class SimpleClass {

     public final int simpleField = 100;

 }

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

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

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

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

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

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

不过在实例的构造方法中却再也找不到对应的初始化代码了。

因为static变量会在类的构造方法中进行初始化,并且它用的是putstatic指令而不是putfiled。

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

未完待续。

本文最早发表于本人个人博客:Java译站

Java字节码浅析(—)的更多相关文章

  1. Java字节码浅析(二)

    英文原文链接,译文链接,原文作者:James Bloom,译者:有孚 条件语句 像if-else, switch这样的流程控制的条件语句,是通过用一条指令来进行两个值的比较,然后根据结果跳转到另一条字 ...

  2. 在Eclipse里查看Java字节码

    要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...

  3. JAVA字节码解析

    Java字节码指令 Java 字节码指令及javap 使用说明 ### java字节码指令列表 字节码 助记符 指令含义 0x00 nop 什么都不做 0x01 aconst_null 将null推送 ...

  4. 【转】在Eclipse里查看Java字节码

    要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...

  5. Java字节码(.class文件)格式详解(一)

    原文链接:http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html 小介:去年在读<深入解析JVM>的时候写的,记得当时还 ...

  6. 通过Java字节码发现有趣的内幕之String篇(上)(转)

    原文出处: jaffa 很多时候我们在编写Java代码时,判断和猜测代码问题时主要是通过运行结果来得到答案,本博文主要是想通过Java字节码的方式来进一步求证我们已知的东西.这里没有对Java字节码知 ...

  7. 掌握Java字节码(转)

    Java是一门设计为运行于虚拟机之上的编程语言,因此它需要一次编译,处处运行(当然也是一次编写,处处测试).因此,安装到你系统上的JVM是原生的程序,而运行在它之上的代码是平台无关的.Java字节码就 ...

  8. Java字节码操纵框架ASM小试

    本文主要内容: ASM是什么 JVM指令 Java字节码文件 ASM编程模型 ASM示例 参考资料汇总 JVM详细指令 ASM是什么 ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既 ...

  9. Java:从面试题“i++和++i哪个效率高?"开始学习java字节码

    今天看到一道面试题,i++和++i的效率谁高谁低. 面试题的答案是++i要高一点. 我在网上搜了一圈儿,发现很多回答也都是同一个结论. 如果早个几年,我也会认同这个看法,但现在我负责任的说,这个结论是 ...

随机推荐

  1. 配置完centos 6以后,大概需要安装的软件(主要是yum)

    根据实践,把我的经验说一下,以后我自己也可以按照这个快速安装软件. 1. 配置源.百度网盘的tools/download/linux已经放了几个挺重要的 东西了. yum -y install epe ...

  2. 4.C#虚方法virtual详解

    C#虚方法virtual详解 在C++.Java等众多OOP语言里都可以看到virtual的身影,而C#作为一个完全面向对象的语言当然也不例外. 虚拟函数从C#的程序编译的角度来看,它和其它一般的函数 ...

  3. cocos2d JS-(JavaScript) 静态方法的例子

    function User(name, age) { this.name = name; this.age = age; } var user = new User('angela',26); Use ...

  4. cocos2d JS-(JavaScript) 基础语法间的函数方法相互调用

    1.函数嵌套函数 function calcuate(opr, a, b) { // 定义函数,opr - -> 符号,a,b - -> 数值 //定义 + 函数 function add ...

  5. cocos2d-x JS 字符串

    1.charCodeAt方法返回一个整数,代表指定位置字符的Unicode编码. strObj.charCodeAt(index) 说明: index将被处理字符的从零开始计数的编号.有效值为0到字符 ...

  6. openCV学习——一、图像读取、显示、输出

    openCV学习——一.图像读取.显示.输出   一.Mat imread(const string& filename,int flags=1),用于读取图片 1.参数介绍 filename ...

  7. Linux MySQL数据库文件同步及数据库备份

    Mysql数据库链接 mysql -uroot -p -hdatacenter.jiaofukeyan.com -P33069 1.文件同步 rsync -avz --delete root@(需要同 ...

  8. kali linux wifi破解(aircrack)

    需要一个能监听的网卡 airmon-ng start wlan0(監聽網卡) airmon-ng check kill(清除其他有影响的環境) airodump-ng mon0 (掃描附近wifi) ...

  9. ResourceExhaustedError 解决方案

    原因:网络层太多,运算量太大导致GPU资源耗尽 解决方案: 1.限制GPU的使用: config = tf.ConfigProto()config.gpu_options.per_process_gp ...

  10. Gitlab注册时报错:There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.

    今天注册时碰到以下问题: 上面的错误是因为注册时有一个google的验证码需要输入.但是中国无法访问google,因此无法访问并输入该验证码导致. 解决方案: FanQiang或者通过Github登陆 ...