目录:
本文问题
1.线程在是如何运行代码的,代码和数据在线程中的结构是字母样的?
2.方法是如何被调用的。
1.运行时栈帧结构
-
栈帧
线程运行代码是基于栈帧的,这里不要跟操作数栈搞混了。线程使用一个栈来保存栈帧,而一个栈帧则代表了一个方法,包含了局部变量表,操作数帧,动态链接,和返回地址。另外,为了优化,帧之间并不是完全无关的,有些会把数据存放在共享区域,避免复制。
-
局部变量表
局部变量表顾名思义,是用来保存局部变量的值。他的最小单位是slot,一个slot可以保存一个boolean,byte,char,short,int,float,refrence或者returnAddress类 -
操作数栈
操作数栈其实在文章“JVM-2.1 虚拟机执行子系统-字节码和字节码指令”的字节码的执行中提到过。虚拟机把方法中的代码编译成字节码指令,字节码指令占一个字节,如果指令有参数,则后续的跟随着一个或多个参数字节。具体的实现方式伪代码如下
do{
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作指令;
if(字节码存在参数)
从字节码流中取出参数;
执行操作码所定义的操作;
}while(字节码流长度>0);
- 动态链接
如果方法中调用了其他的方法(且该方法时符号引用而不是直接引用),那么首先要调用方法,即找到被调用方法的直接引用,然后将其存入到动态链接中,如果下次再调用就不用重新解析,而是直接获取动态链接中的直接引用。 - 方法返回地址
当前方法被上层调用的地址,用于方法返回或者方法异常时回到调用的位置。当方法退出时,会恢复上层方法的局部变量和操作数栈,并把返回值(如果有)压入调用这的栈帧操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
2.方法如何被调用
方法调用不是方法执行,方法执行其实就是操作数帧字节码指令的调用,而方法调用的重点是,定位到被调用的方法所在。
解析
如果被调用的方法时静态方法或者私有方法,那么会直接转换成直接引用,因为他们是不可变的,不会被继承或者重写。
五种调用方法的字节码指令
- invokestatic:调用静态方法
- invokespecial:调用构造器,私有方法和父类方法
- invokevirtual:调用虚方法
- invokeinterface:调用接口方法
- invokedynamic:调用运行时解析的方法(动态)
只要能被invokestatic或者invokespecial调用的方法,会在类加载的时候就解析成直接引用。称之为费虚方法。其他方法称为虚方法(除去final方法)
分派
当调用一个方法时,如何确定应该调用哪一个方法呢,一个方法的可能被重载,也可能重写。
- 静态分派
public class StaticDispatch{
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public void sayHello(Human guy){
System.out.println("hello,guy!");
}
public void sayHello(Man guy){
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy){
System.out.println("hello,lady!");
}
public static void main(String[]args){
Human man=new Man();
Human woman=new Woman();
StaticDispatch sr=new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
先看上面的代码,这是一个典型的重载的过程,打印的结果是什么?
hello,guy!
hello,guy!
为什么会这样,因为重载是根据变量的静态类型进行判断的。无论是man或者woman,静态变量都是前面的Human,所以调用的都是Human的sayHello方法。这里的分派在编译的时候就已经确定了。所以被称为静态分派。
- 动态分派
与静态分派的区别,动态分配是在运行时决定的。
public class DynamicDispatch{
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello(){
System.out.println("man say hello");
}
}
static class Woman extends Human{
@Override
protected void sayHello(){
System.out.println("woman say hello");
}
}
public static void test(){
Human man=new Man();
Human woman=new Woman();
man.sayHello();
woman.sayHello();
man=new Woman();
man.sayHello();
}
}
这段代码是典型的多态的变现,通过继承,不同的类实现了同一个方法的不同实现。
代码的输出应该很容易理解。
man say hello
woman say hello
woman say hello
由于并没有使用重载,而是多态,实际上就是直接调用对象的方法。这时虚拟机会在运行时去查找该对象内是否有和被调用的方法名称和签名一样的方法,如果有就直接调用方法。如果没有就尝试寻找父类的方法。
3.解释执行和编译执行
java经常被定义为“解释执行”的代码,并被扣上了运行效率低的帽子,实际上现在的java版本也支持编译执行了。当被调用的方法式一个热点代码时,会编译成机器码,下次执行时就不需要再次解释或者编译了。
4.字节码执行
总的来说,字节码执行的过程是这样的。
1.方法被封装成一个栈帧。
2.当方法被调用时,方法的栈帧会被压入当前线程的栈中。
3.调用栈顶元素的操作数栈执行指令,期间可能会和局部变量表发生数据交换。
4.如果方法内调用了其他方法,如果该方法是符号引用,就会去解析符号引用,找到指定的符合的方法。
5.找到符合的方法后,会把对应的方法压入线程栈顶,执行调用的方法。
6.如果方法返回了,首先弹出栈顶的方法(这时候新的栈顶其实就是调用方)
7.根据方法返回的地址和返回值(如果有),操作新栈顶的操作数和计数器。
8.一直执行到栈为空。
9.如果发生异常,实际上也会退出方法,直到栈为空。除非发生使得虚拟机直接挂掉的错误。