Welcome everyone

Java 字节码初体验

java 汪明鑫 102浏览 0评论

概述

Java代码通过javac编译成字节码

JVM可以识别字节码,一处编写处处运行

JVM相当于是操作系统的中间一层抽象,屏蔽平台差异性

字节码并不是某种虚拟 CPU 的机器码,而是一种介于源码和机器码中间的一种抽象表示方法,不过字节码通过 JIT(Just in time)技术可以被进一步编译成机器码。

体验字节码

上小demo

public class Xinye {
    public static void main(String[] args) {
        System.out.println("xinyeshuaiqi");
    }
}

进入代码目录

~/Documents/code/xinye/src/main/java/pers/wmx/springbootfreemarkerdemo(master*) »

编译

javac Xinye.java

用xxd 命令以 16 进制的方式查看这个 class 文件。

xxd Xinye.class

00000000: cafe babe 0000 0034 001d 0a00 0600 0f09  .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507  ................
00000020: 0016 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 046d 6169  umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67  n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75  /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 5869 6e79 652e  rceFile...Xinye.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800  java............
00000090: 1901 000c 7869 6e79 6573 6875 6169 7169  ....xinyeshuaiqi
000000a0: 0700 1a0c 001b 001c 0100 2770 6572 732f  ..........'pers/
000000b0: 776d 782f 7370 7269 6e67 626f 6f74 6672  wmx/springbootfr
000000c0: 6565 6d61 726b 6572 6465 6d6f 2f58 696e  eemarkerdemo/Xin
000000d0: 7965 0100 106a 6176 612f 6c61 6e67 2f4f  ye...java/lang/O
000000e0: 626a 6563 7401 0010 6a61 7661 2f6c 616e  bject...java/lan
000000f0: 672f 5379 7374 656d 0100 036f 7574 0100  g/System...out..
00000100: 154c 6a61 7661 2f69 6f2f 5072 696e 7453  .Ljava/io/PrintS
00000110: 7472 6561 6d3b 0100 136a 6176 612f 696f  tream;...java/io
00000120: 2f50 7269 6e74 5374 7265 616d 0100 0770  /PrintStream...p
00000130: 7269 6e74 6c6e 0100 1528 4c6a 6176 612f  rintln...(Ljava/
00000140: 6c61 6e67 2f53 7472 696e 673b 2956 0021  lang/String;)V.!
00000150: 0005 0006 0000 0000 0002 0001 0007 0008  ................
00000160: 0001 0009 0000 001d 0001 0001 0000 0005  ................
00000170: 2ab7 0001 b100 0000 0100 0a00 0000 0600  *...............
00000180: 0100 0000 0700 0900 0b00 0c00 0100 0900  ................
00000190: 0000 2500 0200 0100 0000 09b2 0002 1203  ..%.............
000001a0: b600 04b1 0000 0001 000a 0000 000a 0002  ................
000001b0: 0000 0009 0008 000a 0001 000d 0000 0002  ................
000001c0: 000e                                     ..

javap -c Xinye.class

对类进行反编译。

Compiled from "Xinye.java"
public class pers.wmx.springbootfreemarkerdemo.Xinye {
  public pers.wmx.springbootfreemarkerdemo.Xinye();
    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 xinyeshuaiqi
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

invokespecial 指令调用实例初始化方法、私有方法、父类方法,#1 指的是常量池中的第一个,这里是方法引用java/lang/Object.”<init>”:()V,也即构造器函数

return,这个操作码属于 ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组中的一员,其中 i 表示 int,返回整数,同类的还有 l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void

getstatic #2,getstatic 获取指定类的静态域,并将其值压入栈顶,#2 代表常量池中的第 2 个,这里表示的是java/lang/System.out:Ljava/io/PrintStream;,其实就是java.lang.System 类的静态变量 out(类型是 PrintStream)

ldc #3、,ldc 用来将常量从运行时常量池压栈到操作数栈,#3 代表常量池的第三个(字符串 Hello, World)

invokevirtual #4,invokevirutal 指令调用一个对象的实例方法,#4 表示 PrintStream.println(String) 函数引用,并把栈顶两个元素出栈

javap -v Xinye.class

Classfile /Users/xinye/Documents/code/xinye/src/main/java/pers/wmx/springbootfreemarkerdemo/Xinye.class
  Last modified 2022-6-13; size 450 bytes
  MD5 checksum 5961df6f642b1f8b652adf6d1ed5eda1
  Compiled from "Xinye.java"
public class pers.wmx.springbootfreemarkerdemo.Xinye
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // xinyeshuaiqi
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // pers/wmx/springbootfreemarkerdemo/Xinye
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Xinye.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               xinyeshuaiqi
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               pers/wmx/springbootfreemarkerdemo/Xinye
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public pers.wmx.springbootfreemarkerdemo.Xinye();
    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 7: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String xinyeshuaiqi
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
}
SourceFile: "Xinye.java"

class文件结构

这一部分还是比较复杂的

class 文件由下面十个部分组成

  • 魔数(Magic Number)
  • 版本号(Minor&Major Version)
  • 常量池(Constant Pool)
  • 类访问标记(Access Flags)
  • 类索引(This Class)
  • 超类索引(Super Class)
  • 接口表索引(Interfaces)
  • 字段表(Fields)
  • 方法表(Methods)
  • 属性表(Attributes)

魔数

魔数 0xCAFEBABE 是 JVM 识别 .class 文件的标志,虚拟机在加载类文件之前会先检查这四个字节,如果不是 0xCAFEBABE 则拒绝加载该文件。

版本号

这里的主版本是 52(0x34),虚拟机解析这个类时就知道这是一个 Java 8 编译出的类,如果类文件的版本号高于 JVM 自身的版本号,加载该类会被直接抛出java.lang.UnsupportedClassVersionError异常

3 * (16 ^ 1) + 4 * (16 ^ 0) = 52 52是Java 8

常量池

对于 JVM 字节码来说,如果操作数非常小或者很常用的数字 0 之类的,这些操作数是内嵌到字节码中的。如果是字符串常量和较大的整数等,class 文件是把这些操作数存储在一个叫常量池(Constant Pool)的地方,当使用这些操作数时,使用的是常量池数据中的索引位置。

常量池结构

{
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
}

每个常量项都由两部分构成:表示类型的 tag 和表示内容的字节数组

cp_info {
    u1 tag;
    u1 info[];
}

常量池类型

类访问标记

常量池之后存储的是访问标记(Access flags),用来标识一个类是是不是final、abstract 等,由两个字节表示总共可以有 16 个标记位可供使用,目前只使用了其中的 8 个。

转载请注明:汪明鑫的个人博客 » Java 字节码初体验

喜欢 (0)

说点什么

您将是第一位评论人!

提醒
avatar
wpDiscuz