| 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
Java虚拟机栈的生命周期和线程是一致的
主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
| 局部变量 VS 成员变量(属性)
| 基本数据类型变量 VS 引用类型变量(类、数组、接口)
| java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
| 我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k
| byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true,引用类型也是32;
| long和double则占据两个slot,long和double占8个字节,一个字节8位,8*8=64。
栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量出了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
| 变量的分类:
| static修饰:类变量:类加载linking的准备阶段给类变量默认赋值—>初始化阶段给类变量显式赋值即静态代码块赋值;
| 没有被static修饰:实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
| 食材列表(局部变量表) —> 做法步骤(做菜的过程就可以理解为操作数栈的执行过程)
| 比如:执行复制、交换、求和等操作
| 操作数栈是空的,意思是说数组已经创建,但是数组内部是空的。在创建数组的时候,数组长度就确定了。
| 32bit的类型占用一个栈单位深度
| 64bit的类型占用两个栈深度单位
| 只是以数组的方式来实现,其本身并不是数组,而是栈
| 上图中局部变量表长度是4(3个变量+1个变量this),操作数栈深度是2
| 小补充
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令
在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
| 为什么不直接把方法放到运行时常量池,要使用方法的引用?为什么需要常量池呢?
当一个 字节码文件被装载进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方法、实例构造器、父类方法都是非虚方法
| 其他方法称为虚方法(编译器期间不确定调用哪个方法)
/** * 解析调用中非虚方法、虚方法的测试 * * invokestatic指令和invokespecial指令调用的方法称为非虚方法 */ 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("ysy.com"); //invokestatic,非虚方法 super.showStatic("good!"); //invokespecial,非虚方法 showPrivate("hello!"); //invokespecial,非虚方法 super.showCommon(); //invokevirtual本来是调用虚方法,但是因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。 showFinal(); //虚方法如下: //invokevirtual // 没有显式加super,被认为是虚方法,因为子类可能重写showCommon 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(); }
| 虚方法表什么时候被创建?
虚方法表会在类加载的链接阶段(解析环节)被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕。
7.代码演示
/** * * 返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、 * lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、 * 实例初始化方法、类和接口的初始化方法使用。 */ public class ReturnAddressTest { public boolean methodBoolean() { return false; } public byte methodByte() { return 0; } public short methodShort() { return 0; } public char methodChar() { return ‘a‘; } public int methodInt() { return 0; } public long methodLong() { return 0L; } public float methodFloat() { return 0.0f; } public double methodDouble() { return 0.0; } public String methodString() { return null; } public Date methodDate() { return null; } public void methodVoid() { } static { int i = 10; } public void method2() { methodVoid(); try { method1(); } catch (IOException e) { e.printStackTrace(); } } public void method1() throws IOException { FileReader fis = new FileReader("atguigu.txt"); char[] cBuffer = new char[1024]; int len; while ((len = fis.read(cBuffer)) != -1) { String str = new String(cBuffer, 0, len); System.out.println(str); } fis.close(); } }
栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。(很多资料都忽略了附加信息)
递归调用等,通过-Xss设置栈的大小;
不能,如递归无限次数肯定会溢出,调整栈大小只能保证溢出的时间晚一些
越大是会延长了StackOverflowError发生的时间,但是会挤占其他线程的空间
内存区块
|
Error |
GC |
程序计数器 |
X |
X |
本地方法栈 |
√ |
X |
JVM虚拟机栈 |
√ |
X |
堆 |
√ |
√ |
方法区 |
√ |
√
|
5 方法中定义的局部变量是否线程安全?
/** * 何为线程安全? * 如果只有一个线程可以操作此数据,则是线程安全的。 * 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题 * * StringBuffer是线程安全的,StringBuilder不是 */ public class StringBuilderTest { //s1的声明方式是线程安全的 public static void method1(){ //StringBuilder线程不安全 StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); } //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); } }
原文:https://www.cnblogs.com/qiu-hua/p/13226542.html