Jvm(9),运行时数据---独占区---虚拟机栈

一,总览

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

时间: 2024-10-09 20:54:23

Jvm(9),运行时数据---独占区---虚拟机栈的相关文章

Jvm(11),运行时数据---独占区---本地方法栈

本地方法栈主要是来处理native的方法的,我们来看一下什么是native的方法. Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java 程序的功能. 其实在java中我们通用的虚拟机HotSpot中,本地方法栈和虚拟机栈是同一块区域在这里讲的一般是通用的虚拟机. 原文地址:https://www.cnblogs.com/qingruihappy/p/9691301.html

Jvm(8),运行时数据---独占区---程序计数器

一,什么是程序计数器. 程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支.循环.跳转.异常处理.线程恢复等基础功能都需要依赖这个计数器来完成. 说白了就是代码该执行哪一行比如说下面的代码 程序计数器主要记录的就是行号,该执行哪一行不该执行哪一行的代码比如说5--6--10--11 程序计数器存储的就是这些代码. 以上的红色区域就是存储在程序计数器里面的. 二

Jvm(10),运行时数据---独占区---StackOverflowError和OutOfMemoryError区别

1.StackOverflowError 源代码解释说:抛出这个错误是因为递归太深.其实真正的原因是因为Java线程操作是基于栈的,当调用方法内部方法也就是进行一次递归的时候就会把当前方法压入栈直到方法内部的方法执行完全之后,就会返回上一个方法,也就是出栈操作执行上一个方法. public class StackOverflowTest { public static void main(String[] args){ method(); } private static void method

JVM入门——运行时数据区

这张图我相信基本上对JVM有点接触的都应该很熟悉,可以说这是JVM入门的第一课.其中的“堆”和“虚拟机栈(栈)”更是耳熟能详.下面将围绕这张图对JVM的运行时数据区做一个简单介绍. 程序计数器(Program Counter Register) 这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能

jvm的运行时数据区

jvm在java程序运行时会将它所管理的内存划分成不同的区域做不同的功能,这并不难以想象.主要有两类结构,即:堆和栈. java堆主要是保存运行时的对象和数组数据,是所有线程共享的内存区域,在堆中有方法区.运行时常量池.方法区是代码的存储区,类.方法数据.方法的字节码.字段.构造函数等信息都会存储在这里. 运行时常量池是存放类或接口中的方法和数据的常量池,当类或接口被加载的时候就会产生对应的运行时常量池,里面存储了从编译期可知的字面量到运行期才解析出来的方法和字段引用. jvm的线程是通过栈结构

JVM<一>----------运行时数据区域

参考:1.JVM Specification: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5 2.<深入理解Java虚拟机> 刚开始看JVM Specification ,说实话由于专业英语不过关,有些关键词总是看不懂意思,后来参考.<深入理解Java虚拟机>方能感悟到JVM的强大. 我们就先从运行时数据区域开始 一.运行时数据区域分配图 The Java Virtual Machine

JVM学习-运行时数据区

不同于C,C++程序,Java程序的内存管理工作由Java虚拟机(JVM)接管,这减低了java程序员的负担,但如果出现内存泄露与溢出问题如报OutOfMemory,StackOverFlow异常错误时,如果不了解JVM虚拟机的内存管理细节,往往很难快速定位错误. JVM在运行时会把其所管理的内存分为几个不同的数据区域,分别为:程序计数器,虚拟机栈,本地方法栈,堆,方法区等.这些区域存放的数据不同,功能也不同. JVM管理的内存包含以下几个运行时数据区: 1.程序计数器 程序计数器是一块较小的内

jvm(运行时数据区域)

以上是jvm在运行时内存的数据分区图例(各个分区简介): 1.程序计数器:           在jvm中一块很小的区域,主要作用就是记录当前线程执行字节码的行号指示器.           在单核的多线程中,cpu会在不同线程之间切换,为了切换回来时正确的回到当前线程的执行位置,           每个线程都有自己单独的程序计数器,之间互不影响,独立运行,同时这块区域也是在java虚拟机           规范中唯一没有规定任何OutOfMemoryError情况的区域. 2.虚拟机栈:

运行时数据区域(堆 栈 方法区 常量池)和内存分配策略

内存管理 内存分配和内存释放 内存分配由程序完成,内存释放由GC完成 运行时数据区域 (1)程序计数器(program counter register) 一块较小的内存空间 当前线程所执行的字节码的行号指示器,字节码解释器在工作的时候就是通过改变程序计数器的值来选取下一跳要执行的指令 多线程环境下,线程轮流切换执行,程序计数器保证线程切换之后能恢复到正确的位置 每个线程都有一个独立的程序计数器 线程私有 没有任何异常 java方法,程序计数器的值为当前正在执行的虚拟机字节码指令的地址 nati