Java字节码浅析(—)

news/2024/7/3 7:10:38

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

比如:

//java学习交流:737251827  进入可领取学习资源及对十年开发经验大佬提问,免费解答!
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文件中的每一个方法都会包含一个局部变量表,如果这段代码在一个方法里面的话,你会在类文件的局部变量表中发现如下的一条记录。

 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

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

这里的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

未完待续。


http://www.niftyadmin.cn/n/4352411.html

相关文章

leetcode 字符串的排列 python3

给定两个字符串 s1 和 s2&#xff0c;写一个函数来判断 s2 是否包含 s1 的排列。 换句话说&#xff0c;第一个字符串的排列之一是第二个字符串的子串。 示例1:输入: s1 "ab" s2 "eidbaooo" 输出: True 解释: s2 包含 s1 的排列之一 ("ba").示…

python高级应用_Python高级应用程序设计任务

一、主题式网络爬虫设计方案(15分)1.主题式网络爬虫名称关于链家泉州本地租房信息的爬虫2.主题式网络爬虫爬取的内容与数据特征分析2.1爬取的内容租房类型&#xff0c;所属区县&#xff0c;详细地址&#xff0c;房屋面积&#xff0c;房屋朝向&#xff0c;房屋房型&#xff0c;房…

计划的定义与要素

要素&#xff1a;目标、时间、方案。 提出在未来一定时期内要达到的组织目标以及实现目标的方案途径。 http://wiki.mbalib.com/wiki/计划 可以把计划的内容简要地概括为八个方面&#xff0c;即&#xff1a; What(什么)——计划的目的、内容&#xff1b; Who&#xff08;谁&…

微信小程序 Array对象操作

转载于:https://www.cnblogs.com/liudabao123/p/8329642.html

《Java字节码浅析(二)》

条件语句 像if-else, switch这样的流程控制的条件语句&#xff0c;是通过用一条指令来进行两个值的比较&#xff0c;然后根据结果跳转到另一条字节码来实现的。 循环语句包括for循环&#xff0c;while循环&#xff0c;它们的实现方式也很类似&#xff0c;但有一点不同&#x…

LeetCode 字符串相乘 python3

描述 给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 示例 输入: num1 "123", num2 "456" 输出: "56088"class Solution:def multiply(self, num1: str, …

python中如何对一个属性或方法进行封装_中级软件设计师下半年上午试题附答案解析.doc...

1.在程序执行过程中&#xff0c;Cache与主存的地址映射是由()完成的。A&#xff0e;操作系统B&#xff0e;程序员调度C&#xff0e;硬件自动D&#xff0e;用户软件2.某四级指令流水线分别完成取指、取数、运算、保存结果四步操作。若完成上述操作的时间依次为8ns、9ns、4ns、8n…

LeetCode 翻转字符串里的单词 python3

给定一个字符串&#xff0c;逐个翻转字符串中的每个单词。 说明&#xff1a; 无空格字符构成一个 单词 。 输入字符串可以在前面或者后面包含多余的空格&#xff0c;但是反转后的字符不能包括。 如果两个单词间有多余的空格&#xff0c;将反转后单词间的空格减少到只含一个。…