其他分享
首页 > 其他分享> > 第五章 - 虚拟机栈

第五章 - 虚拟机栈

作者:互联网

 不需要太用力表达什么,大家都有感受,不如沉默

1.虚拟机栈概述

虚拟机栈出现的背景

内存中的栈与堆

首先栈是运行时的单位,而堆是存储的单位

虚拟机栈的基本内容

Java虚拟机栈是什么?

Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用

声明周期

生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了。

作用

主管Java程序的运行,它保存方法的局部变量(8 种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

虚拟机栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

虚拟机栈的异常

面试题:栈中可能出现的异常
栈异常演示

递归调用

public class StackError {
   public static void main(String[] args) {
       main(args);
  }
}

设置栈内存大小

使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

public class StackError {
   private static int count = 1;
   public static void main(String[] args) {
       System.out.println(count);
       count++;
       main(args);
  }
}

设置

-Xss1024m       // 栈内存为 1024MBS
-Xss1024k // 栈内存为 1024KB

结果

没有设置栈内存大小的时候,程序可以递归10824次
当缩小栈内存大小,设置为256k时,程序递归1874次

2.栈的存储单位

栈中存储什么?

栈的运行原理

代码举例
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()执行结束...");
  }

   public int method2() {
       System.out.println("method2()开始执行...");
       int i = 10;
       int m = (int)method3();
       System.out.println("method2()即将结束...");
       return i + m;
  }

   public double method3() {
       System.out.println("method3()开始执行...");
       double j = 20.0;
       System.out.println("method3()即将结束...");
       return j;
  }
}
method1()开始执行...
method2()开始执行...
method3()开始执行...
method3()即将结束...
method2()即将结束...
method1()执行结束...

栈帧的内部结构

每个栈帧中存储着:

每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表操作数栈决定的

3.局部变量表

局部变量表介绍

局部变量表所需的容量大小是在编译期确定下来的
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 = "baidu.com";
       String info = test2(date, name1);
       System.out.println(date + name1);
  }

   public String test2(Date dateP, String name2) {
       dateP = null;
       name2 = "xiexu";
       double weight = 185.5;//占据两个slot
       char gender = '男';
       return dateP + name2;
  }

}

反编译后,可得结论:

JClassLib参数详解

关于 Slot 的理解

在构造器以及实例方法中,对象引用this 都会存放在索引为0的位置

//构造器
public LocalVariablesTest() {
   this.count = 1;
}

//实例方法
public void test1() {
   Date date = new Date();
   String name1 = "baidu.com";
   test2(date, name1);
   System.out.println(date + name1);
}

64位的类型(long和double)占用两个slot

public String test2(Date dateP, String name2) {
       dateP = null;
       name2 = "lijiatu";
       double weight = 185.5; //占据两个slot
       char gender = '男';
       return dateP + name2;
  }

static方法无法调用this

public static void testStatic() {
       LocalVariablesTest test = new LocalVariablesTest();
       Date date = new Date();
       int count = 10;
       System.out.println(count);
       //因为this变量不存在于该静态方法的局部变量表中!!!
//       System.out.println(this.count);
  }

Slot 的重复利用

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量出了其作用域,那么在其作用域之后声明新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

public void test4() {
       int a = 0;
      {
           int b = 0;
           b = a + 1;
      }
       //变量c使用 之前已经销毁的变量b占据的slot的位置
       int c = a + 1;
  }

局部变量c 重用了 局部变量b 的slot位置

4.操作数栈(Operand Stack)

操作数栈的特点

操作数栈的作用

代码追踪

public void testAddOperation() {
       //byte、short、char、boolean:都以int型来保存
       byte i = 15;
       int j = 8;
       int k = i + j;
}
0 bipush 15
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return

程序执行流程

关于 int j =8; 的说明

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中。

public int getSum() {
       int m = 10;
       int n = 20;
       int k = m + n;
       return k;
  }

   public void testGetSum() {
       //获取上一个栈桢返回的结果,并保存在操作数栈中
       int i = getSum();
       int j = 10;
  }

6.栈顶缓存技术(Top Of Stack Cashing)

7.动态链接(Dynamic Linking)

动态链接(或指向运行时常量池的方法引用)

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。

public class DynamicLinkingTest {

   int num = 10;

   public void methodA(){
       System.out.println("methodA()....");
  }

   public void methodB(){
       System.out.println("methodB()....");
       methodA();
       num++;
  }

}
public void methodB();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=3, locals=1, args_size=1
        0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #6                  // String methodB()....
        5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: aload_0
        9: invokevirtual #7                  // Method methodA:()V
       12: aload_0
       13: dup
       14: getfield      #2                  // Field num:I
       17: iconst_1
       18: iadd
       19: putfield      #2                  // Field num:I
       22: return
     LineNumberTable:
       line 12: 0
       line 13: 8
       line 14: 12
       line 15: 22
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0      23     0  this   Lcn/sxt/java1/DynamicLinkingTest;
Constant pool:
  #1 = Methodref          #9.#23         // java/lang/Object."<init>":()V
  #2 = Fieldref           #8.#24         // cn/sxt/java1/DynamicLinkingTest.num:I
  #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
  #4 = String             #27            // methodA()....
  #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #6 = String             #30            // methodB()....
  #7 = Methodref          #8.#31         // cn/sxt/java1/DynamicLinkingTest.methodA:()V
  #8 = Class              #32            // cn/sxt/java1/DynamicLinkingTest
  #9 = Class              #33            // java/lang/Object
 #10 = Utf8               num
 #11 = Utf8               I
 #12 = Utf8               <init>
 #13 = Utf8               ()V
 #14 = Utf8               Code
 #15 = Utf8               LineNumberTable
 #16 = Utf8               LocalVariableTable
 #17 = Utf8               this
 #18 = Utf8               Lcn/sxt/java1/DynamicLinkingTest;
 #19 = Utf8               methodA
 #20 = Utf8               methodB
 #21 = Utf8               SourceFile
 #22 = Utf8               DynamicLinkingTest.java
 #23 = NameAndType        #12:#13        // "<init>":()V
 #24 = NameAndType        #10:#11        // num:I
 #25 = Class              #34            // java/lang/System
 #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
 #27 = Utf8               methodA()....
 #28 = Class              #37            // java/io/PrintStream
 #29 = NameAndType        #38:#39        // println:(Ljava/lang/String;)V
 #30 = Utf8               methodB()....
 #31 = NameAndType        #19:#13        // methodA:()V
 #32 = Utf8               cn/sxt/java1/DynamicLinkingTest
 #33 = Utf8               java/lang/Object
 #34 = Utf8               java/lang/System
 #35 = Utf8               out
 #36 = Utf8               Ljava/io/PrintStream;
 #37 = Utf8               java/io/PrintStream
 #38 = Utf8               println
 #39 = Utf8               (Ljava/lang/String;)V

为什么要用常量池呢?

8.方法的调用:解析和分派

静态链接与动态链接

JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

静态链接:

动态链接:

方法的绑定机制

静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding)晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

代码举例
/**
* 说明早期绑定和晚期绑定的例子
*/
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(); //表现为:晚期绑定
  }
}

虚方法和非虚方法

子类对象的多态性的使用前提
虚拟机中调用方法的指令
代码举例
/**
* 解析调用中非虚方法、虚方法的测试
*
* 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("baidu.com");

       //invokestatic 非虚方法
       super.showStatic("good!");

       //invokespecial 非虚方法
       showPrivate("hello!");

       //invokevirtual
       //虽然字节码指令中显示为invokevirtual,但因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
       showFinal();

       //invokespecial 非虚方法
       super.showCommon();

       //invokevirtual 虚方法
       //有可能子类会重写父类的showCommon()方法
       showCommon();
       
       //invokevirtual 虚方法
    //info()是普通方法,有可能被重写,所以是虚方法
       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();
}

关于 invokedynamic 指令

动态类型语言和静态类型语言
Java语言:String info = "mogu blog";           (Java是静态类型语言的,会先编译再进行类型检查)
JS语言:var name = "shkstart";  var name = 10; (运行时才进行检查)
Python语言:info = 130.5;  (动态类型语言)
/**
* 体会invokedynamic 指令
*/
@FunctionalInterface
interface Func {
   public boolean func(String str);
}

public class Lambda {
   public void lambda(Func func) {
       return;
  }

   public static void main(String[] args) {
       Lambda lambda = new Lambda();

       Func func = s -> {
           return true;
      };

       lambda.lambda(func);

       lambda.lambda(s -> {
           return true;
      });
  }
}

方法重写的本质

Java 语言中方法重写的本质

IllegalAccessError介绍

虚方法表

9.方法返回地址(return address)

方法退出的两种方式

当一个方法开始执行后,只有两种方式可以退出这个方法:

异常处理表

代码举例
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();
  }
}

10.一些附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。

11.栈的相关面试题

举例栈溢出的情况?(StackOverflowError)
调整栈大小,就能保证不出现溢出么?
分配的栈内存越大越好么?
垃圾回收是否涉及到虚拟机栈?
方法中定义的局部变量是否线程安全?

何为线程安全?

具体问题具体分析:

/**
* 面试题:
* 方法中定义的局部变量是否线程安全?具体情况具体分析
*
*   何为线程安全?
*     如果只有一个线程才可以操作此数据,则必是线程安全的。
*     如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
*/
public class StringBuilderTest {

   //s1的声明方式是线程安全的,因为s1只在方法内部操作,属于局部变量
   public static void method1(){
       //StringBuilder:线程不安全
       StringBuilder s1 = new StringBuilder();
       s1.append("a");
       s1.append("b");
       //...
  }

   //sBuilder通过参数传递方法内,存在线程不安全的问题
   public static void method2(StringBuilder sBuilder){
       sBuilder.append("a");
       sBuilder.append("b");
       //...
  }

   //操作s1之后,将s1作为返回值返回,这样可能被其他线程所调用,所以存在线程不安全的问题
   public static StringBuilder method3(){
       StringBuilder s1 = new StringBuilder();
       s1.append("a");
       s1.append("b");
       return s1;
  }

   //s1的操作:是线程安全的,因为String是线程安全的
   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);
  }
}
 

标签:调用,void,局部变量,public,第五章,线程,方法,虚拟机
来源: https://www.cnblogs.com/l12138h/p/16585313.html