javap查看class类文件结构

javap是JDK提供的一个命令行工具,使用javap可以对编译后的class文件进行反编译。这种反编译方式跟我们平常使用的如JD-GUI等一些Java反编译工具有所不同,开发中我们希望使用反编译工具是为了通过class文件获取Java源代码,而使用javap会为我们生成虚拟机字节码指令集。我们可以对照Java源代码和字节码,从而了解很多编译器内部的工作,并且可以根据JVM内存区域的划分,对局部变量、字段、静态变量、常量池在编译阶段有一个直观认识,当类加载进入JVM时,运行时数据区域的上述几种类型存储会有差异,毕竟一个是编译器做的工作,另外一个是JVM虚拟机做的事情。

本文示例使用的Java版本是"1.8.0_111"。

在进行介绍之前,我们先看一下Java源代码。

package com.yimi.demo;

public class SampleTest {

	private static final int LOCAL_CONSTANT = 100;
	static int staticVar = 12;
	String helloworld = "hello world";
	User user = new User("admin", 20);

	public void sayHello() {
		int localVar = 10;
		String localHello = new String("hello");
		User localUser = new User("localName", 18);
	}

}

javap是JDK提供的命令行工具,只要安装了JDK并且正确配置了环境变量,javap命令都可以直接使用。

javap是反编译的class文件而非Java源代码文件,因此第一步就是使用javac将Java源文件编译为class字节码文件,然后再使用javap进行反编译,javap常用命令如下:

用法: javap <options> <classes> 其中, 可能的选项包括: -help --help -? 输出此用法消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示最终常量 -classpath <path> 指定查找用户类文件的位置 -cp <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置

我们使用javap -v -p -s -sysinfo -constants xxx对上面Java源码编译后的class文件进行反编译,代码如下:

public class com.sunny.demo.SampleTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#32        // java/lang/Object."<init>":()V
   #2 = String             #33            // hello world
   #3 = Fieldref           #13.#34        // com/yimi/demo/SampleTest.helloworld:Ljava/lang/String;
   #4 = Class              #35            // com/yimi/demo/User
   #5 = String             #36            // admin
   #6 = Methodref          #4.#37         // com/yimi/demo/User."<init>":(Ljava/lang/String;I)V
   #7 = Fieldref           #13.#38        // com/yimi/demo/SampleTest.user:Lcom/yimi/demo/User;
   #8 = Class              #39            // java/lang/String
   #9 = String             #40            // hello
  #10 = Methodref          #8.#41         // java/lang/String."<init>":(Ljava/lang/String;)V
  #11 = String             #42            // localName
  #12 = Fieldref           #13.#43        // com/yimi/demo/SampleTest.staticVar:I
  #13 = Class              #44            // com/yimi/demo/SampleTest
  #14 = Class              #45            // java/lang/Object
  #15 = Utf8               LOCAL_CONSTANT
  #16 = Utf8               I
  #17 = Utf8               ConstantValue
  #18 = Integer            100
  #19 = Utf8               staticVar
  #20 = Utf8               helloworld
  #21 = Utf8               Ljava/lang/String;
  #22 = Utf8               user
  #23 = Utf8               Lcom/yimi/demo/User;
  #24 = Utf8               <init>
  #25 = Utf8               ()V
  #26 = Utf8               Code
  #27 = Utf8               LineNumberTable
  #28 = Utf8               sayHello
  #29 = Utf8               <clinit()>
  #30 = Utf8               SourceFile
  #31 = Utf8               SampleTest.java
  #32 = NameAndType        #24:#25        // "<init>":()V
  #33 = Utf8               hello world
  #34 = NameAndType        #20:#21        // helloworld:Ljava/lang/String;
  #35 = Utf8               com/yimi/demo/User
  #36 = Utf8               admin
  #37 = NameAndType        #24:#46        // "<init>":(Ljava/lang/String;I)V
  #38 = NameAndType        #22:#23        // user:Lcom/yimi/demo/User;
  #39 = Utf8               java/lang/String
  #40 = Utf8               hello
  #41 = NameAndType        #24:#47        // "<init>":(Ljava/lang/String;)V
  #42 = Utf8               localName
  #43 = NameAndType        #19:#16        // staticVar:I
  #44 = Utf8               com/yimi/demo/SampleTest
  #45 = Utf8               java/lang/Object
  #46 = Utf8               (Ljava/lang/String;I)V
  #47 = Utf8               (Ljava/lang/String;)V
{
  private static final int LOCAL_CONSTANT = 100;
    descriptor: I
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: int 100

  static int staticVar;
    descriptor: I
    flags: ACC_STATIC

  java.lang.String helloworld;
    descriptor: Ljava/lang/String;
    flags:

  com.sunny.demo.User user;
    descriptor: Lcom/yimi/demo/User;
    flags:

  public com.sunny.demo.SampleTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=5, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String hello world
         7: putfield      #3                  // Field helloworld:Ljava/lang/String;
        10: aload_0
        11: new           #4                  // class com/yimi/demo/User
        14: dup
        15: ldc           #5                  // String admin
        17: bipush        20
        19: invokespecial #6                  // Method com/yimi/demo/User."<init>":(Ljava/lang/String;I)V
        22: putfield      #7                  // Field user:Lcom/yimi/demo/User;
        25: return
      LineNumberTable:
        line 3: 0
        line 7: 4
        line 8: 10

  public void sayHello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: new           #8                  // class java/lang/String
         6: dup
         7: ldc           #9                  // String hello
         9: invokespecial #10                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
        12: astore_2
        13: new           #4                  // class com/yimi/demo/User
        16: dup
        17: ldc           #11                 // String localName
        19: bipush        18
        21: invokespecial #6                  // Method com/yimi/demo/User."<init>":(Ljava/lang/String;I)V
        24: astore_3
        25: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 13
        line 14: 25

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        12
         2: putstatic     #12                 // Field staticVar:I
         5: return
      LineNumberTable:
        line 6: 0
}
SourceFile: "SampleTest.java"

反编译之后内容很多,在这里我们对指令只做部分介绍,更详细的介绍可以参考Java bytecode instruction listings

其中major version和minor version分别代表了编译器的主次版本号,由于上面显示的major version是52,所以这里使用的是JDK8。flags是访问标记,用于标识字段、类、接口或者方法的方法权限及属性,常见的有ACC_PUBLIC、ACC_PRIVATE、ACC_STATIC、ACC_FINAL等。

上述class文件展示了四个主要部分:常量池、字段、构造方法和sayHello方法,接下来我们一一介绍。

Constant pool常量池

常量池中的数据都是以#加上一个数字开始,其实这是表示的一个索引,跟我们编程时所说的地址或者引用类似,只要能够找到这个索引标记就可以取得该索引对应的常量值。等号后面是常量值类型,在后面就是对应的常量值了。从这里可以看出,编译后的class常量池中的常量跟想象中的还是有差异的,平常开发中所说的String常量以及用static final修饰的常量仅仅是对应的一部分,可以说是很少的一部分。

一般常量池包含了下面的类型:

Integer 4 字节整型常量
Long 8 字节长整型常量
Float 4 字节Float类型常量
Double 8 字节常量
String 字符串常量指向常量池的另外一个包含真正字节 Utf8 编码的实体
Utf8 Utf8 编码的字符序列字节流
Class 一个 Class 常量,指向常量池的另一个 Utf8 实体,这个实体包含了符合 JVM 内部格式的类的全名(动态链接过程需要用到)
NameAndType 冒号(:)分隔的一组值,这些值都指向常量池中的其它实体。第一个值(“:”之前的)指向一个 Utf8 字符串实体,它是一个方法名或者字段名。第二个值指向表示类型的 Utf8 实体。对于字段类型,这个值是类的全名,对于方法类型,这个值是每个参数类型类的类全名的列表。
Fieldref, Methodref, InterfaceMethodref 点号(.)分隔的一组值,每个值都指向常量池中的其它的实体。第一个值(“.”号之前的)指向类实体,第二个值指向 NameAndType 实体。

在Java源文件中表示的全类名或者全限定名都是以.分割的,而上面反编译之后的非限定名(全类名是Java中的名称)都是以/分割的。Lcom/yimi/demo/User;Ljava/lang/String;前面都一个L前缀,L代表的是一个对象类型,更多的介绍我们在下文字段部分进行介绍。

在常量池这里我们可以看到定义的各种字段名称以及字符串值,也可以看到我们定义的常量LOCAL_CONSTANT对应的值100,其实这个值存储在#17对应的ConstantValue所对应的常量表中,JVM规范常量表中定义了14种常量类型,这里就不一一列出了。

<init>、<clinit()>、以及"<init>":()V在方法部分介绍。

字段

字段表示一般包括字段描述符descriptor、访问标记flags,用static final定义的常量还包括ConstantValue。

字段描述符一般分为三种类型:

  • 基本类型,直接使用字符表示即可,如I表示整型数,常见的还有D表示double类型、F表示float类型、J表示long类型等。
  • 对象类型,要使用对象标记L、对象的非限定名以及“;”,如Ljava/lang/String;
  • 数组类型,使用[字符,如double[][][],描述符为[[[D。
数据类型 描述符
byte B
char C
double D
float F
int I
long J
short S
boolean Z
特殊类型void V
对象类型 L + 对象非限定名+“;”。如 Ljava/lang/String; 表示 String 类型
数组类型 每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述符为 [I,二维数组 String[][] 的描述符为 [[java/lang/String

构造方法和方法

在介绍构造方法之前,先看反编译后最后的代码部分,使用了一个static{},其实这里对应的是<clinit()>()V,编译器在输出的时候只是把名字“美化”了一下而已。在class文件进行初始化的时候会执行<clinit()>()V方法,这时候会将静态字段赋值。

方法描述符包括0个或者多个参数描述符以及一个返回值描述符,假设一个方法如下:

public void myMethod(int x,double y,User u){
	...	
}

那么生成的描述符descriptor对应为(IDLcom/yimi/demo/User;)V,这里的IDLcom/yimi/demo/User;可以参考上面字段部分介绍,多个参数描述符之间不需要任何分割。 在刚接触Java时我们都知道每个类都需要一个构造方法,如果自己不设置,编译器会自动为我们增加一个默认的构造方法。但是stack=5, locals=1, args_size=1中,args_size的值是1,args_size代表了入参的个数,默认构造方法应该是无参的,可是这里为什么会多了一个参数呢?其实这个参数是this,即当前对象。通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。

如果我们使用的是低版本的编译器编译的class文件,然后再使用javap反编译,在每个方法的最后还会有一个如下的表示形式:

LocalVariableTable:
Start  Length  Slot  Name   Signature
	0      26     0  this   Lcom/yimi/demo/SampleTest;

LocalVariableTable表示本地变量表,存储的是一些局部变量信息。这里代表本地变量的作用域起始位置为0,作用域宽度为26(0-25),slot的起始位置也是0,名称为this,类型为SampleTest。

部分指令集描述

  • astore、astore_将一个reference类型的数据保存到本地变量表中,n是一个当前栈帧局部变量表的索引值。
  • aload、aload_从局部变量表中加载一个reference类型数据到操作数栈中,n同上是一个索引值。
  • invokespecial调用父类方法、初始化实例方法、私有方法。
  • putfield为指定类的实例字段赋值。
  • ldc将int、float或者String类型常量从常量池推送至栈顶。
  • putstatic为指定类的静态字段赋值。
  • new创建一个对象,将其引用值压入栈顶。
  • dup复制栈顶数据,并将复制值压入栈顶。
  • return代表从当前方法返回void。
  • bipush将单字节的常量值(-128~127)推送至栈顶。

参考资料

Java虚拟机规范 JavaSE 8版

JVM Internals

javap实例分析

Java bytecode instruction listings

JVM常量池浅析

评论

您确定要删除吗?删除之后不可恢复