简单介绍一下,在JVM中的运行时数据区分为五大区域,分别是程序计数器、java虚拟机栈、本地方法栈、堆和方法区。
而今天我们主要讲的就是java虚拟机栈。这一块区域是线程私有的,也就是每一个线程就是一个栈,在这个栈里存放的元素就是栈帧,这个栈帧说白了就是方法,每一个栈帧的入栈出栈就是一次方法的执行。在栈帧里主要有局部变量表,操作数栈,动态链接和方法出口。
了解这个东西离不开代码,我们先来定义一个简单地类,就让他实现两数相加的功能
public class Number {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a+b;
}
}
这个代码很简单,但我们想看到内部是如何实现的,现在我们对他进行反编译。
javac Number.java //生成class文件,文件位置与类在同一目录下
javap -c Number.class //对class文件进行反编译
通过这两个命令我们就能得到我们想要的信息,下面来看一下
Compiled from "Number.java"
public class Number {
public Number();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1 //int型常量值1进栈
1: istore_1 //将栈顶int型数值存入第一个局部变量(a)
2: iconst_2 //int型常量值2进栈
3: istore_2 //将栈顶int型数值存入第二个局部变量(b)
4: iload_1 //指定的int型局部变量进栈,这里就是a
5: iload_2 //指定的int型局部变量进栈,这里就是b
6: iadd //栈顶两int型数值相加,并且结果进栈
7: istore_3 //将栈顶int型数值存入第二个局部变量(c)
8: return
}
在上面所说的栈就是操作数栈, 很明显,对数据的操作必须依赖于操作数栈,变量存放在局部变量表里(这个从名字也能看出来好吧!),下面我们用图解的方式解释一下具体的操作。
首先会将int类型的数字1压入操作数栈,随后出栈并赋值给变量a,这样就完成了一次对变量a的赋值。同理,对b的赋值也是如此。
随后进行a+b的操作,首先他会将a和b的值依次进入操作数栈,然后再将1和2的结果相加并再次将结果压入栈中,这时操作数栈中就有一个int类型数据3,再将3出栈赋值给c,完成相加操作。
那我们知道了底层的操作之后是不是也能弄懂i++和++i到底是怎么回事了!下面就让我们一起来探索一下。
public class IAddTest {
public static void main(String[] args) {
int a = 0;
for (int i = 0;i<10;i++){
a = a++;
}
System.out.println(a);
}
}
很简单的小例子,如果你的面试官问你,你认为结果应该输出多少?如果你脱口而出10,那么凉凉,面试官眉头一皱,你感觉事情没那么简单。那么让我们一步步将这个小东西剖析开来。
我们知道a++是先赋值,再自增,拆解成代码也就是 a = a; a = a +1;
按照道理来说结果应该是10,因为每次都自增了,那为什么不是呢?
Compiled from "IAddTest.java"
public class IAddTest {
public IAddTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: bipush 10
7: if_icmpge 21
10: iload_1
11: iinc 1, 1 //指定int型变量增加指定值(增加之后的结果,增加值)
14: istore_1
15: iinc 2, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
}
这里我们主要看10、11、14,我们可以看到在执行a = a++
时,它先把a的值压入操作数栈,这时候a的值是0,然后执行了iinc
指令,注意,这时候局部变量表中的a的值发生了改变,变成了1。但是!但是!操作数栈中的数还是0!然后执行了什么命令呢?他执行了istore_1
,它把操作数栈中的0赋值给了a!!!这就明白了为啥最后输出是0了。
搞懂了i++
,那++i
自然也就懂了,与上面类似,只不过是先执行自增,再入栈出栈。至此我们通过剖析底层操作原理终于搞懂了这两者的区别!
到了这我还有一个疑问(与上面的没关系,我自己瞎想的),那我定义一个非静态变量为什么不能再静态方法里使用(别忘了,main方法也是静态方法),这就要牵扯到数据在内存存储的具体情况了。
如果是一个类的静态变量,那么这个变量存放在方法区里,而对于普通变量是必须要存放在实例中的,是存放在堆里面的,在栈帧中还有一个动态连接,这个就是指向这个栈帧所执行的方法,我们能通过这个引用找到这个类,并能获取这个类的类信息,自然也就能找到静态变量。而我们如果不新建一个类的实例的话,那么将没有这个普通变量,自然也就找不到,用不了。