您的位置:首页 > 编程语言 > Java开发

用java反汇编研究java语言细节

2011-04-11 14:14 155 查看
在Sun公司的JDK中提供了java反编译程序javap。可以用他对编译后的.class文件进行反编译,其中javap -c 类名 命令可以反编译
出类似汇编语言的结果,可以称之为"java虚拟机汇编语言"。主要是由java虚拟机的指令集组成。可以用它来研究java程序中的一些细节。
java中基本类型:
public void nmbericDefine(){
byte b = 5;
char c = 5;
short s = 5;
int i = 5;
float f = 5;
long l = 5;
double d = 5;
float f2 = 5f;
long l2 = 5l;
double d2 = 5d;
}
用javap命令反汇编的结果为
public void nmbericDefine();
Code:
0: iconst_5 ///将5推送至栈顶
1: istore_1 ///将栈顶值存储到索引为1的局部变量上
2: iconst_5
3: istore_2
4: iconst_5
5: istore_3
6: iconst_5
7: istore 4
9: ldc #5; //float 5.0f 从常量池中取出5.0f并推送至栈顶
11: fstore 5 ///将值存储在索引为4的局部变量中
13: ldc2_w #6; //long 5l
16: lstore 6
18: ldc2_w #8; //double 5.0d
21: dstore 8
23: ldc #5; //float 5.0f
25: fstore 10
27: ldc2_w #6; //long 5l
30: lstore 11
32: ldc2_w #8; //double 5.0d
35: dstore 13
37: return

}
Code下面跟:号的是操作码的索引。可以看出java语言中的基本类型byte,char,short的常量会被转换成int类型。float,long,double的值
常量会被存储在常量池中。boolean类型在虚拟机中没有单独定义,只是用int类型的0表示false,1表示true。9号和23号操作码(索引值为9和
23,这里作简称)的操作数均为#5,这并不表示f和f2共享常数池中的值,在java虚拟机运行的过程当中f和f2分别存储值,互相不影响。
这样做可以节省.class做占的空间。
注:偶然中发现Sun的java编译器有个很有趣的现象。在java程序中纯粹基本类型常量运算,比如double d = 1+1f+1d+1L,编译器在编译的过
当中就把结果计算出来用作操作数。这样可以减小java虚拟机的运算负担。

基本类型的运算
public void nmbericDefine(){
int i = 10;
int j = 20;
int x = i+j;
int y = (x+i)*j;
i++;
j++;
}
反汇编的结果为
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1 ///将索引为1的局部变量压入栈顶
7: iload_2
8: iadd ///计算栈顶的两个值相加的结果
9: istore_3
10: iload_3
11: iload_1
12: iadd
13: iload_2
14: imul
15: istore 4
17: iinc 1, 1///索引1的局部变量增加1
20: iinc 2, 1
23: return
java虚拟机指令集中只有基本的单元运算,也就是先将两个要进行运算的值通过iload指令推送至栈顶,并弹出根据相应的运算指令计算
出结果并将其压入栈顶。复杂的算式交由java编译器分析并生成一系列单元运算指令。除此之外还专门定义了变量自增自减的指令iinc,这个指令
只操作int型变量。后面跟变量的索引和增值两个操作数。这样做的原因是可以重用局部变量使代码变的高效。
i++和++i的区别:
一个有趣的程序:
int j = 0;
for(int i = 0;i<100;i++){
j = j++;
}
无论循环多少次,j始终等于0;我们来看看反编译以后的结果:
0: iconst_0
1: istore_0
2: iconst_0
3: istore_1
4: iload_1
5: bipush 100
7: if_icmpge 21
10: iload_0
11: iinc 0, 1 ///自增后并没有并没有将新值压入栈顶
///若是++i则此处会有iload_0指令,该指令会将自增后的值压入栈顶,这样存储的就是自增后的值了
14: istore_0 ///此时变量j存储的值仍然为没有自增之前的值
15: iinc 1, 1
18: goto 4
21: iload_0
22: ireturn

字符串:
在听张老师的公开课的时候看到网友给出的这么一个程序(面试题):
String str = null;
String str1 = "itcast"+str;
str1的值为itcastnull,我们来看看反编译的结果:
0: aconst_null
1: astore_0
2: new #2; //class java/lang/StringBuilder
5: dup
6: invokespecial #3; //Method java/lang/StringBuilder."<init>":()V
9: ldc #4; //String itcast
11: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/
String;)Ljava/lang/StringBuilder;
14: aload_0
15: invokevirtual #5; //Method java/lang/StringBuilder.append:(Ljava/lang/
String;)Ljava/lang/StringBuilder;
18: invokevirtual #6; //Method java/lang/StringBuilder.toString:()Ljava/la
ng/String;
java编译器在编译这段程序的时候使用了StringBuilder,这样可以提升程序的性能。如果是"itcast"+"null";在java编译器编译的时候会像处理
整型常量运算一样直接算出结果,当做"itcastnull"处理。可以看出java虚拟机在实现+运算符的时候会用不同的策略。似乎有这么个原则:程序中确
定,永远不变的运算,java编译器会直接计算出结果。
看下面一个程序:
类 A
public static void StringTest(){
String str = null;
String str1 = "itcast"+str;
String s = "csdn黑马";
boolean b = s== new B().getString();
boolean b2 = s==getString();
}
public static String getString(){
String s1 = "csdn";
String s2 = "黑马";
return s1+s2;
}
·类B
public String getString(){
String s = "csdn黑马";
return s;
}
b的结果为true,b2为false,为什么会这样呢?有这么一个说法,就是在java虚拟机在给字符串引用赋值的时候会
检查内存中是否有相同字符串,如果有会把该字符串的引用赋给它。我认为是不正确的(仅仅是个人猜想,未经验证),因为java虚拟机并没有定义比较
的字符串指令,就算定义这样的检查会使程序的效率很低。我想这份工作应该交给java编译器。因为java编译器很清楚程序中的字符串常量是否
相同,编译器只要将程序中所有相同的字符串的引用都指向相同的内存地址就可以了。但是程序运行的过程当中内存地址是不固定的,而java编译器
又无法得知程序运行时的状况。那应该如何处理呢?可以用这样一个方法。举一个例子,一个机构要接待陆续来自不同地方由不同学校的学生,
要把同一个学校的学生放在同一间教室。而且不清楚到底有哪些学校。首先接待员接待学生的时候问清楚是哪个学校的,如果是该学校首个到达的学生
要为其指派向导,由向导为负责教室安排。并将学校和向导的信息记录在案。下次如有该学校的学生,就为其指定该向导。同理,java编译器编译的时候
遇到一个常量,如果是新的常量java编译器就为其定义一个指针,并把该常量的引用指向该指针,该指针具体的指向地址由java虚拟机指定。然后将常量记
录下来用作比较。

对象:
Object o = null;
o = new Object();
Object[] os = null;
os = new Object[3];
反汇编结果为:
8: aconst_null
9: astore_1
10: new #12; //class java/lang/Object
13: dup
14: invokespecial #1; //Method java/lang/Object."<init>":()V
17: astore_1
18: aconst_null
19: astore_2
20: iconst_3
21: anewarray #12; //class java/lang/Object
24: astore_2
25: return
在java虚拟机的指令集中创建对象只有new指令,那数组是不是对象?在java语言中数组是当做Object看待的。Object o = new String[3];这样是可以的,
所有Object类的方法都适用数组,所以说数组是一个对象。唯一不同的就是数组有length属性,java编译器会编译成arraylength指令求出数组长度。
那么是不是就能说明数组是继承自Object?答案是否定的。我们在jdk文档中我们找不到任何数组类的描述,这说明数组不是一个类,那么java虚
拟机是如何创建数组对象的呢?它是专门指定了一个指令anewarray创建对象数组,基本类型数组则用newarray来创建。这两者之间有什么区别,和
new指令又有什么区别?为什么有拥有和Object同样的方法?这我目前还没有搞清楚,有待研究。
Object o = new Object();有人认为此时内存有两个对象o和new Object(),之所以这样认为,因为他们信奉这样一条真理:在java中一切皆对象。
我认为这种说法是错误的,o并不是一个对象,它只是个引用而已,在java虚拟机规范中引用是有统一的大小的。例如Student s = new Teacher();
Student和Teacher没有任何关系,但这在java虚拟机中是可行的(呵呵,猜想而已),但是这种做法过不了编译器这关。java虚拟机是不会检查对象
的类型的,也不知道任何的对象与对象的关系,Object o是写给编译器看的。java虚拟机用了多少次new指令内存中就有多少个对象
(newarray和anewarray也是。)

类变量
int i ;
String str = "java";
Object o = new Object();
public void objTest(){
String s = str;
}
反汇编的结果为
public ObjTest();
Code:
Stack=3, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2; //String java
7: putfield #3; //Field str:Ljava/lang/String;
10: aload_0
11: new #4; //class java/lang/Object
14: dup
15: invokespecial #1; //Method java/lang/Object."<init>":()V
18: putfield #5; //Field o:Ljava/lang/Object;
21: return
LineNumberTable:
line 1: 0
line 3: 4
line 4: 10

public void objTest();
Code:
Stack=1, Locals=2, Args_size=1
0: aload_0
1: getfield #3; //Field str:Ljava/lang/String;
4: astore_1
5: return

所有的类变量都存储在常量池中,在默认的构造函数中用putfield指令为类变量赋值(现在明白为什么类变量不需要显式初始化而局部变量需要显式
初始化)。在方法中是使用变量要通过getfield指令从常量池中获取(以前在一本android书上面看到说尽量使用局部变量,少用类变量,用类变量的时候
现在方法中定义同样的类型的变量,把类变量的引用赋给它。看来这样做的原因是因为使用getfield要付出性能方面的代价的)。但是默认构造函数中并没
有显式为i赋值,在常量池中只是用i:I表示,当我显式为这个变量赋值的时候,在构造函数才通过putfield赋值。这个让人费解,目前我也没搞明白。

方法的调用
public class MethodInvoke{
public static void main(String args[]){
new MethodInvoke().sayHello();
}
public void sayHello(){
System.out.println("Hello");
}
}
反编译结果为:
public class MethodInvoke extends java.lang.Object{
public MethodInvoke();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2; //class MethodInvoke
3: dup
4: invokespecial #3; //Method "<init>":()V
7: invokevirtual #4; //Method sayHello:()V
10: new #5; //class java/util/ArrayList
13: dup
14: invokespecial #6; //Method java/util/ArrayList."<init>":()V
17: astore_1
18: aload_1
19: iconst_1
20: invokestatic #7; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Int
eger;
23: invokeinterface #8, 2; //InterfaceMethod java/util/List.add:(Ljava/lang
/Object;)Z
28: pop
29: return

public void sayHello();
Code:
0: getstatic #9; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10; //String Hello
5: invokevirtual #11; //Method java/io/PrintStream.println:(Ljava/lang/St
ring;)V
8: return

}
java指令集中有四个调用方法的指令
invokevirtual调用对象的方法
invokespecial调用构造函数等特殊方法
invokestatic调用静态方法
invokeinterface调用接口方法
这四个方法的执行效率是不一样的,invokeinterface执行是会去解析所有实现类(看来灵活性是以牺牲效率为代价的,对于嵌入式的系统要在这
两者之间找个平衡点),速度最慢。invokevirtual要解析方法属于的对象,速度中等。invokestatic不依赖对象,效率最高。invokespecial尚且不清楚.(从invoke单词上可以看出对于java虚拟机而言我们写的方法都是回调方法,看来所谓的回调方法都是相对而言的,就像servlet中的doGet和doPost方法一样,相对于服务器他们是回调方法。
在java语言中把类的构造函数设为私有,除了类里面的静态方法,没人能创建该类的对象。其实这都是java编译器作的限制。在java虚拟机中对象的创建不依赖构造函数。只是java编译器在编译的时候通常都会在创建对象以后,调用该对象的构造方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: