对象的创建
上篇文章从字节码的角度分析了异常处理和线程安全中的原子性,现在我们再从字节码的角度看一下对象的创建是一个怎样的过程,先从一简单的面试题开始。
假如有如下方法:
public String string() {
String str = new String("abc");
return str;
}
问整个方法用到了哪些运行时数据区域?
看到这道题,大多数人可能会这样回答:abc 存储在运行时常量池中,jdk1.7以前常量池存放在方法区,而1.7之后则存放在java堆中。而str存储在局部变量表中,创建一个string对象则在java堆中,由于对象的大小会分配到堆中的老年代或者新生代,所以该方法在创建的过程中只使用了java栈和java堆。
那么,上述方法的执行过程大致是怎样的呢?
实际上,当我们创建string对象时,还调用了string中的构造方法,它的代码如下:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
那么我们通过反编译,可以得到上述string()方法的字节码如下所示:
0 new #16 <java/lang/String>
3 dup
4 ldc #18 <abc>
6 invokespecial #20 <java/lang/String.<init>>
9 astore_1
10 aload_1
11 areturn
由上可以看到,代码执行该方法时先创建String的实例进行将其复制存放到操作数栈中,然后将abc加载到常量池(ldc)中,通过调用String对象的初始化方法完成对象的创建(invokespecial)并将对象存到java堆中,然后将该对象的引用str存储(astore)到局部变量表中。
方法执行中的过程如下图:
则方法执行完的运行时数据区如下所示:
对象的引用
示例-面试题
public class Main {
public static void main(String[] args) {
Main inc = new Main();
int i = 0;
inc.fermin(i);
i = i++;
System.out.println(i);
}
void fermin(int i) {
i++;
}
}
问程序的输出结果是什么?
首先我们来看看main方法中的程序字节码是怎样的。
0 new #2 <test/Main> //创建对象
3 dup
4 invokespecial #3 <test/Main.<init>> //实例化
7 astore_1 //存储变量inc
8 iconst_0 //将0从常量池加载到操作数栈
9 istore_2 //存储int值0到操作数栈
10 aload_1 //加载inc到操作数栈
11 iload_2 //加载0到操作数栈
12 invokevirtual #4 <test/Main.fermin> //调用fermin方法
15 iload_2 //加载0到操作数栈
16 iinc 2 by 1 //将局部变量中的值自增1
19 istore_2 //将0存储到局部变量表
20 getstatic #5 <java/lang/System.out>
23 iload_2 //将0加载到操作数栈
24 invokevirtual #6 <java/io/PrintStream.println>
27 return //返回0
可以看到,执行i=i++操作时,会先将局部变量表中的值加载到操作数栈,然后在将局部变量表中的值自加1,这时候赋值操作又会将操作数栈中的值放入到局部变量中,所以自加1的操作会被抹去,相当于只做了一个赋值操作,将值取出再放回。那么i++与++i到底有什么区别呢?
不妨看如下示例:
public static void main(String[] args) {
int a=0,b=0;
System.out.println(a++);
System.out.println(++b);
}
如上,我们习惯性的可以给出答案,a实现输出再+1,b是先+1再输出,那么从字节码层面者两者有何不同呢?
0 iconst_0 //加载0到操作数栈
1 istore_1 //存储0到局部变量表1槽
2 iconst_0
3 istore_2 //存储0到局部变量表2槽
4 getstatic #2 <java/lang/System.out>
7 iload_1 //加载局部变量表1槽中的值到操作数栈
8 iinc 1 by 1 //局部变量表中1槽中的值自加1
11 invokevirtual #3 <java/io/PrintStream.println>
14 getstatic #2 <java/lang/System.out>
17 iinc 2 by 1 //局部变量表中2槽中值自加1
20 iload_2 //加载局部变量表2槽中的值到操作数栈
21 invokevirtual #3 <java/io/PrintStream.println>
24 return
可以看到,i++和++i都是进行加载局部变量中的值到操作数栈和局部变量表中的值+1操作,只是顺序不同,我们很好理解,局部变量表变化前后值的不同,问题自然迎刃而解。
那么这个问题就算完了吗?并没有。原面试题中,fermin对i进行+1操作后为什么i的值没有发生变化?
不妨来看以下这个例子:
public class Main {
public static void main(String[] args) {
Main inc = new Main();
int i = 0;
i = inc.fermin(i);
}
int fermin(int i) {
i++;
return i;
}
}
这个程序最后i的值肯定是1,但是这个过程中的字节码操作是怎么样的呢?
首先来看下main方法是怎样的:
0 new #2 <test/Main>
3 dup
4 invokespecial #3 <test/Main.<init>>//实例化对象
7 astore_1 //局部变量表存储inc
8 iconst_0 //取常量0到操作数栈
9 istore_2 //存到局部变量表
10 aload_1 //加载inc
11 iload_2 //加载0
12 invokevirtual #4 <test/Main.fermin> //调用方法
15 istore_2 //存储值
16 return
可以看到初始化后会将inc和0加载到操作数栈,然后调用fermin方法,那么我们再看fermin方法:
0 iinc 1 by 1 //局部变量表+1
3 iload_1 //加载到操作数栈返回
4 ireturn //返回栈顶值
可以看到该方法会先将局部变量表中的值+1后加载到操作数栈返回,那么我们可以假设调用该方法后方法参数都被加载到给方法的局部变量表中。可以写出以下示例,
public class Main {
public static void main(String[] args) {
Main inc = new Main();
int a=1,b=2;
a = inc.fermin(a,b);
}
int fermin(int a,int b) {
return a+b;
}
}
可以查看fermin方法的字节码:
0 iload_1
1 iload_2
2 iadd
3 ireturn
可以验证我们的论证。所以,方法中的参数传递的是值,而不是方法的应用,方法中的参数在方法被调用后会转换成实际值存储到方法的局部变量表中。