JVM 3-线程 程序计数器&虚拟机栈

发布于:2023-01-24 ⋅ 阅读:(16) ⋅ 点赞:(0) ⋅ 评论:(0)

目录

运行时数据区Runtime Data Area

线程的内存空间

PC寄存器:程序计数器 

虚拟机栈

栈的运行原理: 

 栈帧的内部结构

栈帧-局部变量表:

栈帧-操作数栈: Operand Stack

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

栈帧-方法返回地址:

一些面试题


运行时数据区Runtime Data Area

一个进程对应一个JVM的实例,一个JVM实例中只有一个运行时数据区

其中的红色部分为这个进程下所有线程共享的方法区和堆,一个Runtime实例只有一个方法区和堆

灰色部分为每个线程独有的,也就是说有几个线程就有几份灰色部分区域


线程的内存空间

灰色的为单独线程私有的,红色的为多个线程共享的:

1. 线程独有:独立包括程序计数器、虚拟机栈、本地方法栈

2. 线程间共享:堆、方法区

如果一个进程中有5个线程,意味着有5组程序计数器、本地方法栈NMS、虚拟机栈VMS,这5个线程公用方法区和堆。

JVM 线程

1. 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行

2. 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射

3. 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收

4. 操作系统负责将线程安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法


PC寄存器:程序计数器 

 1.它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。

2.JVM规范中,每个线程都有自己的程序计数器,生命周期与线程一致

3.任何时间一个线程都只有一个方法在执行。程序计数器就是存储这个Java方法的JVM指令地址,如果是本地方法(native)则是未指定值(undefnded)。

4.它是程序控制流的指示器。分支,循环,跳转,异常处理,线程恢复等基础功能都要依赖这个计数器完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

5、它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域。

PC寄存器的作用:

存储指向下一条指令的地址,也就是即将要执行的指令代码的地址。由执行引擎读取下一条指令并执行。类似于遍历数据库结果集的游标,不停的指向下一条。

PC寄存器为什么被设定为线程私有的?

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,或者时间片轮转调度,CPU都会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?

为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。


虚拟机栈

这是线程私有的另一个区域。

内存中的栈与堆

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

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。

堆解决的是数据存储的问题,即数据怎么放,放哪里

1.Java虚拟机栈是什么?

Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,栈是线程私有的

2.虚拟机栈的生命周期

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

3.虚拟机栈的作用

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

局部变量,它是相比于成员变量来说的(或属性)

栈的特点

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

JVM直接对Java栈的操作只有两个:

  1. 每个方法执行,伴随着进栈(入栈、压栈)
  2. 方法执行结束后的出栈工作

对于栈来说不存在垃圾回收问题(栈存在溢出的情况)

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

Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。

如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量(压栈过多),Java虚拟机将会抛出一个StackoverflowError栈溢出 异常。

创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutofMemoryError内存溢出 异常。

设置栈内存的大小

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

-Xss1024m           // 栈内存为 1024MBS

-Xss1024k               // 栈内存为 1024KB


栈的运行原理: 

栈存储什么?

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在

在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈的运行原理 (*可以参考递归的dbug过程感受压栈)

1、JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出(后进先出)原则

2、在一条活动线程中,一个时间点上,只会有一个活动的栈帧。

     1、即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的这个栈帧被称为当前栈帧(Current Frame)

     2、与当前栈帧相对应的方法就是当前方法(Current Method)

     3、定义这个方法的类就是当前类(Current Class)

3、执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

4、如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧(压栈)。

5、不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。

6、如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧(出栈)。

7、Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常,但不管使用哪种方式,都会导致栈帧被弹出(出栈)。


 栈帧的内部结构

前边说了:栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

其实这些信息宝包括:

1、局部变量表(Local Variables)

2、操作数栈(Operand Stack)(或表达式栈)

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

4、方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

5、一些附加信息

栈帧的大小主要由局部变量表 和 操作数栈决定的

 


栈帧-局部变量表:

1、局部变量表:Local Variables,被称之为局部变量数组或本地变量表

2、定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。

3、由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

4、局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。

5、方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。

对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。

进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

6、局部变量表中的变量只在当前方法调用中有效。

在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。

当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

在编译期间,局部变量的个数、每个局部变量的大小都已经被记录下来

所以局部变量表所需的容量大小是在编译期确定下来的

静态变量与局部变量的对比:

按照在类中声明的方式分:

1、成员变量:

     1、类变量:

                linking的prepare阶段:给类变量默认赋值

                initial阶段:给类变量显式赋值即静态代码块赋值

     2、实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值

2、局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过

 

 静态方法不能调用实例变量和方法,所以没有this


栈帧-操作数栈: Operand Stack

1、每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)

2、操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)

主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

int num1 = 15;

int num2 = 8;

int result = num1 + num2;


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

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

2、比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

为什么要用常量池呢?

常量池的作用,就是为了提供一些符号和常量,便于指令的识别。

因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,然后记录其引用即可,节省了空间

方法的调用

在JVM中将符号引用转化为直接引用与方法的绑定机制相关。

静态链接  (类加载器中的 链接(解析))

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变。这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接。

动态链接:

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种转换过程具备动态性,因此就被称为动态链接。


栈帧-方法返回地址:

存放调用该方法的pc寄存器的值,方法结束之后要回调调用该方法的位置继续往下执行。

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


一些面试题

1、举例栈溢出的情况?(StackOverflowError)

通过 -Xss 设置栈的大小

2、调整栈大小,就能保证不出现溢出么?

不能保证不溢出

3、分配的栈内存越大越好么?

不是,一定程度减少了内存溢出的概率,但是会挤占其它的空间,因为整个虚拟机的内存空间是有限的,会出现OOM

4、垃圾回收是否涉及到虚拟机栈?

不会。Java虚拟机栈只有入栈和出栈,销毁就是出栈

5、方法中定义的局部变量是否线程安全?

何为线程安全?

(1)如果只有一个线程才可以操作此数据,则必是线程安全的。

(2)如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。

具体问题具体分析:

如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。

public class StringBuilderTest {
    int num = 10;
 
    //s1的声明方式是线程安全的,方法调用压入栈帧,只在这个线程访问
    //定义的全局变量int num = 10;可能被多个线程访问就是线程不安全的
    public static void method1(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        //...
    }
    //sBuilder的操作过程:是线程不安全的,sBuilder从外面传递过来,可能被多个线程调用
    public static void method2(StringBuilder sBuilder){
        sBuilder.append("a");
        sBuilder.append("b");
        //...
    }
    //s1的操作:是线程不安全的
    public static StringBuilder method3(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1;
    }
    //s1的操作:是线程安全的
    // s1.toString() 里面new 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(new Runnable() {
            @Override
            public void run() {
                s.append("a");
                s.append("b");
            }
        }).start();
 
        method2(s);
    }
}

6、运行时数据区,哪些部分存在Error和GC?