Java虚拟机中,每个线程的启动都会相应的开辟一个Java虚拟机栈,所以Java虚拟机栈的生命周期和线程是同步的,它也是线程私有的。虚拟机栈中的存储单位是栈帧(Stack Frame ),当线程执行方法的时候,就会在虚拟机栈中创建一个栈帧用来存储当前方法相关的数据(如下图),当方法执行完后就会进行栈帧出栈操作。一条活动线程在某一个时间点上,只会有一个活动的栈帧,即只有当前正在执行方法的栈帧(栈顶栈帧)是有效的,被称作当前栈帧,与之对应的方法,那它就是当前方法,他所在的类就是当前类。由于栈的特性,在某一时刻,有且只有一个方法被线程执行。每一个方法从调用到执行完毕的过程,都对应着一个栈帧从虚拟机栈里面从入栈到出栈的过程。
虚拟机栈如果内存不够时,会报StackOverFlowError异常(递归调用如果没有出口,就会引发此异常)
执行引擎运行的所有字节码指令只针对当前栈帧操作;
如果当前方法中调用了其他方法,则其他方法对应的栈帧就会被创建出来,进行压栈,放到栈顶,成为当前栈帧。栈帧中又包括以下几个结构(如下图) 每个栈帧的大小在程序编译期间就确定了,并写入方法的Code属性中
用于存放方法的参数和方法内部定义的局部变量。
基本数据类型的值直接存储在局部变量表中,则引用类型存放的是引用地址。
在程序编译期间就确定了局部变量表的大小,存放在方法Code属性中的max_locals数据项中。
局部变量表的容量是以**变量槽(Slot)**为单位,一个变量槽占32位(Hotspot虚拟机)
Java虚拟机通过索引定位的方式使用局部变量表, 索引值的范围是从0开始至局部变量表最大的变 量槽数量。如果执行的是实例方法(不是被static修饰的)那局部变量表的第0索引位置存放的是this变量,其他变量则按照代码书写顺序一次存放,如果碰到double、float类型,则占用连续的两个Slot
局部变量表中的变量槽是可以重用的
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
它的大小跟局部变量表一样,在编译阶段就被确定了,存放在Code属性的max_stacks数据项中,存储单位是1(32位)。
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标砖的入栈push和出栈pop操作来完成一次数据访问
**如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,**并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
当一个方法刚开始执行的时候,操作数栈是空的,局部变量表也是空的,数据首先是进入操作数栈,然后通过字节码指令把数据压入局部变量表。如下图
把上图的执行步骤拆分来看,如下图
每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令。
在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
下面看一下运行时常量池,最左边一列则为常量池
当一个方法开始执行后,只有两种方式退出。
第一种,当执行引擎遇到任意一个方法返回的字节码指令(return、ireturn、lreturn、freturn、dreturn……),这时候如果有返回值的话,则把返回值返回给调用者,没有返回值就正常回到调用处,这种属于正常退出;第二种,当方法执行过程中出现了异常,如果方法中没有正常的异常处理机制,则不会给调用者返回任何信息。正常退出时,主调方法的程序计数器的值作为返回地址;异常退出时,返回地址需要异常处理表来确定。JVM中,符号引用转为方法的直接引用,与方法的绑定机制相关
绑定是一个字段、方法、或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
当一个class文件被加载进JVM中时,如果被调用的方法在编译期间可知,且运行期间保持不变,这种情况下将调用方法的符号引用转为直接引用的过程称为静态绑定。
如果被调用的方法在编译期间无法被确定下来,只有在运行期间才可以将调用方法的符号引用转为直接引用,由于这种引用转换过程具有动态性,所以称为动态绑定。
早期绑定对应的就是静态绑定,是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定对应的就是动态绑定,如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
interface Huntable { void hunt(); } class Animal { public void eat(){ System.out.println("动物吃东西"); } } class Cat extends Animal implements Huntable{ public Cat(){ super(); //早期绑定,编译期就可以确定调用的是父类构造方法 非虚方法 } public Cat(String name){ this(); //早期绑定,编译期就可以确定调用的是自身的无参构造 非虚方法 } @Override public void eat() { System.out.println("猫吃鱼"); } @Override public void hunt() { System.out.println("拿耗子干正事"); } } class Dog extends Animal implements Huntable{ @Override public void eat() { System.out.println("够吃骨头"); } @Override public void hunt() { System.out.println("狗拿耗子多管闲事"); } } public class DynamicLinkingTest { public void showAnimal(Animal animal){ animal.eat(); //表现为:晚期绑定,不确定animal代表的是Dog还是Cat 虚方法 } public void showHunt(Huntable hunt){ hunt.hunt(); //表现为:晚期绑定,不确定hunt代表的是狗拿耗子还是猫捉老鼠 虚方法 } }如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
静态方法(被static修饰)、私有方法、final修饰的方法、实例构造器、父类方法都是非虚方法
上面2.3.1方法调用中的早期绑定和晚期绑定就是指的调用非虚方法和虚方法,调用非虚方法和虚方法都有对应的字节码指令,下面会提到。
除了上面提到的五类非虚方法之外,其他的方法都是虚方法。
虚方法跟多态关系很紧密,因为在编译期间确定。
普通调用指令
invokestatic:调用静态方法,解析阶段确定唯一方法版本
invokespecial:调用方法、私有方法、父类方法、解析阶段确定唯一方法版本
invokevirtual:调用所有虚方法
invokeinterface:调用接口方法
以上这四条指令固化在虚拟机内部,方法的调用执行认为不可干预,而invokedynamic指令支持用户确定方法版本;其中invokestatic指令和invokespecial指令调用的方法成为非虚方法,其余的(final修饰的除外)称为虚方法。
class Father { public Father(){ System.out.println("Father默认构造器"); } public static void showStatic(String s){ System.out.println("Father show static"+s); } public final void showFinal(){ System.out.println("Father show final"); } public void showCommon(){ System.out.println("Father show common"); } } public class Son extends Father{ public Son(){ super(); } public Son(int age){ this(); } public static void main(String[] args) { Son son = new Son(); son.show(); } /** * 父类方法是static修饰,不能重写,这是子类自己的方法 */ public static void showStatic(String s){ System.out.println("Son show static"+s); } private void showPrivate(String s){ System.out.println("Son show private"+s); } public void show(){ showStatic("子类静态方法"); super.showStatic("父类静态方法"); showPrivate(" hello!"); super.showCommon(); showFinal(); showCommon(); info(); MethodInterface in = null; in.methodA(); } public void info(){} } interface MethodInterface { void methodA(); }动态调用指令
invokedynamic:动态解析出需要调用的方法,然后执行
JVM字节码指令集一直比较稳定,一直到java7才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进
但是java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令.直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在java中才有了直接生成方式
Java7中增加的动态语言类型支持的本质是对java虚拟机规范的修改,而不是对java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在java凭条的动态语言的编译器
动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的却别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言。
直白来说 静态语言是判断变量自身的类型信息;动态类型预言师判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征
Java是静态类型语言(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言。
找到操作数栈的第一个元素所执行的对象的实际类型,记作C。
如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。 IllegalAccessError介绍 程序视图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
虚方法表
在面向对象编程中,会很频繁期使用到动态分派,如果在每次动态分派的过程中都要重新在累的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,jvm采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。那么虚方法表什么时候被创建? 虚方法表会在类加载的链接阶段被创建 并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方发表也初始化完毕。1.举例栈溢出的情况?(StackOverflowError)
递归调用等,通过-Xss设置栈的大小;2.调整栈的大小,就能保证不出现溢出么?
不能 如递归无限次数肯定会溢出,调整栈大小只能保证溢出的时间晚一些3.垃圾回收是否会涉及到虚拟机栈?
不会
运行时数据区域ErrorGC程序计数器❌❌本地方法栈✅❌Java虚拟机栈✅❌堆✅✅方法区✅✅4.分配的栈内存越大越好么?
不是 会挤占其他线程的空间5.方法中定义的局部变量是否线程安全?
局部变量的作用域如果只在当前方法有效,则为线程安全;如果不仅仅在当前方法有效(变量逃逸),则线程不安全。关于逃逸分析,请看另外一篇文章“逃逸分析”。