JVM学习--虚拟机栈
概述
- 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更过的指令
- 栈是运行时单位,堆是存储的单位
- 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(基本单位),对应一个个Java方法的调用,是线程私有的
- 生命周期和线程一致
- 主管java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址)、部分结果,并参与方法的调用和返回
- 栈不存在GC问题,存在OOM
栈中可能出现的异常
StackOverflow
public class StackErrorTest { public static void main(String[] args) main(args); } }
栈的存储单位
- 每个线程都有自己的栈,栈中的数据是以栈帧的格式存在,
- 在这个线程正在执行的每个方法都各自对应的一个栈帧,
- 栈帧是一个内存区块,是一个数据集
- JVM对栈的操作只有两个:压栈、出栈,遵循先进先出/后进后出的原则
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,对应的方法就是当前方法,定义这个方法的类就是当前类
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作、
- 如果在当前栈帧中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈顶,成为新的当前帧。
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常(没有处理的异常)。这两种方式都会导致栈帧被弹出
栈帧内部结构
- 局部变量表(local variables)
- 操作数栈(operand stack)||表达式栈
- 动态链接(dynamic linking) || 指向运行时常量池的方法引用
- 方法返回地址(return address) || 方法正常退出或异常退出的定义
- 一些附加信息
局部变量表
- 局部变量表也被称为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,包括基本数据类型,对象引用,以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程私有的,因此不存在数据安全问题
- **局部变量表所需要的大小是在编译期确定下来的,**并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小。
编译后使用Jclasslib查看
public class LocalVariablesTest { private int count = 0; public static void main(String[] args) { LocalVariablesTest test = new LocalVariablesTest(); int num = 10; test.test1(); } public void test1() { Date date = new Date(); String name1 = "atguigu.com"; test2(date, name1); System.out.println(date + name1); } public String test2(Date dateP, String name2) { dateP = null; name2 = "songhongkang"; double weight = 130.5;//占据两个slot char gender = '男'; return dateP + name2; } //练习: public static void testStatic(){ LocalVariablesTest test = new LocalVariablesTest(); Date date = new Date(); int count = 10; System.out.println(count); } }
- 方法嵌套调用的次数由栈的大小决定,方法大小由局部变量表决定
- 局部变量表中的变量只在当前方法调用中有效,方法调用结束后,方法栈帧销毁,局部变量表也随之销毁
字节码结构
关于slot的理解
-
参数值的存放总是在局部变量表数组的index0开始,到数组长度-1的索引结束
-
局部变量表最基本的单元就是变量槽(slot)
-
局部变量表存放编译器可知的各种基本数据类型(8中),引用数据类型,return address类型的变量
-
在局部变量表中 ,32位以内的类型只占用一个slot(包括return address)64位类型(long、double)占用两个slot
byte、short、char在存储前转换为int 、boolean转换为int,0=false,!0=true -
JVM会为局部变量表中的每一个slot都分配一个索引,
-
当一个实力方法被调用的时候。它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每个slot
-
如果需要访问局部变量表中一个64bit的局部变量值时,只需要调用前一个所以即可
-
如果当前帧是由构造方法或者实例方法创建的。**那么该对象引用this将会存放在index为0的slot处。**所以在静态方法中不能调用非静态变量或方法,因为静态方法对象引用this存放在slot。
- 栈帧中局部变量表中的槽位是可以重用的,达到节省资源的目的
public void test4() { int a = 0; { int b = 0; b = a + 1; } //变量c使用之前已经销毁的变量b占据的slot的位置 int c = a + 1; }
静态变量和局部变量的对比
变量的分类:按照数据类型分:① 基本数据类型 byte short char int float long double boolean
② 引用数据类型 数组 类 接口
按照在类中声明的位置分:① 成员变量:在使用前,都经历过默认初始化赋值
类变量: linking的prepare阶段:给类变量默认赋值 —> initial阶段:给类变量显式赋值即静态代码块赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
补充
- 局部变量表中的变量也是重要的垃圾回收根节点,主要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈
- 操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 当一个方法开始执行的时候,一个新的栈帧也会被创建,这个方法的操作数栈是空的
- 每个操作数栈都会由一个明确的栈深度用于存储数值。其所需的最大深度在编译期就定义好了,保存在Code属性中
- 栈中任何一个元素都是可以任意的java数据类型
32bit的类型占用一个栈单位深度
64bit的类型占用来两个栈单位深度 - 操作数栈只能通过push和pop来进行操作
- 如果被调用的方法带有返回值,其返回值将会被压入当前栈帧的操作数栈中
public void testAddOperation() { //byte、short、char、boolean:都以int型来保存 byte i = 15; int j = 8; int k = i + j; }
动态链接
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
- 静态链接/早期绑定
如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程成为静态链接 - 动态链接/晚期绑定
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
class Animal{ public void eat(){ System.out.println("动物进食"); } } interface Huntable{ void hunt(); } class Dog extends Animal implements Huntable{ @Override public void eat() { System.out.println("狗吃骨头"); } @Override public void hunt() { System.out.println("捕食耗子,多管闲事"); } } class Cat extends Animal implements Huntable{ public Cat(){ super();//表现为:早期绑定 } public Cat(String name){ this();//表现为:早期绑定 } @Override public void eat() { super.eat();//表现为:早期绑定 System.out.println("猫吃鱼"); } @Override public void hunt() { System.out.println("捕食耗子,天经地义"); } } public class AnimalTest { public void showAnimal(Animal animal){ animal.eat();//表现为:晚期绑定 } public void showHunt(Huntable h){ h.hunt();//表现为:晚期绑定 } }
虚方法和非虚方法
- 非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。.
其他方法称为虚方法。
class Father { public Father() { System.out.println("father的构造器"); } public static void showStatic(String str) { System.out.println("father " + str); } public final void showFinal() { System.out.println("father show final"); } public void showCommon() { System.out.println("father 普通方法"); } } public class Son extends Father { public Son() { //invokespecial super(); } public Son(int age) { //invokespecial this(); } //不是重写的父类的静态方法,因为静态方法不能被重写! public static void showStatic(String str) { System.out.println("son " + str); } private void showPrivate(String str) { System.out.println("son private" + str); } public void show() { //invokestatic showStatic("atguigu.com"); //invokestatic super.showStatic("good!"); //invokespecial showPrivate("hello!"); //invokespecial super.showCommon(); //invokevirtual showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。 //虚方法如下: //invokevirtual showCommon(); info(); MethodInterface in = null; //invokeinterface in.methodA(); } public void info(){ } public void display(Father f){ f.showCommon(); } public static void main(String[] args) { Son so = new Son(); so.show(); } } interface MethodInterface{ void methodA(); }
方法重写的本质
虚方法表
方法返回地址
一些题目
- 举例栈溢出的情况
StackOverflowError
- 通过-Xss设置栈的大小
OOM - 调整栈的大小,能保证不出现溢出吗?
答:不能;只能延后溢出 - 垃圾回收会涉及到栈吗?
答:不会;
Error | GC | |
---|---|---|
PC计数器 | X | X |
虚拟机栈 | √ | X |
本地方法栈 | √ | X |
堆 | √ | √ |
方法区 | √ | √ |
- 分配的栈内存越大越好吗?
答:并不是
- JVM学习之:虚拟机中的运行时栈帧总结(一)
- JVM学习笔记(二)HotSpot虚拟机对象探秘
- java 学习篇(JVM(java 虚拟机)-GC)
- jvm虚拟机学习
- 学习jvm虚拟机
- Java虚拟机JVM学习03 连接过程:验证、准备、解析
- JVM学习--认识虚拟机
- 1.17java学习之JVM(虚拟机)
- 学习深入理解jvm虚拟机心得——谈谈GC垃圾收集器和回收策略(2)
- 《深入理解 Java 虚拟机》学习笔记(1)—— JVM 运行时数据区
- JVM学习之:虚拟机中的运行时栈帧总结
- JVM虚拟机学习讲解
- JVM虚拟机学习笔记
- Java虚拟机jvm学习三:垃圾回收算法
- 五分钟学Java:如何学习Java面试必考的JVM虚拟机
- JVM学习笔记:虚拟机的类加载机制
- 深入JVM学习笔记-虚拟机体系结构
- JVM虚拟机学习笔记01:类的加载、连接、初始化
- JVM 学习 第 2 课 虚拟机class文件从哪里寻找?哪里来?
- Java虚拟机jvm学习二:常用JVM配置参数