一、运行时栈帧
1、简介
JVM内存模型中有一个虚拟机栈,这个栈对应着方法的入栈与出栈。
如下示例:
public static void main(String[]args){
调用方法1
调用方法2
调用方法3
}
虚拟机栈会先调用main方法,main方法被压入了栈底,随后方法一入栈,方法一出栈,方法二入栈,方法二出栈,方法三入栈,
方法三出栈,main方法出栈,结束。
虚拟机栈中每一个位置(元素)称为栈帧。
java方法里实现的功能能被正确地执行,栈帧中必须存放足够的信息量,一个栈帧中有局部变量表、操作数栈、
动态连接、方法返回等,下面将具体介绍。
2、局部变量表
一个方法可能会的参数,方法体里也可能被定义多个局部变量。
如:
public int function(int a, int b){
int c = a + b;
return c;
}
上述方法中a,b,c都是方法function的局部变量。而它被存放在这个方法对应的栈帧中的局部变量表中。换句话说,你能使用
的所有局部变量都被存放在当前的局部变量表中(尽管这种说法并不完全正确)。
局部变量表里能存放所有的java类型,八种基本数据类型、引用、returnAddress。
你可以将局部变量表当成一个数组,对应的每一个位置称为slot,在指令中,astore_index表示将局部变量存放在下标为index
的局部变量表里,aload_index表示从局部变量表里取出下标为index的局部变量。
如示例:
public class Test{
public void f(){
//do something
}
public static void main(String[]args){
Test t = new Test();
t.f();
}
}
如以上示例所示,在main方法这个栈帧中,参数args以及引用t都被存放在这个栈帧对应的局部变量表里。为了更清楚地
理解它,下面通过JDK自带的javap命令来查看Test类被编译过后的类文件。
args_size表示这个方法只有一个参数。并且可以清楚地看到指令:
astore_1
aload_1
第一个指令表示将t引用存入局部变量表中下标为1的位置,第二个指令表示从局部变量表中
下标为1的位置取出局部变量。
3、操作数栈
一个方法中避免不了数据的运算,比如:1+1或10*20等等。
操作数栈是提供算术运算的数据结构(当然它并不只有这个功能,还还负责方法调用时的参数
传递)。
举一个例子:
public int function(int a, int b){
int c = a + b;
return c;
}
通过前面的介绍,相信大家知道,这个方法里有三个局部变量,a,b,c,那么它们的运算过程
是如何配合操作数栈来进行的呢?
结果存入局部变量表中。
4、动态连接
java的JDK里提供了成千上万的类,这些类并没有在虚拟机启动时就被加载,而是需要用到时
才会去加载。虚拟机中有一块是运行时常量池,哪些数据应该放入常量池中并不是运行的时候
来决定的,而是java文件被编译成.class文件就已经决定了。
当你用javap命令去查看一个类文件时,会看到如下内容:
你可以看到,每一个常量都对应着一个唯一的符号。当类文件被加载到JVM里时,虚拟机必须
将这些符号引用替换成直接引用,如具体指向某内存地址。
所有加载过程中将符号引用替换成直接引用的过程称为静态解析,它并不是动态连接。
动态连接是在运行期间来调整直接引用的过程,这部分的具体内容会在方法调用中的动态分派
里详解。
5、方法返回地址
这部分功能很简单,不过方法返回地址并不是指方法结束时返回的值。如:
public int f(){
return 10;
}
方法返回地址并非指这里的10。
它是指定当这个方法返回(不管是正常返回还是异常返回)后,下一次应该去执行哪一行!
如例子:
public static void main(String[]args){
int c = f(10, 20);
}
public static int f(int a, int b){
return a + b;
}
当方法f返回时,下一行应该执行main方法里的对变量c赋值操作。
二、方法调用
1、简介
一个功能的实现,涉及到大量的操作就是方法的调用,这里的方法调用并不是指方法体的执行,而是在遇上两难时具体选择
哪一个方法来调用。
在方法的实现上,java语言为我们提供了重载和重写,对于具有面向对象的开发都来说自然知道什么时候调用哪个方法,但
JVM是怎么做的?
2、解析
在方法调用之前,必须先从类文件中解析出来。
不同种类的方法其调用过程也不相同,如静态方法可以直接通过类名调用,而实例方法只能通过实例对象调用,当用
到A a = new A();时会调用实例构造方法,当方法被重写时子类对象会去调用被重写过的方法等。
JVM里提供了四种方法调用字节码指定:
invokestatic:调用静态方法
invokespecial:调用实例构造方法,私有方法,父类方法
invokevirtual:除其它三种以外的方法
invokeinterface:调用接口方法
如:
class Super{
public void say(){
System.out.println("super say");
}
}
public class Sub extends Super{
public void say(){
System.out.println("sub say");
}
public void fatherSay(){
super.say();
}
public void f(){
System.out.println("hello sub");
}
public static void main(String[]args){
Sub s = new Sub();
s.say();
s.fatherSay();
s.f();
}
}
main方法里的第一行,即创建Sub对象时,会出现invokespecial指令,Sub类中fatherSay方法里的super.say()也会出现
invokesecial指令,而s.say(),s.f()则会出现invokevirtual指令。有兴趣的朋友可以用javap命令查看验证。
3、静态分派
静态分派对应着方法重载的实现。
4、动态分派
动态分派对应着方法重写的实现。
由于时间原因,静态分派与动态分派这里不详述了,想更深入了解的朋友可以查阅与JVM相关的书籍。
1、简介
JVM内存模型中有一个虚拟机栈,这个栈对应着方法的入栈与出栈。
如下示例:
public static void main(String[]args){
调用方法1
调用方法2
调用方法3
}
虚拟机栈会先调用main方法,main方法被压入了栈底,随后方法一入栈,方法一出栈,方法二入栈,方法二出栈,方法三入栈,
方法三出栈,main方法出栈,结束。
虚拟机栈中每一个位置(元素)称为栈帧。
java方法里实现的功能能被正确地执行,栈帧中必须存放足够的信息量,一个栈帧中有局部变量表、操作数栈、
动态连接、方法返回等,下面将具体介绍。
2、局部变量表
一个方法可能会的参数,方法体里也可能被定义多个局部变量。
如:
public int function(int a, int b){
int c = a + b;
return c;
}
上述方法中a,b,c都是方法function的局部变量。而它被存放在这个方法对应的栈帧中的局部变量表中。换句话说,你能使用
的所有局部变量都被存放在当前的局部变量表中(尽管这种说法并不完全正确)。
局部变量表里能存放所有的java类型,八种基本数据类型、引用、returnAddress。
你可以将局部变量表当成一个数组,对应的每一个位置称为slot,在指令中,astore_index表示将局部变量存放在下标为index
的局部变量表里,aload_index表示从局部变量表里取出下标为index的局部变量。
如示例:
public class Test{
public void f(){
//do something
}
public static void main(String[]args){
Test t = new Test();
t.f();
}
}
如以上示例所示,在main方法这个栈帧中,参数args以及引用t都被存放在这个栈帧对应的局部变量表里。为了更清楚地
理解它,下面通过JDK自带的javap命令来查看Test类被编译过后的类文件。
args_size表示这个方法只有一个参数。并且可以清楚地看到指令:
astore_1
aload_1
第一个指令表示将t引用存入局部变量表中下标为1的位置,第二个指令表示从局部变量表中
下标为1的位置取出局部变量。
3、操作数栈
一个方法中避免不了数据的运算,比如:1+1或10*20等等。
操作数栈是提供算术运算的数据结构(当然它并不只有这个功能,还还负责方法调用时的参数
传递)。
举一个例子:
public int function(int a, int b){
int c = a + b;
return c;
}
通过前面的介绍,相信大家知道,这个方法里有三个局部变量,a,b,c,那么它们的运算过程
是如何配合操作数栈来进行的呢?
结果存入局部变量表中。
4、动态连接
java的JDK里提供了成千上万的类,这些类并没有在虚拟机启动时就被加载,而是需要用到时
才会去加载。虚拟机中有一块是运行时常量池,哪些数据应该放入常量池中并不是运行的时候
来决定的,而是java文件被编译成.class文件就已经决定了。
当你用javap命令去查看一个类文件时,会看到如下内容:
你可以看到,每一个常量都对应着一个唯一的符号。当类文件被加载到JVM里时,虚拟机必须
将这些符号引用替换成直接引用,如具体指向某内存地址。
所有加载过程中将符号引用替换成直接引用的过程称为静态解析,它并不是动态连接。
动态连接是在运行期间来调整直接引用的过程,这部分的具体内容会在方法调用中的动态分派
里详解。
5、方法返回地址
这部分功能很简单,不过方法返回地址并不是指方法结束时返回的值。如:
public int f(){
return 10;
}
方法返回地址并非指这里的10。
它是指定当这个方法返回(不管是正常返回还是异常返回)后,下一次应该去执行哪一行!
如例子:
public static void main(String[]args){
int c = f(10, 20);
}
public static int f(int a, int b){
return a + b;
}
当方法f返回时,下一行应该执行main方法里的对变量c赋值操作。
二、方法调用
1、简介
一个功能的实现,涉及到大量的操作就是方法的调用,这里的方法调用并不是指方法体的执行,而是在遇上两难时具体选择
哪一个方法来调用。
在方法的实现上,java语言为我们提供了重载和重写,对于具有面向对象的开发都来说自然知道什么时候调用哪个方法,但
JVM是怎么做的?
2、解析
在方法调用之前,必须先从类文件中解析出来。
不同种类的方法其调用过程也不相同,如静态方法可以直接通过类名调用,而实例方法只能通过实例对象调用,当用
到A a = new A();时会调用实例构造方法,当方法被重写时子类对象会去调用被重写过的方法等。
JVM里提供了四种方法调用字节码指定:
invokestatic:调用静态方法
invokespecial:调用实例构造方法,私有方法,父类方法
invokevirtual:除其它三种以外的方法
invokeinterface:调用接口方法
如:
class Super{
public void say(){
System.out.println("super say");
}
}
public class Sub extends Super{
public void say(){
System.out.println("sub say");
}
public void fatherSay(){
super.say();
}
public void f(){
System.out.println("hello sub");
}
public static void main(String[]args){
Sub s = new Sub();
s.say();
s.fatherSay();
s.f();
}
}
main方法里的第一行,即创建Sub对象时,会出现invokespecial指令,Sub类中fatherSay方法里的super.say()也会出现
invokesecial指令,而s.say(),s.f()则会出现invokevirtual指令。有兴趣的朋友可以用javap命令查看验证。
3、静态分派
静态分派对应着方法重载的实现。
4、动态分派
动态分派对应着方法重写的实现。
由于时间原因,静态分派与动态分派这里不详述了,想更深入了解的朋友可以查阅与JVM相关的书籍。