由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
栈是运行时的单位,而堆是存储的单位
java虚拟机栈是什么?
java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这个一次次的java方法调用。它是线程私有的。
生命周期
生命周期和线程是一致的
作用:
主管java程序的运行,它保存方法的局部变量、8种基本数据类型、对象的引用地址、部分结果,并参与方法的调用和返回。
局部变量:相较于成员变量(成员变量或称属性)
基本数据变量:8种基本数据类型(shrot,int ,long,char,)
引用类型变量:类,数组,接口
栈的特点(优点) | |
---|---|
● 栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器(程序计数器) | |
● JVM直接对Java栈的操作只有两个: | |
(1)➢每个方法执行,伴随着进栈(入栈、压栈) | |
(2)➢执行结束后的出栈工作 | |
● 对于栈来说不存在GC(垃圾回收)问题,但是存在OOM(数据溢出) |
栈中可能出现的异常
/**
* 演示栈中的异常
* 自己调自己出现死循环
*/
public class StackErrorTest {
public static void main(String[] args) {
main(args);
}
}
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 (IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k)
/**
* 演示栈中的异常
*
* 默认情况下:count 11420
* 设置栈的大小: -Xss256k count 1872
*/
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
设置栈的大小
修改大小 -Xss256k ,记住命令是-Xss
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
在这个线程上正在执行的每个方法都对应各自的一个栈帧(一个方法的执行,就对应着一个栈帧的入栈,一个方法的执行结束,就对应着一个栈帧的出栈)
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的和原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame)定义这个方法的类就是当前类(Current Class)
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest test = new StackFrameTest();
test. method1() ;
}
public void method1(){
System. out. println("method1()开始执行...");
method2();
System. out . println("method1()执行结束...");
}
private int method2() {
System. out. println("method2()开始执行...");
inti=10;
int m = (int) method3();
System. out . println("method2()即将结束...");
return i + m;
}
private double method3() {
System. out . println( "method3()开始执行...");
double j = 20.0;
System. out. pr intln("method3()即将结束...");
return j;
}
}
不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
● 每个栈帧中存储着:
● 局部变量表(Local variables)
● 操作数栈(operand stack) ( 或表达式栈)
● 动态链接(Dynamic Linking) ( 或指向运行时常量池的方法引用)
● 方法返回地址(Return Address) (或方法 正常退出或者异常退出的定义)
●一些附加信息
5. 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间。
6. 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
局部变量表
StartPC字节码指令行号
Length描述当前变量的作用域的范围
Index索引
Name变量名
●参数值的存放总是在局部变量数组的index0开始, 到数组长度-1的索引结束。
●局部变量表,最基本的存储单元是Slot (变量槽)
● 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型.(reference),returnAddress类 型的变量。
●在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long 和double)占用两个slot。
➢byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false ,非0表示true。
➢long和double 则占据两个Slot。
●JVM会为 局部变量表中的每一个Slot都分配一个访问索引,通过这个素引即可成功访问到局部变量表中指定的局部变量值
●当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
●.如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一一个索引即可。(比如:访问1ong或double类型变量)
●如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
●静态方法中不能引用this,是因为静态方法所对应的栈帧当中的局部变量表中不存在this
public class LocalVariablesTest {
private int count = 1;
//静态方法不能使用this
public static void testStatic(){
//编译错误,因为this变量不存在与当前方法的局部变量表中!!!
System.out.println(this.count);
}
}
补充说明
结合上图结合下面的图来看一下一个方法(栈帧)的执行过程
①15入栈;②存储15,15进入局部变量表
注意:局部变量表的0号位被构造器占用,这里的15从局部变量表1号开始
③压入8;④8出栈,存储8进入局部变量表;
⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈;⑥iadd相加操作
⑦iadd操作结果23出栈⑧将23存储在局部变量表索引为3的位置上istore_3
● 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指 令
● 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference) 保存在class文 件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。下面提供一张测试类的运行时字节码文件格式
每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令
在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class字节码文件(javap反编译查看)的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用。
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
对应的方法的绑定机制为:早起绑定(Early Binding)和晚期绑定(Late Bingding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
随着高级语言的横空出世,类似于java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装,集成和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
非虚方法:
其他所有体现多态特性的方法称为虚方法
子类对象的多态性使用前提:
① 类的继承关系(父类的声明)
② 方法的重写(子类的实现)
实际开发编写代码中用的接口,实际执行是导入的的三方jar包已经实现的功能
虚拟机中提供了以下几条方法调用指令
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而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();
}
Java:String info = "硅谷";//静态语言
JS:var name = "硅谷“;var name = 10;//动态语言
Pythom: info = 130;//更加彻底的动态语言
interface Friendly{
void sayHello();
void sayGoodbye();
}
class Dog {
public void sayHello() { }
public String toString() {
return "Dog" ;
}
}
class CockerSpaniel extends Dog implements Friendly{
public void sayHello(){
super.sayHello();
}
public void sayGoodbye(){
}
}
Dog类的虚方法表:
可卡犬虚方法表:可卡犬若是使用toString方法无需向上找Object类,只需找到Dog类即可;这是一个效率的提升
class Cat implements Friendly{
public void eat(){ }
public void sayHello(){}
public void sayGoodbye(){}
protected void finalize(){ }
public String toString(){ }
}
存放调用该方法的PC寄存器的值。
一个方法的结束,有两种方式:
– 正常执行完成
– 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者(方法的调用者可能也是一个方法)的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定.
在字节码指令中,返回指令包含ireturn(当返回值是boolena、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn(引用类型的)
在字节码当中的4~11行是可能存在异常的代码,11代表字节码中能够处理该异常的位置是第11行也就是上图中的第72行
栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。(很多资料都忽略了附加信息)
/**
* 面试题:
* 方法中定义的局部变量是否线程安全?具体情况具体分析
*
* 何为线程安全?
* 如果只有一个线程可以操作此数据,则必定是线程安全的。
* 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题
*
* 我们知道StringBuffer是线程安全的源码中实现synchronized,StringBuilder源码未实现synchronized,在多线程情况下是不安全的
* 二者均继承自AbstractStringBuilder
*
*/
public class StringBuilderTest {
//s1的声明方式是线程安全的,s1在方法method1内部消亡了
public static void method1(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//stringBuilder的操作过程:是不安全的,
//stringBuilder是从外面传进来的,有可能被多个线程调用了,因为method2可以被多个线程调用
public static void method2(StringBuilder stringBuilder){
stringBuilder.append("a");
stringBuilder.append("b");
}
//s1的操作:是线程不安全的 有返回值,可能被其他线程共享
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是线程安全的 ,StringBuilder的toString方法是创建了一个新的String,s1在内部消亡了
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(()->{
s.append("a");
s.append("b");
}).start();
method2(s);
}
}