本文共 9842 字,大约阅读时间需要 32 分钟。

List list = new ArrayList(); 把这种比喻用到方法区则有:java 7中:方法区 f = new 永久代();
方法去 f = new 元空间();
注意:即,栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪里。
栈是线程私有,不存在垃圾回收
虚拟机栈的生命周期同线程一致
栈帧的概念:java中的方法被扔进虚拟机的栈空间之后就成为“栈帧”,比如main方法,是程序的入口,被压栈之后就成为栈帧。
栈的作用:
主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

1. 局部变量表
StartPC:变量的作用域起始字节码指令位置,Length:作用域长度;StartPC+Length=总字节码指令数
JVM会为每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,如果要访问一个64bit的局部变量值时,只需要使用两个slot中的第一个slot的索引即可。如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会放在index=0的slot处,其余的参数按照参数表顺序继续排列,这也就解释了为什么静态方法中不可以引用this,因为this变量(当前对象的引用)不存在于静态方法的局部变量表中。此外,栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
静态变量与局部变量的对比及小结
变量的分类:
按照数据类型分:
按照在类中声明的位置分:
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的操作数栈,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即操作数栈的入栈/出栈。例如某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)
比如:执行复制、交换、求和等操作
如果说Java虚拟机的解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈。
操作数栈主要用于保存计算过程中的中间结果,同时作为计算过程中的变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
栈中任何一个元素都是可以任意的Java数据类型,其中,32bit类型占用一个栈单位深度,64bit类型占用两个。
操作数栈不同于局部变量表,并非采用访问索引的方式来进行数据访问,而是通过标准的入栈出栈操作来完成一次数据访问。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈代码追踪
结合上图结合下面的图来看一下一个方法(栈帧)的执行过程
①15入栈;②存储15,15进入局部变量表
注意:局部变量表的0号位被构造器占用,这里的15从局部变量表1号开始
③压入8;④8出栈,存储8进入局部变量表;
⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈;⑥iadd相加操作
⑦iadd操作结果23出栈⑧将23存储在局部变量表索引为3的位置上istore_3 
栈顶缓存技术ToS(Top-of-Stack Cashing)
运行时常量池位于方法区(注意: JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。)
每一个栈帧内部都包含一个指向运行时常量池(运行时常量池是在方法区里的)中该栈帧所属方法的引用。这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如:invokedynamic指令。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其它方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
常量池的作用就是为了提供一些符号和常量以便于指令的识别,以节约内存。

方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Bingding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
随着高级语言的横空出世,类似于java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装,集成和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。虚方法和非虚方法
子类对象的多态性使用前提:
①类的继承关系(父类的声明)②方法的重写(子类的实现)实际开发编写代码中用的接口,实际执行是导入的的三方jar包已经实现的功能
非虚方法
虚方法
虚拟机中提供了以下几条方法调用指令
普通调用指令:
1、invokestatic:调用静态方法,解析阶段确定唯一方法版本; 2、invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本; 3.invokevirtual调用所有虚方法; 4.invokeinterface:调用接口方法; 动态调用指令(Java7新增): 5.invokedynamic:动态解析出需要调用的方法,然后执行 . 前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法
其中invokevirtual(final修饰的除外,JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法)invokeinterface指令调用的方法称称为虚方法。
/** * 解析调用中非虚方法、虚方法的测试 */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(); } //不是重写的父类方法,因为静态方法不能被重写 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(){ //invokestatic showStatic(" 大头儿子"); //invokestatic super.showStatic(" 大头儿子"); //invokespecial showPrivate(" hello!"); //invokespecial super.showCommon(); //invokevirtual 因为此方法声明有final 不能被子类重写,所以也认为该方法是非虚方法 showFinal(); //虚方法如下 //invokevirtual showCommon();//没有显式加super,被认为是虚方法,因为子类可能重写showCommon info(); MethodInterface in = null; //invokeinterface 不确定接口实现类是哪一个 需要重写 in.methodA(); } public void info(){ }}interface MethodInterface { void methodA();} 关于invokedynamic指令
动态类型语言和静态类型语言
Java:String info = "test";//静态语言JS:var name = "test“;var name = 10;//动态语言Pythom: info = 130;//更加彻底的动态语言
方法重写的本质
虚方法表
存放调用该方法得到PC寄存器的值
一个方法的结束,有正常执行完成和出现未处理的异常从而非正常退出两种方式,无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条地址。如果是异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分的信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。(很多资料都忽略了附加信息)
开发过程中,关于虚拟机栈可能的异常?
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
public class Test{ public static void m(){ m(); } public static void main(String[] args) { System.out.println("111"); //Exception in thread "main" java.lang.StackOverflowError m(); System.out.println("222"); }}/**output:* 111* Exception in thread "main" java.lang.StackOverflowError* */ 注意:
- StackOverflowError是一个“错误”,而不是“异常”。
1.举例栈溢出的情况?(StackOverflowError)
2.调整栈的大小,就能保证不出现溢出么?
3.分配的栈内存越大越好么?
4.垃圾回收是否会涉及到虚拟机栈?
5.方法中定义的局部变量是否线程安全?
注意:
三种JVM

转载地址:http://ukkaz.baihongyu.com/