05类加载机制篇(D3_深入理解Class)

目录

一、Class常量池理解

1. 常量池在class文件的什么位置?

2. 常量池的里面是怎么组织的?

3. 常量池项 (cp_info) 的结构是什么?

4. int和float数据类型的常量在常量池中是怎样表示和存储的?

5. long和 double数据类型的常量在常量池中是怎样表示和存储的?

6. String类型的字符串常量在常量池中是怎样表示和存储的?

7. 类文件中定义的类名和类中使用到的类在常量池中是怎样被组织和存储的?

8. 哪些字面量会进入常量池中?

二、class文件中的引用和特殊字符串

1. 符号引用

2. 直接引用

3. 引用替换的时机

4. 特殊字符串字面量

4.1. 类的全限定名

4.2. 描述符

1> 各类型的描述符

2> 字段描述符

3> 方法描述符

4> 总结

三、通过javap命令分析java指令

1. javap命令简述

2. javap测试及内容详解

例子1:分析以下代码反汇编之后结果

例子2:下面一个例子

3. 知识小结

四、案例分析

1. class文件解读

2. javap显示结果解读


一、Class常量池理解

1. 常量池在class文件的什么位置?

2. 常量池的里面是怎么组织的?

cp_info:常量池项

constant_pool_count:常量池计算器

3. 常量池项 (cp_info) 的结构是什么?

JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下:

所以根据cp_info中的tag 不同的值,可以将cp_info 更细化为以下结构体:

现在让我们看一下细化了的常量池的结构会是类似下图所示的样子:

4. int和float数据类型的常量在常量池中是怎样表示和存储的?

Java语言规范规定了 int类型和Float 类型的数据类型占用 4 个字节的空间。

那么存在于class字节 码文件中的该类型的常量是如何存储的呢?、

举例:建下面的类 IntAndFloatTest.java,在这个类中,我们声明了五个变量,但是取值就两种int 类型的10 和Float类型的11f。

package com.kkb.jvm;  
public class IntAndFloatTest {  
     private final int a = 10;  
    private final int b = 10; 
     //private int c = 20;

     private float c = 11f;  
     private float d = 11f;  
     private float e = 11f;  
}

然后用编译器编译成IntAndFloatTest.class字节码文件,我们通过javap -v IntAndFloatTest 指令来看一下其常量池中的信息,可

以看到虽然我们在代码中写了两次10 和三次11f,但是常量池 中,就只有一个常量10 和一个常量11f,如下图所示:

从结果上可以看到常量池第#8 个常量池项(cp_info) 就是CONSTANT_Integer_info,值为10;

第#23个常量池项(cp_info) 就是CONSTANT_Float_info,值为11f。

(常量池中其他的东西先别纠结 啦,我们后面会一一讲解的哦)。

代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#8 定位到第#8 个常量池项 (cp_info),即值为 10的结构体

CONSTANT_Integer_info,而用到float类型的11f时,也会指向常 量池的指针值#23来定位到第#23个常量池项(cp_info) 即值为11f的结

构体CONSTANT_Float_info。

如下图所示:

5. long和 double数据类型的常量在常量池中是怎样表示和存储的?

Java语言规范规定了 long 类型和 double类型的数据类型占用8 个字节的空间。

那么存在于class 字节码文件中的该类型的常量是如何存储的呢?

举例:建下面的类 LongAndDoubleTest.java,在这个类中,我们声明了六个变量,但是取值就两种Long 类型

的-6076574518398440533L 和Double 类型的10.1234567890D。

package com.kkb.jvm;   
public class LongAndDoubleTest {  
    private long a = -6076574518398440533L;  
     private long b = -6076574518398440533L;  
     private long c = -6076574518398440533L;  
     private double d = 10.1234567890D;  
     private double e = 10.1234567890D;  
     private double f = 10.1234567890D;  
}

然后用编译器编译成 LongAndDoubleTest.class 字节码文件,我们通过javap -v LongAndDoubleTest指令来看一下其常量池中的信息,

可以看到虽然我们在代码中写了三 次-6076574518398440533L 和三次10.1234567890D,但是常量池中,就只有一个常

量-6076574518398440533L 和一个常量10.1234567890D,如下图所示:

从结果上可以看到常量池第 #18 个常量池项(cp_info) 就是CONSTANT_Long_info,值 为 -6076574518398440533L ;

第 #26个常量池项(cp_info) 就是 CONSTANT_Double_info,值为10.1234567890D。

(常量池中其他的东西先别纠结啦,我们会面会一一讲解的哦)。

代码中所有用到 long 类型-6076574518398440533L 的地方,会使用指向常量池的指针值#18 定位 到第 #18 个常量池项(cp_info),即

值为-6076574518398440533L 的结构体CONSTANT_Long_info,而用到double类型的10.1234567890D时,也会指向常量池的指针值

#26来定 位到第 #26 个常量池项(cp_info) 即值为10.1234567890D的结构体CONSTANT_Double_info。

如 下图所示:

6. String类型的字符串常量在常量池中是怎样表示和存储的?

对于字符串而言,JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中。这么 说可能有点摸不着北,我们先从直

观的Java源码中中出现的用双引号"" 括起来的字符串来看,在编译 器编译的时候,都会将这些字符串转换成CONSTANT_String_info结构

体,然后放置于常量池中。

其结 构如下所示:

如上图所示的结构体,CONSTANT_String_info结构体中的string_index的值指向了 CONSTANT_Utf8_info结构体,

而字符串的utf-8编码数据就在这个结构体之中。

如下图所示:

请看一例,定义一个简单的StringTest.java类,然后在这个类里加一个"JVM原理" 字符串,

然后, 我们来看看它在class文件中是怎样组织的。

package com.kkb.jvm;  
public class StringTest {  
    private String s1 = "JVM原理";  
    private String s2 = "JVM原理";  
    private String s3 = "JVM原理";  
    private String s4 = "JVM原理";  
}

将Java源码编译成StringTest.class文件后,在此文件的目录下执行 javap -v StringTest 命令, 会看到如下的常量池信息的轮廓:

(PS :使用javap -v 指令能看到易于我们阅读的信息,查看真正的字节码文件可以使用HEXWin、 NOTEPAD++、UtraEdit 等工具。)

在面的图中,我们可以看到CONSTANT_String_info结构体位于常量池的第#15个索引位置。

而存 放"Java虚拟机原理" 字符串的 UTF-8编码格式的字节数组被放到CONSTANT_Utf8_info结构体中,

该 结构体位于常量池的第#16个索引位置。上面的图只是看了个轮廓,让我们再深入地看一下它们的组织 吧。

请看下图:

由上图可见:“JVM原理”的UTF-8编码的数组是:

4A564D E5 8E 9FE7 90 86,并且存入了CONSTANT_Utf8_info结构体中。

7. 类文件中定义的类名和类中使用到的类在常量池中是怎样被组织和存储的?

JVM会将某个Java 类中所有使用到了的类的完全限定名 以二进制形式的完全限定名 封装成

CONSTANT_Class_info结构体中,然后将其放置到常量池里。

CONSTANT_Class_info 的 tag 值为 7 。

其结构如下

Tips:类的完全限定名和二进制形式的完全限定名

在某个Java源码中,我们会使用很多个类,比如我们定义了一个 ClassTest的类,并把它放到

com.kkb.jvm 包下,则 ClassTest类的完全限定名为com.kkb.jvm.ClassTest,将JVM编译 器将类编译成class文件后,此完全限定名

在class文件中,是以二进制形式的完全限定名存储 的,即它会把完全限定符的"."换成"/" ,即在class文件中存储的 ClassTest类的完

全限定名 称是"com/kkb/jvm/ClassTest"。因为这种形式的完全限定名是放在了class二进制形式的字 节码文件中,所以就称之为 二

进制形式的完全限定名。

举例,我们定义一个很简单的ClassTest类,来看一下常量池是怎么对类的完全限定名进行存储的。

package com.jvm;  
import  java.util.Date;  
public class ClassTest {  
    private Date date =new Date();  
}

将Java源码编译成ClassTest.class文件后,在此文件的目录下执行 javap -v ClassTest 命令, 会看到如下的常量池信息的轮廓:

如上图所示,在ClassTest.class文件的常量池中,共有 3 个CONSTANT_Class_info结构体,分别 表示ClassTest 中用到的Class信息。

我们就看其中一个表示com/jvm.ClassTest的CONSTANT_Class_info 结构体。

它在常量池中的位置是#1,它的name_index值为#2,它指向了常量 池的第2 个常量池项,如下所示:

注意:

对于某个类而言,其class文件中至少要有两个CONSTANT_Class_info常量池项,用来表示自己的类 信息和其父类信息。

(除了java.lang.Object类除外,其他的任何类都会默认继承自java.lang.Object)

如果类声明实现了某些接口,那么接口的信息也会生成对应的CONSTANT_Class_info常量池项。

除此之外,如果在类中使用到了其他的类,只有真正使用到了相应的类,JDK编译器才会将类的信息组 成CONSTANT_Class_info常量池项

放置到常量池中。

如下图:

package com.kkb.jvm;  
import java.util.Date;  
public  class Other{  
     private Date date;  
     public Other() {  
         Date da;  
     }  
}

上述的Other的类,在JDK将其编译成class文件时,常量池中并没有java.util.Date对应的

CONSTANT_Class_info常量池项,为什么呢?

在Other类中虽然定义了Date类型的两个变量date、da,但是JDK编译的时候,认为你只是声明

了“Ljava/util/Date”类型的变量,并没有

实际使用到Ljava/util/Date类。将类信息放置到常量 池中的目的,是为了在后续的代码中有可能会反复用

到它。很显然,JDK在编译Other类的时候,会解 析到Date类有没有用到,发现该类在代码中就没有用到

过,所以就认为没有必要将它的信息放置到常量 池中了。

将上述的Other类改写一下,仅使用new Date(),如下图所示:

package com.kkb.jvm;  
import java.util.Date;  
public  class Other{  
   public Other()  {  
       new Date();  
   }  
}

这时候使用javap -v Other ,可以查看到常量池中有表示java/util/Date的常量池项:

知识小结

  1. 对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成 CONSTANT_Class_info常量池项放置到常量池中;
  2. 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有 对应的CONSTANT_Class_info常量池项;
  3. 类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成 CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器 只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中。

8. 哪些字面量会进入常量池中?

结论

  1. final类型的8种基本类型的值会进入常量池。
  2. 非final类型(包括static的)的8种基本类型的值,只有double、float、long的值会进入常量 池。
  3. 常量池中包含的字符串类型字面量(双引号引起来的字符串值)。

测试代码

public class Test{
    private int int_num = 110;
    private char char_num = 'a';
    private short short_num = 120;
    private float float_num = 130.0f;
    private double double_num = 140.0;
    private byte byte_num = 111;
    private long long_num = 3333L;
    private long long_delay_num;
    private boolean boolean_flage = true;
    public void init() {
        this.long_delay_num = 5555L;
    }
}

使用javap命令打印的结果如下:

二、class文件中的引用和特殊字符串

1. 符号引用

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定 位到目标即可。

例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等类型的常量出现。

符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。

在Java中,一个java类将会编译成一个class文件。

在编译时,java类并不知道所引用的类的实际地 址,因此只能使用符号引用来代替。

比如 org.simple.People类 引用了 org.simple.Language类 ,在编译时People类并不知道Language 类的实际内存地址,因此只能

使用符号 org.simple.Language (假设是这个,当然实际中是由类似于 CONSTANT_Class_info的常量来表示的)来表示Language

类的地址。

各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字 面量形式明确定义在Java虚

拟机规范的Class文件格式中。

2. 直接引用

直接引用可以是:

  1. 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指 向方法区的指针)
  2. 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  3. 一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引 用一般不会相同。

如果有了直接引用,那引用的目标必定已经被加载入内存中了。

3. 引用替换的时机

符号引用替换为直接引用的操作发生在类加载过程(加载 -> 连接(验证、准备、解析) -> 初始化)中 的解析阶段,会将符号引用转换(替换)为对应的直接引用,放入运行时常量池中。

后面我们会通过一些问题,来更深入的了解class常量池相关的知识点!!!

4. 特殊字符串字面量

特殊字符串包括三种: 类的全限定名, 字段和方法的描述符, 特殊方法的方法名。

下面我们就分别介绍这三种特殊字符串。

4.1. 类的全限定名

Object类,在源文件中的全限定名是 java.lang.Object 。

而class文件中的全限定名是将点号替换成“/” 。 也就是 java/lang/Object 。

源文件中一个类的名字, 在class文件中是用全限定名表述的。

4.2. 描述符

1> 各类型的描述符

对于字段的数据类型,其描述符主要有以下几种

  • 基本数据类型(byte、char、double、float、int、long、short、boolean):除 long 和 boolean,其他基本数据类型的描述符用对应单词的大写首字母表示。long 用 J 表示, boolean 用 Z 表示。
  • void:描述符是 V。
  • 对象类型:描述符用字符 L 加上对象的全限定名表示,如 String 类型的描述符为 Ljava/lang/String 。
  • 数组类型:每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述 符为 [I ,二维数组 String 的描述符为 [[Ljava/lang/String 。

数据类型

描述符

byte

B

char

C

double

D

float

F

int

I

long

J

short

S

boolean

Z

特殊类型void

V

对象类型

“L” + 类型的全限定名 + “;”。如 Ljava/lang/String; 表示 String 类 型

数组类型

若干个 “[” + 数组中元素类型的对应字符串,如一维数组 int[] 的描述符为

2> 字段描述符

字段的描述符就是字段的类型所对应的字符或字符串。

如:

int i 中, 字段i的描述符就是 I 
Object o中, 字段o的描述符就是 Ljava/lang/Object;  
double[][] d中, 字段d的描述符就是 [[D
3> 方法描述符

方法的描述符比较复杂, 包括所有参数的类型列表和方法返回值。

它的格式是这样的:

(参数1类型 参数2类型 参数3类型 ...)返回值类型

注意事项:

不管是参数的类型还是返回值类型, 都是使用对应字符和对应字符串来表示的, 并且参数列表 使用小括号括起来,

并且各个参数类型之间没有空格, 参数列表和返回值类型之间也没有空格。

方法描述符举例说明如下:

方法描述符

方法声明

()I

int getSize()

()Ljava/lang/String;

String toString()

([Ljava/lang/String;)V

void main(String[] args)

()V

void wait()

(JI)V

void wait(long timeout, int nanos)

(ZILjava/lang/String;II)Z

boolean regionMatches(boolean ignoreCase, int toOffset, String other, int ooffset, int len)

([BII)I

int read(byte[] b, int off, int len )

()[[Ljava/lang/Object;

Object getObjectArray()

特殊方法的方法名

首先要明确一下, 这里的特殊方法是指的类的构造方法和类型初始化方法。

构造方法就不用多说了, 至于类型的初始化方法, 对应到源码中就是静态初始化块。

也就是说, 静态初始化块, 在class文件中是以一个方法表述的, 这个方法同样有方法描述符和方法名,具体如 下:

  • 类的构造方法的方法名使用字符串 表示
  • 静态初始化方法的方法名使用字符串 表示。
  • 除了这两种特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同。
4> 总结
  1. 方法和字段的描述符中, 不包括字段名和方法名,字段描述符中只包括字段类型, 方法描述 符中只包括参数列表和返回值类型。
  2. 无论method()是静态方法还是实例方法,它的方法描述符都是相同的。尽管实例方法除了传递 自身定义的参数,还需要额外传递参数this,但是这一点不是由方法描述符来表达的。参数this 的传递,是由Java虚拟机实现在调用实例方法所使用的指令中实现的隐式传递。

三、通过javap命令分析java指令

1. javap命令简述

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区 (汇编指令)、本地变量表、

异常表和代码行偏移量映射表、常量池等等信息。 当然这些信息中,有些信息(如本地变量表、指令和代码行偏移量映射表、常量

池中方法的参数名称等 等)需要在使用javac编译成class文件时,指定参数才能输出,比如,你直接javac xx.java,就不 会在生成

对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。如果 你使用的eclipse,则默认情况下,eclipse

在编译时会帮你生成局部变量表、指令和代码行偏移量映 射表等信息的。

通过反编译生成的汇编代码,我们可以深入的了解java代码的工作机制。比如我们可以查看i++;这行 代码实际运行时是先获取变量

i的值,然后将这个值加1,最后再将加1后的值赋值给变量i。 通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等

信息,甚至可以看到槽位复用等信 息。

javap的用法格式:

javap <options> <classes>

其中classes就是你要反编译的class文件。 在命令行中直接输入javap或javap -help可以看到javap的options有如下选项:

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

一般常用的是-v -l -c三个选项。

  • javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用 到的常量池等信息。
  • javap -l 会输出行号和本地变量表信息。
  • javap -c 会对当前class字节码进行反编译生成汇编代码。

查看汇编代码时,需要知道里面的jvm指令,可以参考官方文档:

Chapter 6. The Java Virtual Machine Instruction Set

另外通过jclasslib工具也可以看到上面这些信息,而且是可视化的,效果更好一些。

2. javap测试及内容详解

前面已经介绍过javap输出的内容有哪些,东西比较多,这里主要介绍其中code区(汇编指令)、局部变 量表和代码行偏移映射三个

部分。 如果需要分析更多的信息,可以使用javap -v进行查看。

另外,为了更方便理解,所有汇编指令不单拎出来讲解,而是在反汇编代码中以注释的方式讲解。

下面写段代码测试一下:

例子1:分析以下代码反汇编之后结果

public class TestDate {
    
    private int count = 0;
    
    public static void main(String[] args) {
        TestDate testDate = new TestDate();
        testDate.test1();
   }
    
    public void test1(){
        Date date = new Date();
        String name1 = "wangerbei";
        test2(date,name1); 
        System.out.println(date+name1);
   }
    public void test2(Date dateP,String name2){
        dateP = null;
        name2 = "zhangsan";
   }
    public void test3(){
        count++;
   }
    
    public void  test4(){
        int a = 0;
       {
            int b = 0;
            b = a+1;
       }
        int c = a+1;
   }
}

上面代码通过JAVAC -g 生成class文件,然后通过javap命令对字节码进行反汇编:

$ javap -c -l TestDate

得到下面内容(指令等部分是我参照着官方文档总结的):

Warning: Binary file TestDate contains com.justest.test.TestDate
Compiled from "TestDate.java"

public class com.justest.test.TestDate {
  //默认的构造方法,在构造方法执行时主要完成一些初始化操作,包括一些成员变量的初始化赋值等操作
 public com.justest.test.TestDate();
   Code:
       0: aload_0 //从本地变量表中加载索引为0的变量的值,也即this的引用,压入栈

       1: invokespecial #10 //出栈,调用java/lang/Object."<init>":()V 初始化对象,就是this指定的对象的<init>方法完成初始化
       4: aload_0  // 4到6表示,调用this.count = 0,也即为count复制为0。这里this引用入栈
       5: iconst_0 //将常量0,压入到操作数栈

       6: putfield     //出栈前面压入的两个值(this引用,常量值0), 将0取出,并赋
值给count
       9: return//指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中指令前面的数字
   LineNumberTable:
     line 5: 0
     line 7: 4
     line 5: 9
    //局部变量表,start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置
(this生命周期从头0到结尾10),slot就是这个变量在局部变量表中的槽位(槽位可复用),name就是变量名称,Signatur局部变量类型描述
   LocalVariableTable:
     Start Length Slot Name   Signature
         0      10     0 this   Lcom/justest/test/TestDate;
 
 public static void main(java.lang.String[]);
   Code:
// new指令,创建一个class com/justest/test/TestDate对象,new指令并不能完全创建一
个对象,对象只有在初,只有在调用初始化方法完成后(也就是调用了invokespecial指令之后),
对象才创建成功,
       0: new  //创建对象,并将对象引用压入栈
       3: dup //将操作数栈定的数据复制一份,并压入栈,此时栈中有两个引用值
       4: invokespecial #20 //pop出栈引用值,调用其构造函数,完成对象的初始化
       7: astore_1 //pop出栈引用值,将其(引用)赋值给局部变量表中的变量testDate
       8: aload_1  //将testDate的引用值压入栈,因为testDate.test1();调用了testDate,这里使用aload_1从局部变量表中获得对应的变量testDate的值并压入操作数栈
       9: invokevirtual #21 // Method test1:()V 引用出栈,调用testDate的test1()方法
      12: return //整个main方法结束返回

   LineNumberTable:
     line 10: 0

     line 11: 8

     line 12: 12

    //局部变量表,testDate只有在创建完成并赋值后,才开始声明周期

   LocalVariableTable:
     Start Length Slot Name   Signature
         0      13     0 args   [Ljava/lang/String;
         8       5     1 testDate   Lcom/justest/test/TestDate;
 public void test1();
   Code:
       0: new           #27                 // 0到7创建Date对象,并赋值给date变量
       3: dup
       4: invokespecial #29                 // Method java/util/Date."<init>":()V
       7: astore_1
       8: ldc           #30     // String wangerbei,将常量“wangerbei”压入栈
      10: astore_2  //将栈中的“wangerbei”pop出,赋值给name1
      11: aload_0 //11到14,对应test2(date,name1);默认前面加this.
      12: aload_1 //从局部变量表中取出date变量
      13: aload_2 //取出name1变量
      14: invokevirtual #32                 // Method test2: (Ljava/util/Date;Ljava/lang/String;)V 调用test2方法
  // 17到38对应System.out.println(date+name1);
      17: getstatic     #36                 // Field 
java/lang/System.out:Ljava/io/PrintStream;

  //20到35是jvm中的优化手段,多个字符串变量相加,不会两两创建一个字符串对象,而使用

StringBuilder来创建一个对象

      20: new           #42                 // class 
java/lang/StringBuilder

      23: dup
      24: invokespecial #44                 // Method 
java/lang/StringBuilder."<init>":()V

      27: aload_1
      28: invokevirtual #45                 // Method 
java/lang/StringBuilder.append:
(Ljava/lang/Object;)Ljava/lang/StringBuilder;

      31: aload_2
      32: invokevirtual #49                 // Method 
java/lang/StringBuilder.append:
(Ljava/lang/String;)Ljava/lang/StringBuilder;

      35: invokevirtual #52                 // Method 
java/lang/StringBuilder.toString:()Ljava/lang/String;

      38: invokevirtual #56                 // Method 
java/io/PrintStream.println:(Ljava/lang/String;)V invokevirtual指令表示基于
类调用方法

      41: return

   LineNumberTable:
     line 15: 0

     line 16: 8

     line 17: 11

     line 18: 17

     line 19: 41

   LocalVariableTable:
     Start Length Slot Name   Signature
             0      42     0 this   Lcom/justest/test/TestDate;
             8      34     1 date   Ljava/util/Date;
            11      31     2 name1   Ljava/lang/String;
 
 public void test2(java.util.Date, java.lang.String);
   Code:
       0: aconst_null //将一个null值压入栈

       1: astore_1 //将null赋值给dateP
       2: ldc           #66       // String zhangsan 从常量池中取出字符
串“zhangsan”压入栈中

       4: astore_2 //将字符串赋值给name2
       5: return

   LineNumberTable:
     line 22: 0

     line 23: 2

     line 24: 5

   LocalVariableTable:
     Start Length Slot Name   Signature
             0       6     0 this   Lcom/justest/test/TestDate;
             0       6     1 dateP   Ljava/util/Date;
             0       6     2 name2   Ljava/lang/String;
 
 public void test3();
   Code:
       0: aload_0 //取出this,压入栈

       1: dup   //复制操作数栈栈顶的值,并压入栈,此时有两个this对象引用值在操作数组栈
       2: getfield #12// Field count:I this出栈,并获取其count字段,然后压入栈,
此时栈中有一个this和一个count的值

       5: iconst_1 //取出一个int常量1,压入操作数栈

       6: iadd  // 从栈中取出count和1,将count值和1相加,结果入栈

       7: putfield      #12 // Field count:I 一次弹出两个,第一个弹出的是上一步
计算值,第二个弹出的this,将值赋值给this的count字段

      10: return

   LineNumberTable:
     line 27: 0

     line 28: 10

   LocalVariableTable:
     Start Length Slot Name   Signature
             0      11     0 this   Lcom/justest/test/TestDate;
 public void test4();
   Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_1
       5: iconst_1
       6: iadd
       7: istore_2
       8: iload_1
       9: iconst_1
      10: iadd
      11: istore_2
      12: return

   LineNumberTable:
     line 33: 0

     line 35: 2

     line 36: 4

     line 38: 8

     line 39: 12

    //看下面,b和c的槽位slot一样,这是因为b的作用域就在方法块中,方法块结束,局部变量表
中的槽位就被释放,后面的变量就可以复用这个槽位

   LocalVariableTable:
     Start Length Slot Name   Signature
             0      13     0 this   Lcom/justest/test/TestDate;
             2      11     1     a   I
             4       4     2     b   I
            12       1     2     c   I
}

例子2:下面一个例子

先有一个User类:

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

然后写一个操作User对象的测试类:

public class TestUser {

    private int count;

    public void test(int a){
        count = count + a;
    }

    public User initUser(int age,String name){
        User user = new User();
        user.setAge(age);
        user.setName(name);
        return user;
    }

    public void changeUser(User user,String newName){
        user.setName(newName);
    }
}

先javac -g 编译成class文件。 然后对TestUser类进行反汇编:

$ javap -c -l TestUser

得到反汇编结果如下:

Warning: Binary file TestUser contains com.justest.test.TestUser
Compiled from "TestUser.java"

public class com.justest.test.TestUser {

    //默认的构造函数

    public com.justest.test.TestUser();
    Code:
    0: aload_0
    1: invokespecial #10                 // Method java/lang/Object."
    <init>":()V

    4: return

    LineNumberTable:
    line 3: 0

    LocalVariableTable:
    Start Length Slot Name   Signature
    0       5     0 this   Lcom/justest/test/TestUser;
    public void test(int);
    Code:
    0: aload_0 //取this对应的对应引用值,压入操作数栈

    1: dup //复制栈顶的数据,压入栈,此时栈中有两个值,都是this对象引用

    2: getfield      #18 // 引用出栈,通过引用获得对应count的值,并压入栈

    5: iload_1 //从局部变量表中取得a的值,压入栈中

    6: iadd //弹出栈中的count值和a的值,进行加操作,并将结果压入栈

    7: putfield      #18 // 经过上一步操作后,栈中有两个值,栈顶为上一步操作结
    果,栈顶下面是this引用,这一步putfield指令,用于将栈顶的值赋值给引用对象的count字段

    10: return //return void
    LineNumberTable:
    line 8: 0

    line 9: 10

    LocalVariableTable:
    Start Length Slot Name   Signature
    0      11     0 this   Lcom/justest/test/TestUser;
    0      11     1     a   I
    public com.justest.test.User initUser(int, java.lang.String);
    Code:
    0: new           #23   // class com/justest/test/User 创建User对象,
    并将引用压入栈

    3: dup //复制栈顶值,再次压入栈,栈中有两个User对象的地址引用

    4: invokespecial #25   // Method com/justest/test/User."<init>":()V 

    调用user对象初始化

    7: astore_3 //从栈中pop出User对象的引用值,并赋值给局部变量表中user变量

    8: aload_3 //从局部变量表中获得user的值,也就是User对象的地址引用,压入栈中

    9: iload_1 //从局部变量表中获得a的值,并压入栈中,注意aload和iload的区别,一
    个取值是对象引用,一个是取int类型数据

    10: invokevirtual #26 // Method com/justest/test/User.setAge:(I)V 

    操作数栈pop出两个值,一个是User对象引用,一个是a的值,调用setAge方法,并将a的值传给这
    个方法,setAge操作的就是堆中对象的字段了

    13: aload_3 //同7,压入栈

    14: aload_2 //从局部变量表取出name,压入栈

    15: invokevirtual #29 // MethodUser.setName:(Ljava/lang/String;)V 

    操作数栈pop出两个值,一个是User对象引用,一个是name的值,调用setName方法,并将a的值传
    给这个方法,setName操作的就是堆中对象的字段了

    18: aload_3 //从局部变量取出User引用,压入栈

    19: areturn //areturn指令用于返回一个对象的引用,也就是上一步中User的引用,这
    个返回值将会被压入调用当前方法的那个方法的栈中objectref is popped from the operand 
    stack of the current frame ([§2.6]
    (https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6)) 
    and pushed onto the operand stack of the frame of the invoker

    LineNumberTable:
    line 12: 0

    line 13: 8

    line 14: 13

    line 15: 18

    LocalVariableTable:
    Start Length Slot Name   Signature
    0      20     0 this   Lcom/justest/test/TestUser;
    0      20     1   age   I
    0      20     2 name   Ljava/lang/String;
    8      12     3 user   Lcom/justest/test/User;
    public void changeUser(com.justest.test.User, java.lang.String);
    Code:
    0: aload_1 //局部变量表中取出this,也即TestUser对象引用,压入栈

    1: aload_2 //局部变量表中取出newName,压入栈

    2: invokevirtual #29 // Method User.setName:(Ljava/lang/String;)V 
    pop出栈newName值和TestUser引用,调用其setName方法,并将newName的值传给这个方法

    5: return

    LineNumberTable:
    line 19: 0

     line 20: 5

   LocalVariableTable:
     Start Length Slot Name   Signature
             0       6     0 this   Lcom/justest/test/TestUser;
             0       6     1 user   Lcom/justest/test/User;
             0       6     2 newName   Ljava/lang/String;
public static void main(java.lang.String[]);
   Code:
       0: new      #1 // class com/justest/test/TestUser 创建TestUser对象,
将引用压入栈

       3: dup //复制引用,压入栈

       4: invokespecial #43   // Method "<init>":()V 引用值出栈,调用构造方法,
对象初始化

       7: astore_1 //引用值出栈,赋值给局部变量表中变量tu
       8: aload_1 //取出tu值,压入栈

       9: bipush    10 //将int值10压入栈

      11: ldc           #44   // String wangerbei 从常量池中取出“wangerbei” 

压入栈

      13: invokevirtual #46   // Method 
initUser(ILjava/lang/String;)Lcom/justest/test/User; 调用tu的initUser方法,并
返回User对象 ,出栈三个值:tu引用,10和“wangerbei”,并且initUser方法的返回值,即

User的引用,也会被压入栈中,参考前面initUser中的areturn指令

      16: astore_2 //User引用出栈,赋值给user变量

      17: aload_1 //取出tu值,压入栈

      18: aload_2 //取出user值,压入栈

      19: ldc           #48     // String lisi 从常量池中取出“lisi”压入栈

      21: invokevirtual #50     // Method changeUser:
(Lcom/justest/test/User;Ljava/lang/String;)V 调用tu的changeUser方法,并将user

引用和lisi传给这个方法

      24: return //return void
   
 LineNumberTable:
     line 23: 0

     line 24: 8

     line 25: 17

     line 26: 24

   LocalVariableTable:
     Start Length Slot Name   Signature
             0      25     0 args   [Ljava/lang/String;
             8      17     1   tu   Lcom/justest/test/TestUser;
            17       8     2 user   Lcom/justest/test/User;
}

3. 知识小结

1、通过javap命令可以查看一个java类反汇编、常量池、变量表、指令代码行号表等等信息。

2、平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行 的,

可以参考官方文档查看每个指令的含义,很简单:Chapter 6. The Java Virtual Machine Instruction Set

3、通过对前面两个例子代码反汇编中各个指令操作的分析,可以发现,一个方法的执行通常会涉及下 面

几块内存的操作:

(1)java栈:局部变量表、操作数栈。这些操作基本上都值操作。

(2)java堆:通过对象的地址引用去操作。

(3)常量池。

(4)其他如帧数据区、方法区<?(jdk1.8之前,常量池也在方法区)等部分,测试中没有显示出来,这

里说明一下。

在做值相关操作时:

一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可 能

是指,可能是对象的引用)被压入操作数栈。

一个指令,也可以从操作数数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系 统

调用等等操作。

四、案例分析

1. class文件解读

2. javap显示结果解读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodingW丨编程之路

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值