fish-stack / Java

Java技能树。个人博客请访问issues区域
0 stars 0 forks source link

class文件的结构 #8

Open bitfishxyz opened 5 years ago

bitfishxyz commented 5 years ago

我们知道我们的Java代码最终会编译成class文件,然后我们把class文件交给jvm来执行。所以就有了下面的问题:

好,接下我们就来讨论这些内容。

编译.java文件

我们知道,我们编写的java源代码文件都是以.java结尾的。.java文件对于人类来说是便于阅读的,.class文件对于机器来说是便于阅读的。编译的本质就是把对机器不友好的文件格式转化为便于JVM理解的文件。

首先呢,我们来写一个最简单的Java类:

public class Hello {
}

这个类看起没有任何多余的信息。但是我们知道,Java语言有下面的规则:

所以实际的类应该是这样的:

public class Hello extends Object {
    public Hello() {
    }
}

这一点大家知道就行了,我们在分析编译后的class文件时,能够证明这一点。

然后我们用javac编译一下上面的类:

$  javac Hello.java

不出意外的话,我们就可以拿到我们的Hello.class文件了。

查看class文件的二进制格式

class文件是一个二级制文件,我们需要使用能够解析二进制文件的工具来查看。这里我使用了xxd,你可以使用其他工具。

xxd的用法

$ xxd Hello.class
00000000: cafe babe 0000 0036 000d 0a00 0300 0a07  .......6........
00000010: 000b 0700 0c01 0006 3c69 6e69 743e 0100  ........<init>..
00000020: 0328 2956 0100 0443 6f64 6501 000f 4c69  .()V...Code...Li
00000030: 6e65 4e75 6d62 6572 5461 626c 6501 000a  neNumberTable...
00000040: 536f 7572 6365 4669 6c65 0100 0a65 6c6c  SourceFile...ell
00000050: 6f2e 6a61 7661 0c00 0400 0501 0005 4865  o.java........He
00000060: 6c6c 6f01 0010 6a61 7661 2f6c 616e 672f  llo...java/lang/
00000070: 4f62 6a65 6374 0021 0002 0003 0000 0000  Object.!........
00000080: 0001 0001 0004 0005 0001 0006 0000 001d  ................
00000090: 0001 0001 0000 0005 2ab7 0001 b100 0000  ........*.......
000000a0: 0100 0700 0000 0600 0100 0000 0100 0100  ................
000000b0: 0800 0000 0200 09                        .......

上面就是编译后的class文件的内容。注意,它是用16进制来展示的。

哎呀,天书,好像有点看不懂。

没关系,首先我们来看看前面的cafe babe

我们知道,我们的磁盘上的文件有png类型的、pdf类型的等,不同的文件格式有不同的处理规则。那么我们的应用程序如何区分不同的文件类型呢?

但是后面的字节是什么意思呢?看起来好像不太清楚。我们需要使用其他工具来帮我我们。

使用javap来查看

这里我们javep 看一下,这是一款jdk自带的开发工具。

$ javap -v Hello.class
Classfile /Users/apple/Downloads/x1hnd1rk/TemplateJava/src/Hello.class
  Last modified Jul 5, 2019; size 184 bytes
  MD5 checksum fc62d8578590d39db2069961c80bdcde
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // Hello
  super_class: #3                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 1
Constant pool:
   #1 = Methodref          #3.#10         // java/lang/Object."<init>":()V
   #2 = Class              #11            // Hello
   #3 = Class              #12            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               SourceFile
   #9 = Utf8               Hello.java
  #10 = NameAndType        #4:#5          // "<init>":()V
  #11 = Utf8               Hello
  #12 = Utf8               java/lang/Object
{
  public Hello();
    descriptor: ()V
    flags: (0x0001) 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
}
SourceFile: "Hello.java"

上面的信息就是我们的二进制文件,按照jvm语言规范解析出来的。

这时候你可能会有疑问,为什么我们的字节码可以转换为上面的格式,或者说转化的规则是什么?

要解决这个问题你就需要详细阅读JVM规范了。具体的规则对我们学习JVM字节码来说并不是很重要,它只对jvm的实现者和一些操作字节的框架的作者来说有用,所以我这里就忽略。如果你很敢兴趣,那么你可以自行阅读JVM规范,后续的内容我都是基于javap的结果来介绍的。

这里我提供了一些图片,介绍了字节码和javap生成的分析文件的关系。大家可以感性的了解一下。

bitfishxyz commented 5 years ago

点解图片来查看大图

20190705103410 20190705134622 20190705134645 20190705134717

上面就是class文件的二级制格式和javap输出结果的对应关系。

为了让读者可以感性的了解,我花了一下午,根据jvm规范的规则,把二级制格式和javap的结果做了对比。。。走过路过的朋友,赏个star做辛苦费吧。。。

bitfishxyz commented 5 years ago

分析class文件

这里我写了一个新的类,它编译后的class文件是这样的,你能够读出来这个类的特征吗?

$ javac Hello.java
$ javap -v Hello.class
Classfile /Users/apple/Downloads/x1hnd1rk/TemplateJava/src/Hello.class
  Last modified Jul 5, 2019; size 412 bytes
  MD5 checksum f67b1372d221fbabc8cab2a175f87060
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #5                          // Hello
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #6.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #19            // Hello world
   #4 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #22            // Hello
   #6 = Class              #23            // java/lang/Object
   #7 = Utf8               age
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               sayHello
  #14 = Utf8               SourceFile
  #15 = Utf8               Hello.java
  #16 = NameAndType        #9:#10         // "<init>":()V
  #17 = Class              #24            // java/lang/System
  #18 = NameAndType        #25:#26        // out:Ljava/io/PrintStream;
  #19 = Utf8               Hello world
  #20 = Class              #27            // java/io/PrintStream
  #21 = NameAndType        #28:#29        // println:(Ljava/lang/String;)V
  #22 = Utf8               Hello
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/lang/System
  #25 = Utf8               out
  #26 = Utf8               Ljava/io/PrintStream;
  #27 = Utf8               java/io/PrintStream
  #28 = Utf8               println
  #29 = Utf8               (Ljava/lang/String;)V
{
  int age;
    descriptor: I
    flags: (0x0000)

  public Hello();
    descriptor: ()V
    flags: (0x0001) 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 void sayHello();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         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
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Hello.java"
bitfishxyz commented 5 years ago

类名和访问权限

public class Hello
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #5                          // Hello
  super_class: #6                         // java/lang/Object

上面这个片段中,minor version 和 major version就是我们的class文件的版本号,它的值是54。这个版本和我们的java语言的版本的对应关系如下:

Java版本 class文件版本
1.0.2  45.0 ≤ v ≤ 45.3
1.1     45.0 ≤ v ≤ 45.65535
1.2     45.0 ≤ v ≤ 46.0
1.3     45.0 ≤ v ≤ 47.0
1.4     45.0 ≤ v ≤ 48.0
5.0    45.0 ≤ v ≤ 49.0
6       45.0 ≤ v ≤ 50.0
7       45.0 ≤ v ≤ 51.0
8       45.0 ≤ v ≤ 52.0
9       45.0 ≤ v ≤ 53.0
10     45.0 ≤ v ≤ 54.0
11     45.0 ≤ v ≤ 55.0

当前的是java10的版本。

而上面的this_class字段就是我们当前类的类名,它的值是#5,也就是字符串常量池中索引为5的位置,也就是Hello,所以我们当前类的类名是Hello

同理,通过super_class这个字段知道我们当前类的父类是java.lang.Object这个类。

而通过flags: (0x0021) ACC_PUBLIC, ACC_SUPER这个字段可知我们的类是public的类。

字段和方法

interfaces: 0, fields: 1, methods: 2, attributes: 1

通过这里我们可以读出,这个类有一个字段,两个方法,没有实现其他接口。

  int age;
    descriptor: I
    flags: (0x0000)

说明这个类有个int类型的字段age,使用的是默认的访问修饰符。

还有连个方法 public Hello() public void sayHello(),方法体的内容暂时不用管。

综合就是这样的:

public class Hello {
    int age;
    public Hello(){
      // ...
    }
    public void sayHello(){
        //...
    }
}

这里也可以看出,我们不需要读懂class文件的二级制格式,只需要读懂javap的结果就行了。