关于后端:字节码初体验从HelloWorld开始

1次阅读

共计 4484 个字符,预计需要花费 12 分钟才能阅读完成。

不学习底层常识可能不会妨碍你成为一个称职的程序员,但兴许会妨碍你成为一个优良的程序员。我所了解的底层常识,是指编程或开发所依赖的平台(或者框架、工具)的常识。对于 Java 开发者来说,虚拟机、字节码就是其底层常识。

这篇文章咱们以输入 “Hello, World” 来开始字节码之旅,如果之前没有怎么接触过字节码的话,这篇文章应该可能让你对字节码有一个最根本的意识

java 文件如何变成 .class 文件

新建一个 Hello.java 文件,源码如下:

public class Hello {public static void main(String[] args) {System.out.println("Hello, World");
    }
}

Java 从源文件到执行的过程如下图所示

JDK 工具 javac(java 编译器)帮咱们实现了源文件编译成 JVM 可辨认的 class 文件的工作。在命令行中执行javac Hello.java,能够看到生成了 Hello.class 文件。用xxd 命令以 16 进制的形式查看这个 class 文件。

xxd Hello.class 
00000000: cafe babe 0000 0034 0022 0a00 0600 1409  .......4."......
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07  ................
00000020: 001b 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63  umberTable...Loc
00000050: 616c 5661 7269 6162 6c65 5461 626c 6501  alVariableTable.
00000060: 0004 7468 6973 0100 074c 4865 6c6c 6f3b  ..this...LHello;

魔数 0xCAFEBABE

class 文件的头四个字节称为魔数(Magic Number),能够看到 class 的魔数为 0xCAFEBABE。很多文件都以魔数来进行文件类型的辨别,比方 PDF 文件的魔数是%PDF-(16 进制0x255044462D),png 文件的魔数是\x89PNG(0x89504E47)。文件格式的制定者能够自在的抉择魔数值,只有魔数值还没有被宽泛的采纳过且不会引起混同即可。

Java 晚期开发者选用了这样一个浪漫气息的魔数,高司令有解释这一段 轶事。这个魔数值在 Java 还称为 Oak 语言的时候就曾经确定下来了。

这个魔数是 JVM 辨认 .class 文件的标记,虚拟机在加载类文件之前会先查看这四个字节,如果不是 0xCAFEBABE 则回绝加载该文件,更多对于字节码格局的阐明,咱们会在前面的文章中缓缓介绍。

javap 详解

类文件是二进制块,想间接与它打交道比拟艰巨,然而很多状况下咱们必须了解类文件。比方服务器上的接口出了 bug,从新打包部署当前问题并没有解决,为了找出起因你可能须要看一下部署当前的 class 文件到底是不是咱们想要的。还有一种状况跟你单干的开发商跑路了,只给你留下一堆编译过的代码,没有源代码,当出 bug 时咱们须要钻研这些类文件,看问题出在哪里。好在 JDK 提供了专门用来剖析类文件的工具:javap,用来不便的窥探 class 文件外部的细节。javap 有比拟多的参数选项,其中 -c -v -l -p -s 是最罕用的。

Usage: javap <options> <classes>
where possible options include:
  -help  --help  -?        Print this usage message
  -version                 Version information
  -v  -verbose             Print additional information
  -l                       Print line number and local variable tables
  -public                  Show only public classes and members
  -protected               Show protected/public classes and members
  -package                 Show package/protected/public classes
                           and members (default)
  -p  -private             Show all classes and members
  -c                       Disassemble the code
  -s                       Print internal type signatures
  -sysinfo                 Show system info (path, size, date, MD5 hash)
                           of class being processed
  -constants               Show final constants
  -classpath <path>        Specify where to find user class files
  -cp <path>               Specify where to find user class files
  -bootclasspath <path>    Override location of bootstrap class files

- c 选项

最罕用的选项是 -c,能够对类进行反编译。执行javap -c Hello 的输入后果如下

Compiled from "Hello.java"
public class Hello {public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3  // String Hello, World
       5: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

下面代码后面的数字示意从办法开始算起的字节码偏移量

  • 3 ~ 7 行:能够看到尽管没有写 Hello 类的结构器函数,编译器会主动加上一个默认结构器函数
  • 5 行:aload_0 这个操作码是 aload_x 格局操作码中的一个。它们用来把对象援用加载到操作数栈。x 示意正在被拜访的局部变量数组的地位。在这里的 0 代表什么呢?咱们晓得非动态的函数都有第一个默认参数,那就是 this,这里的 aload_0 就是把 this 入栈
  • 6 行:invokespecial #1,invokespecial 指令调用实例初始化办法、公有办法、父类办法,#1 指的是常量池中的第一个,这里是办法援用 java/lang/Object."<init>":()V,也即结构器函数
  • 7 行:return,这个操作码属于 ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组中的一员,其中 i 示意 int,返回整数,同类的还有 l 示意 long,f 示意 float,d 示意 double,a 示意 对象援用。没有前缀类型字母的 return 示意返回 void

到此为止,默认结构器函数就讲完了,接下来,咱们来看 9 ~ 14 行的 main 函数

  • 11 行:getstatic #2,getstatic 获取指定类的动态域,并将其值压入栈顶,#2 代表常量池中的第 2 个,这里示意的是 java/lang/System.out:Ljava/io/PrintStream;,其实就是 java.lang.System 类的动态变量 out(类型是 PrintStream)
  • 12 行:ldc #3、,ldc 用来将常量从运行时常量池压栈到操作数栈,#3 代表常量池的第三个(字符串 Hello, World)
  • 13 行:invokevirtual #4,invokevirutal 指令调用一个对象的实例办法,#4 示意 PrintStream.println(String) 函数援用,并把栈顶两个元素出栈

-p 选项

默认状况下,javap 会显示拜访权限为 public、protected 和默认(包级 protected)级别的办法,加上 -p 选项当前能够显示 private 办法和字段

-v 选项

javap 加上 -v 参数的输入更多具体的信息,比方栈大小、办法参数的个数。

public Hello();
    stack=1, locals=1, args_size=1
        
public static void main(java.lang.String[]);
    stack=2, locals=1, args_size=1

为什么 Hello()main()args_size 都等于 1 呢?明明 Hello 的结构器函数没有参数的呀?对于非动态函数,this 对象会作为函数的隐式第一个参数,所以 Hello()args_size=1 对于动态 main 函数,不须要 this 对象,它的参数就是 String[] args 这个数组,也等于1

- s 选项

javap 还有一个好用的选项 -s,能够输入签名的类型描述符。咱们能够看下 Hello.java 所有的办法签名

javap -s Hello  
Compiled from "Hello.java"
public class Hello {public Hello();
    descriptor: ()V
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
}

能够看到 main 函数的办法签名是 ([Ljava/lang/String;)V。JVM 外部应用办法签名与咱们日常浏览的办法签名不太一样,然而前面会频繁遇到,次要分为两局部字段描述符和办法描述符。

字段描述符(Field Descriptor),是一个示意类、实例或局部变量的语法符号,它的示意模式是紧凑的,比方 int 是用 I 示意的。残缺的类型描述符如下表

办法描述符(Method Descriptor)
示意一个办法所需参数和返回值信息,示意模式为 (ParameterDescriptor*) ReturnDescriptor。ParameterDescriptor 示意参数类型,ReturnDescriptor 示意返回值信息,当没有返回值时用 V 示意。比方办法Object foo(int i, double d, Thread t) 的描述符为(IDLjava/lang/Thread;)Ljava/lang/Object;

小结

这篇文章解说了一个输入 “Hello, World” 的字节码的细节,一起来回顾一下要点:

  • 第一,class 文件的魔数是具备浪漫气息的 0xCAFEBABE;
  • 第二,咱们解说了字节码剖析的利器 javap 的各个参数具体的用法。第三,解说了字段描述符与办法描述符在 JVM 层面的示意规定,不便咱们前面文章的了解。

本文由 mdnice 多平台公布

正文完
 0