看一个简单的例子,有几个类变量,类初始化时调用类方法,其中还包括简单加法运算,字符串重载以及输出。
Clac.java //java源码
public class Calc {
private static int numberA = 10;
private static int numberB = (int) (Math.random() * 7.7);
private static String show = "the result is ";
public static void main(String[] args) {
numberA = 20;
int result = numberA + numberB;
show = show + result;
System.out.println(show);
}
}
Calc.class文件结构 //主要信息,并不是全部
D:\Dev>javap -c -s -l -verbose Calc
Constant pool(常量池):
const #1 = Method #17.#32; // java/lang/Object."<init>":()V
const #2 = Field #16.#33; // Calc.numberA:I
const #3 = Field #16.#34; // Calc.numberB:I
const #4 = class #35; // java/lang/StringBuilder
const #5 = Method #4.#32; // java/lang/StringBuilder."<init>":()V
const #6 = Field #16.#36; // Calc.show:Ljava/lang/String;
const #7 = Method #4.#37; // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
const #8 = Method #4.#38; // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
const #9 = Method #4.#39; // java/lang/StringBuilder.toString:()Ljava/lang/String;
const #10 = Field #40.#41; // java/lang/System.out:Ljava/io/PrintStream;
const #11 = Method #42.#43; // java/io/PrintStream.println:(Ljava/lang/String;)V
const #12 = Method #44.#45; // java/lang/Math.random:()D
const #13 = double 7.7d;
const #15 = String #46; // the result is
const #16 = class #47; // Calc
const #17 = class #48; // java/lang/Object
const #18 = Asciz numberA;
const #19 = Asciz I;
const #20 = Asciz numberB;
const #21 = Asciz show;
const #22 = Asciz Ljava/lang/String;;
const #23 = Asciz <init>;
const #24 = Asciz ()V;
const #25 = Asciz Code;
const #26 = Asciz LineNumberTable;
const #27 = Asciz main;
const #28 = Asciz ([Ljava/lang/String;)V;
const #29 = Asciz <clinit>;
const #30 = Asciz SourceFile;
const #31 = Asciz Calc.java;
const #32 = NameAndType #23:#24;// "<init>":()V
const #33 = NameAndType #18:#19;// numberA:I
const #34 = NameAndType #20:#19;// numberB:I
const #35 = Asciz java/lang/StringBuilder;
const #36 = NameAndType #21:#22;// show:Ljava/lang/String;
const #37 = NameAndType #49:#50;// append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
const #38 = NameAndType #49:#51;// append:(I)Ljava/lang/StringBuilder;
const #39 = NameAndType #52:#53;// toString:()Ljava/lang/String;
const #40 = class #54; // java/lang/System
const #41 = NameAndType #55:#56;// out:Ljava/io/PrintStream;
const #42 = class #57; // java/io/PrintStream
const #43 = NameAndType #58:#59;// println:(Ljava/lang/String;)V
const #44 = class #60; // java/lang/Math
const #45 = NameAndType #61:#62;// random:()D
const #46 = Asciz the result is ;
const #47 = Asciz Calc;
const #48 = Asciz java/lang/Object;
const #49 = Asciz append;
const #50 = Asciz (Ljava/lang/String;)Ljava/lang/StringBuilder;;
const #51 = Asciz (I)Ljava/lang/StringBuilder;;
const #52 = Asciz toString;
const #53 = Asciz ()Ljava/lang/String;;
const #54 = Asciz java/lang/System;
const #55 = Asciz out;
const #56 = Asciz Ljava/io/PrintStream;;
const #57 = Asciz java/io/PrintStream;
const #58 = Asciz println;
const #59 = Asciz (Ljava/lang/String;)V;
const #60 = Asciz java/lang/Math;
const #61 = Asciz random;
const #62 = Asciz ()D;
{
public Calc();
Signature: ()V
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
0: bipush 20
2: putstatic #2; //Field numberA:I
5: getstatic #2; //Field numberA:I
8: getstatic #3; //Field numberB:I
11: iadd
12: istore_1
13: new #4; //class java/lang/StringBuilder 创建一个对象,并将其引用值压入栈顶
16: dup //复制栈顶数一个字长的值并将复制值压入栈顶
17: invokespecial #5; //Method java/lang/StringBuilder."<init>":()V
20: getstatic #6; //Field show:Ljava/lang/String;
23: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;append方法返回一个对象
26: iload_1
27: invokevirtual #8; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
30: invokevirtual #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: putstatic #6; //Field show:Ljava/lang/String;
36: getstatic #10; //Field java/lang/System.out:Ljava/io/PrintStream;
39: getstatic #6; //Field show:Ljava/lang/String;
42: invokevirtual #11; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: return
static {};
Signature: ()V
Code:
Stack=4, Locals=0, Args_size=0
0: bipush 10
2: putstatic #2; //Field numberA:I
5: invokestatic #12; //Method java/lang/Math.random:()D
8: ldc2_w #13; //double 7.7d
11: dmul
12: d2i
13: putstatic #3; //Field numberB:I
16: ldc #15; //String the result is
18: putstatic #6; //Field show:Ljava/lang/String;
21: return
}
* 关于常量池
在class文件结构中常量池占了很大一部份,由很多个表组成,每一项就是一个表,关于表的结构可以查看一下《深入Java虚拟机第二版》的第六章,里边有祥细的解释。关于常量池,正如其名称,存储的都是常量,那常量都是哪些呢? 根据《深入Java虚拟机第二版》的介绍,常量包括直接常和对其它类型,字段以及方法的引用。对照上面的class的文件,源码的变量numberA,在常量池对应的是Calc.numberA:I,这个是符号引用,入口地址为#2(常量池的偏移地址),show变量的初始化的值也是存储在常量池中,从常量池入口地址#15可以得到,解析过程后面会讲到。
* JVM第一步 装载
首在当然把这个Class文件装载到虚拟中,在方法区为之分配一块内存,这个过程有三个基本动作:
1)通过该类的完全限定名,产生一个代表该类型的二进制流。
2)解析这个二进制数据流为方法区内总数据结构。
3)创建一个代表该类型的java.lang.Class实例
产生二进制数据流的方式主要有七种,class文件可由启动类型装载器或者用户自定义的类装载器装载,这方面就涉及到ClassLoader编程。
装载的最终产品是Class实例对象,它成为Java程序与内部数据结构之间的接口,要访问类型信息都需要通过Class实例对象,比如说我们新一个对象的时候
* JVM第二步 验证
这个过程发生各个阶段上都会有,只是验证的内容不一样。
1)第一趟:class文件结构的检查
2)类型数据的语义检查
3)字节码验证
4)符号引用的验证 (可能发生在初始化前或者后)
* JVM第三步 准备
当前面的步骤都顺利完成,准备阶段就可以开始,Java为类变量为分配内存并设置其默认值,但并不会为实例变量分配内存,这个在创建对象时才分配
* JVM第四步 解析(可选)
根据JVM不同的实现,解析分为两种,早解析与晚解析,就是发生在初始化前后。解析过程就是把符号引用变成直接引用。比如说前面的例子,当调用System.out变量时,它的符跟引用是 java/lang/System.out,在晚解析的情况下,JVM这时才装载,连接,初始化java/lang/System这个类型,然后再把这个符号引用替换成直接引用。
*** 二,三,四步也统称为连接
* JVM第五步 初始化
初始化过程就是执行 <clinit>()方法,但需要注意的执行顺序,因为有超类的影响。
下面就看前面那个class文件的字节码运行过程,
* 执行static {};(<cinit>()方法),
bipush 10 //将单字节的常量值10推送至栈顶
putstatic #2 //把栈顶的值送给类变量。我们看看常量池入口#2,指向的是字段表,里边两个常量池入口,顺着这些常量池入口就可以得到当前类名,字段名以及类型,符号引用(Calc.numberA:I)就找着了,前面提到在准备阶段已经为类变量分配了一块内存,现在就在常量池入口#2中放入numberA的直接引用并标记已解析,下一次访问这个变量时就可以直接读取
invokestatic #12 //调用静态方法,把方法返回值压入栈顶。JVM找到常量池入口#12,可以得到符号引用(java/lang/Math.random:()D),Java虚拟机要解析这个符号引用,这个将导致java/lang/Math类的装载,连接与初始化,然后在常量池下口#12放入一个指向random()方法的直接引用,把这个入口标记为已解析,并且把invokestatic变成invokestatic_quick.
ldc2_w #13 //将double型常量值从常量池中推送至栈顶,取两个字。虚拟机找到常量池入口#13,发现并未解析,虚拟机把这入口解析为7.7d,标记已解析并且把ldc2_w变成ldc2_w_quick.有意思的是没有常量池入口#14,这是因为double型占用两个位置,一个位置相当于一个字长。
dmul //将栈顶两double型数值相乘并将结果压入栈顶
d2i //将栈顶double型数值强制转换成int型数值并将结果压入栈顶
putstatic #3 // 把栈顶的值送给类变量
ldc #15 // 虚拟机找到常量池入口#15,指向一个String,值在常量池入口#46(the result is),字符串解析并不是那么简单.虚拟机都在会在内部维护一张表,记录在程序运行过程中已被拘留的字符串对象引用.因此虚拟机在解析字符串的时候,首先会在这张内部表查找否是有了这个字符串引用,如果已存在,则把这个拘留字符串对象引用放入常池入口;如果没有,则在堆重创建一个字符串对象,并把它的引用添加到内部表,再把常量池入口指向这个引用。
putstatic #6; //把栈顶的值送给类变量,并标记已解析
类型初始化已经结束.
* main方法的解析
main方法前六条指令比较通俗易懂,就是把常值赋给变量numberA,接下来再取变量numberA与numberB的值,求和并结果赋给局部变量result.
接下来的指令就比较有意思了,首先new一个StringBuilder对象,把引用放入栈顶,接着就dup(在栈顶复制一个字长的值),复制一个对象引用,也放入栈顶,为何要复制一份呢?先看下一条指令,invokespecial #5,调用StringBuilder的对象初始化方法,我们都知道,在调用一个非静态方法时,都必需传一个对象引用作为参数,这就是为什么Java关键字this只能在非静态方法使用,这也说明了在使用一个对象要对其初始化,因此复制的引用就是传递给对象初始化方法。***注意现在栈中只剩下一个对象引用***,可后面还调用了StringBuilder的三个非静态方法,而且没有变量保存StringBuilder引用,一个对象引用怎么可以给三个非静态方法调用,这可怎么办啊?而神奇的地方就在于append方法会返回一个对象引用,为下一个方法准备好参数,不过情况只能发生在StringBuilder为匿名对象,如果是一般对象(StringBuilder s = new StringBuilder()),在调用完append方法之后会有一个pop操作,目的就是清除append返回的对象引用。而在上面的字节码,第一个append方法返回的对象引用作为第二个append方法的参数,第二个append返回的对象引用刚好是作为toString的参数,感觉很顺畅,这应该是(String sa = sb + sc)这种操作的固定模式,对于多个重载操作,速度应该很可观。
后面的指令也比较简单,get变量值,调用println方法输出,最后调用return结束。