JAVA虚拟机系列-字节码结构

知识背景:JVM(Java Virtual Machine)是不能直接运行java的源码文件的,必须要经过编译变成字节码文件才能运行。

java代码运行过程

计算机是不能直接运行java代码的,必须要先运行java虚拟机,再由java虚拟机运行编译后的java代码。这个编译后的java代码,就是本文要介绍的java字节码。

为什么jvm不能直接运行java代码呢,这是因为在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,jvm才能正确识别代码转换后的指令并将其运行。

字节码文件的结构

class文件本质上是一个以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。

Class文件采用一种伪结构来存储数据,它有两种类型:无符号数。这里暂不详细的讲。

下图是接下来本文的简单的java例子编译后的文件,已经将class文件转为16进制。可以看到,我们熟悉的java代码经过编译转换为只有机器能识别的数据。
20191203-1

1.1 Class文件的结构属性

我们先从整体看下java字节码文件包含了哪些类型的数据:
20191203-2

1.2 用一个简单的示例代码分析

新建一个Test.java文件,然后编辑

public class Test{
	public static void main(String[] args){
		Integer a=1;
		Integer b=2;
		Integer c=a+b;	
		System.out.println(c+"");
	}
}

将该代码进行编译并保存在当前目录下:
20191203-3

编译完成目录下会多一个Test.class编译文件。使用16进制打开,内容如下:
20191203-4

在图中,前4个字节cafe babe就是魔数,紧接着魔数的4个字节代表的是Class文件的版本号。

5,6个字节表示的是次版本号(minor version),在上图中为0000,说明class文件的次版本号为 0 。

7,8个字节代表主版本号(major version),在上图中为0034,因为是16进制,计算可以得到该class文件的主版本号为52.

以此类推,根据java字节码的规则,可以依次解析成该字节码文件的所有内容。

当然,jdk中包含了一个可以将字节码文件“可视化”操作的命令javap,运行该命令,可以将java字节码文件解析为符合人类逻辑的文件。

查看java字节码可以使用 javap 命令,当然也可以使用IDE的插件进行查看(比如在Idea中使用jclasslib插件进行查看)

在当前目录下运行

javap -v Test.class

会出现我们可以正常阅读的字节码

Classfile /F:/Test.class                                                                                            
  Last modified 2018-10-21; size 752 bytes                                                                          
  MD5 checksum 4848a65fcbc8b0bc8ce60eb32172471b                                                                     
  Compiled from "Test.java"                                                                                         
public class Test                                                                                                   
  minor version: 0                                                                                                  
  major version: 52                                                                                                 
  flags: ACC_PUBLIC, ACC_SUPER                                                                                      
Constant pool:                                                                                                      
   #1 = Methodref          #13.#22        // java/lang/Object."<init>":()V                                          
       *** 省略部分代码                                                     
  #13 = Class              #36            // java/lang/Object                                                       
  #14 = Utf8               <init>                                                                                   
  #15 = Utf8               ()V                                                                                      
        *** 省略部分代码                                                     
  #22 = NameAndType        #14:#15        // "<init>":()V                                                           
        *** 省略部分代码                                                                   
  #36 = Utf8               java/lang/Object                                                                         
        *** 省略部分代码                                           
{                                                                                                                   
  public Test();                                                                                                    
    descriptor: ()V                                                                                                 
    flags: ACC_PUBLIC                                                                                               
    Code:                                                                                                           
      stack=1, locals=1, args_size=1                                                                                
         0: aload_0                                                                                                 
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V                               
         4: return                                                                                                  
      LineNumberTable:                                                                                              
        line 1: 0                                                                                                   
                                                                                                                    
  public static void main(java.lang.String[]);                                                                      
    descriptor: ([Ljava/lang/String;)V                                                                              
    flags: ACC_PUBLIC, ACC_STATIC                                                                                   
    Code:                                                                                                           
      stack=3, locals=4, args_size=1                                                                                
         0: iconst_1                                                                                                
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
         4: astore_1                                                                                                
         5: iconst_2                                                                                                
         6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
         9: astore_2                                                                                                
        10: aload_1                                                                                                 
        11: invokevirtual #3                  // Method java/lang/Integer.intValue:()I                              
        14: aload_2                                                                                                 
        15: invokevirtual #3                  // Method java/lang/Integer.intValue:()I                              
        18: iadd                                                                                                    
        19: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
        22: astore_3                                                                                                
        23: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;                   
        26: new           #5                  // class java/lang/StringBuilder                                      
        29: dup                                                                                                     
        30: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V                        
        33: aload_3                                                                                                 
        34: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;                                                                                                     
        37: ldc           #8                  // String                                                             
        39: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;                                                                                                     
        42: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;       
        45: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V           
        48: return                                                                                                  
      LineNumberTable:                                                                                              
        line 3: 0                                                                                                   
        line 4: 5                                                                                                   
        line 5: 10                                                                                                  
        line 6: 23                                                                                                  
        line 7: 48                                                                                                  
}                                                                                                                   
SourceFile: "Test.java"                                                                                                                                                                                          

1.3 分析由 javap -v 命令显示的字节码文件

我们从上到下看看例子的字节码文件各个字段代表的意思

字节码文件属性

Classfile /F:/Test.class                                                                                            
  Last modified 2018-10-21; size 752 bytes                                                                          
  MD5 checksum 4848a65fcbc8b0bc8ce60eb32172471b                                                                     
  Compiled from "Test.java"  

这一部分一看就很明白

  • Last modified 最后修改时间
  • size: 该字节码文件大小
  • MD5 checksum 该文件的md5
  • Compiled from "Test.java" 表示该字节码文件是由“Test.java”编译而来

类属性

public class Test                                                                                                   
  minor version: 0                                                                                                  
  major version: 52                                                                                                 
  flags: ACC_PUBLIC, ACC_SUPER          

这一部分表示Test类的各个属性

  • minor version: 0 表示可以支持最小的jdk版本,java的jdk都是向下兼容的,所以该值一般都为0
  • major version: 52 编译Test.java的jdk版本,我使用的是jdk1.8,所以52代表的是jdk8
  • flags: ACC_PUBLIC, ACC_SUPER 表示该类的访问属性

flags属性类型及含义如下

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为Public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义.
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举

常量池

Constant pool:                                                                                                      
   #1 = Methodref          #13.#22        // java/lang/Object."<init>":()V                                          
       *** 省略部分代码                                                     
  #13 = Class              #36            // java/lang/Object                                                       
  #14 = Utf8               <init>                                                                                   
  #15 = Utf8               ()V                                                                                      
        *** 省略部分代码                                                     
  #22 = NameAndType        #14:#15        // "<init>":()V                                                           
        *** 省略部分代码                                                                   
  #36 = Utf8               java/lang/Object                                                                         
        *** 省略部分代码    

常量池,可以理解为java资源的仓库,JVM运行方法时需要用到的数据都会在这里拿。下面从第一个常量开始分析各个字符的意义:

#1 = Methodref #13.#22 // java/lang/Object."<init>":()V

  1. #1 表示常量池值的序号,该数字表示为第一个
  2. #1 = Methodref 表示第一个值是一个方法的引用
  3. #13.#22 表示该方法的完整属性还需要 #13#22的来复合表示

#13 = Class #36 // java/lang/Object
#36 = Utf8 java/lang/Object

  1. #13 说明属性为class,而#36指明了该class为object类
  2. #1336结合一起,#13就得属性就是一个class类,且其具体的类为java.lang.Object
  3. 仔细观察 #13 后的注释,你发现了什么?其实在将java字节码可视化的时候,javap就已经将各个常量的属性都给关联好了

#22 = NameAndType #14:#15 // "<init>":()V

  1. NameAndType 这一看知道是代表名字和类型
  2. 按照上述的方式解析,会发现#22的名字为 <init>,类型为()V表示无参数并且返回值为void

根据上面的步骤分析可以得出:#1 引用的方法就是Test类的初始构造方法

方法表集合

  public Test();                                                                                                    
    descriptor: ()V                                                                                                 
    flags: ACC_PUBLIC                                                                                               
    Code:                                                                                                           
      stack=1, locals=1, args_size=1                                                                                
         0: aload_0                                                                                                 
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V                               
         4: return                                                                                                  
      LineNumberTable:                                                                                              
        line 1: 0    

这个Test()方法就是我们Test类中默认的构造方法,在我们没有重写或者没有指定对应的构造方法时,java编译的时候会默认生成一个空的构造方法。
下面我们来看看这个方法里面的各个字段代表的意思:
descriptor: ()V

·descriptor· 表示该方法的描述,这里表示的是该方法的参数为空,且方法值为void

Code:                                                                                                           
      stack=1, locals=1, args_size=1                                                                                
         0: aload_0                                                                                                 
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V                               
         4: return  
  1. stack 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1
  2. locals 局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。
  3. args_size方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数this
  4. 0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    表示运行方法时,各个指令的顺序,该顺序在下一章会进行详细的介绍。
  5. LineNumberTable 该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。
    start 表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。

总结

至此,把java字节码文件的各个属性都简单的介绍了一次。当然,还有很多其他的属性没有在示例代码中体现出来,这里就不进一步的介绍。本篇文章目的让大家对java字节码有个清晰的认识,在大脑中有个基础的概念,以后如果想深入了解,就知道从哪一方面入手,进行快速的学习。

参考文章:
深入理解JVM-字节码详解
轻松看懂字节码文件
详解java字节码class文件
《深入理解java虚拟机》