一,总览
Java Virtual Machine Stacks,线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),由于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行就对应着栈帧在虚拟机栈中的入栈,出栈过程。
我们来看一个例子
public class Demo3 {
public void test1() {
System.out.println("我是test1的方法");
test2();
}
public void test2() {
System.out.println("我是test2的方法");
test3();
}
public void test3() {
System.out.println("我是test3的方法");
}
public static void main(String[] args) {
Demo3 demo3=new Demo3();
demo3.test1();
}
}
我是test1的方法我是test2的方法我是test3的方法
从上面的图中我们可以看出java代码在虚拟机栈中的运行规律。
二,局部变量表
存放编译期可知的各种基本数据类型、对象引用类型和returnAddress类型(指向一条字节码指令的地址:函数返回地址)。
long、double占用两个局部变量控件Slot。
局部变量表所需的内存空间在编译期确定,当进入一个方法时,方法在栈帧中所需要分配的局部变量控件是完全确定的,不可动态改变大小。
异常:线程请求的栈帧深度大于虚拟机所允许的深度---StackOverFlowError,如果虚拟机栈可以动态扩展(大部分虚拟机允许动态扩展,也可以设置固定大小的虚拟机栈),但是无法申请到足够的内存---OutOfMemorError。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、 short、int、
float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对
象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress类型(指向了一条字节码指令的地址)。
注意上面标红的地方,
我们举个例子比如有一个user对象,现在里面有name的属性,假如现在name发生变化了,那么栈中的大小还不会产生变化吗?答案是肯定的,因为这个时候栈中只是堆对象中的一个引用,假如name的属性发生变化了,那么,在堆内从中的那么就会新产生一个name 的属性,而在栈中就会产生一个新的name的属性,新的name的属性还是大小确定了的不会发生变化的。因为大小在编译器编译成class文件的时候就已经决定了它的大小。
三,操作数栈
后进先出LIFO,最大深度由编译期确定。栈帧刚建立使,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。
操作数栈可以存放一个jvm中定义的任意数据类型的值。
在任意时刻,操作数栈都一个固定的栈深度,基本类型除了long、double占用两个深度,其它占用一个深度
就是在上图中test1,test2,test3,里面具体装的局部变量等数据的。
四,动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
4.1,静态链接
那么,首先,咱们先来聊聊静态链接。
如上面的概念所述,在C/C++中静态链接就是在编译期将所有类加载并找到他们的直接引用,不论是否使用到。而在Java中我们知道,编译Java程序之后,会得到程序中每一个类或者接口的独立的class文件。虽然独立看上去毫无关联,但是他们之间通过接口(harbor)符号互相联系,或者与Java API的class文件相联系。
我们之前也讲述了类加载机制中的一个过程—解析,并在其中提到了解析就是将class文件中的一部分符号引用直接解析为直接引用的过程,但是当时我们并没有详细说明这种解析所发生的条件,现在我给大家进行补充:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。可以概括为:编译期可知、运行期不可变。
符合上述条件的方法主要包括静态方法和私有方法两大类。前者与类型直接关联,后者在外部不可被访问,这两种方法的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们适合在类加载阶段进行解析。
额外补充一点:在Java虚拟机中提供了5条方法调用字节码指令,其中invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法(不知道这是个什么玩意、不重要,先放下)4类。它们在类加载的时候就会把符号引用解析为该方法的直接引用,因此这些方法也被称为非虚方法(包括 final方法),与之相反的称为虚方法。
解析调用一定是个静态过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用转化为可确定的直接引用,不会延迟到运行期再去完成,这也就是Java中的静态链接。
我们通过java中的重载来看一下。
Human man = new Man();
如上代码,Human被称为静态类型,Man被称为实际类型。
再来看一段代码:
//实际类型变化
Human man = new Man(); man = new Woman();
//静态类型变化
StaticDispatch sr = new StaticDispatch(); sr.sayHello((Human) man); sr.sayHello((Woman) man);
可以看到的静态类型和实际类型都会发生变化,但是有区别:静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的,而实际类型变化的结果在运行期才可确定。
知道这些东西之后,我给大家贴上完整代码:
public class Demo2 {
static class Human {
}
static class Man extends Human {
public Man() {
}
}
static class Woman extends Human {
}
public static class StaticDispatch {
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);
sr.sayHello((Man) man);
sr.sayHello((Woman) woman);
}
}
hello, guy! hello, guy!
hello, gentleman! hello, lady! 如上代码与运行结果,在调用 sayHello()方法时,方法的调用者都为sr的前提下,使用哪个重载版本,完全取决于传入参数的数量和数据类型。代码中刻意定义了两个静态类型相同、实际类型不同的变量,可见编译器(不是虚拟机,因为如果是根据静态类型做出的判断,那么在编译期就确定了)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,所以在编译阶段,javac 编译器就根据参数的静态类型决定使用哪个重载版本。这就是静态分派最典型的应用。
通过上面的例子我们可以明确的看到,尤其是打印出hello, guy!的代码,我们可以看到这个时候它实际上是执行的Human guy的代码,因为按照虚拟机的原理,我们可以看到,这个时候其实就是静态的就是在编译的时候就已经确定了要走谁的代码。
4.2,动态链接
上面大概说完了静态链接,那么什么是动态链接、它有什么用?如上所述,在Class文件中的常量持中存有大量的符号引用。字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。
与那些在编译时进行链接的语言不同,Java类型的加载和链接过程都是在运行的时候进行的,这样虽然在类加载的时候稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖动态加载和动态链接这个特点实现的。
class Eat {
}
class Drink {
}
class Father {
public void doSomething(Eat arg) {
System.out.println("爸爸在吃饭");
}
public void doSomething(Drink arg) {
System.out.println("爸爸在喝水");
}
}
class Child extends Father { public void doSomething(Eat arg) {
System.out.println("儿子在吃饭");
}
public void doSomething(Drink arg) {
System.out.println("儿子在喝水");
} }
public class SingleDoublePai {
public static void main(String[] args) {
Father father = new Father(); Father child = new Child(); father.doSomething(new Eat()); child.doSomething(new Drink());
}
}
爸爸在吃饭
看编译阶段编译器的选择过程,即静态分派过程。这时候选择目标方法的依据有两点:一是方法的接受者(即调用者)的静态类型是 Father 还是 Child,二是方法参数类型是 Eat 还是 Drink。因为是根据两个宗量进行选择,所以 Java 语言的静态分派属于多分派类型。
再来看运行阶段虚拟机的选择,即动态分派过程。由于编译期已经了确定了目标方法的参数类型(编译期根据参数的静态类型进行静态分派),因此唯一可以影响到虚拟机选择的因素只有此方法的接受者的实际类型是 Father 还是 Child。因为只有一个宗量作为选择依据,所以 Java 语言的动态分派属于单分派类型。
根据以上论证,我们可以总结如下:目前的 Java 语言(JDK1.6)是一门静态多分派(方法重载)、动态单分派(方法重写)的语言。从上面我们可以看到,这个时候其实newEat 和new Drink都是运行的时候才确定是谁的。
五,模拟栈内存溢出StackOverflowError
public class Demo4 {
public void test1() {
test1();
}
public static void main(String[] args) {
Demo4 demo4=new Demo4();
demo4.test1();
}
}
Exception in thread "main" java.lang.StackOverflowError
就是栈中装满信息了这个时候就会溢出的。
原文地址:https://www.cnblogs.com/qingruihappy/p/9691293.html