我的JVM学习笔记:第三章——运行时数据区(1)

我的JVM学习笔记:第三章——运行时数据区(1)

感谢尚硅谷宋红康老师的JVM入门到精通课程,向每一个用心做免费教课程的老师致敬!
本套教程均为我学习课程之后的学习笔记,防止遗忘,并发送给大家分享,感谢大家查看~

本章包含知识点:运行输数据区概述,程序计数器详解,虚拟机栈概述,虚拟机栈的执行过程,虚拟机栈中的StackOverFlowError和OutOfMemoryError,以及如何设置虚拟机栈的大小!

1.运行时数据区结构

注意:不同JVM对内存管理有着不同的机制,本笔记只讨论Hotspot虚拟机的内存结构!
先看看官方规范的运行时数据区结构:
运行时数据区结构
运行时数据区主要由以下几部分构成:

  • 方法区:类加载后的信息会储存在方法区
  • 堆:负责存放对象数据
  • 程序计数器:用于记录代码运行的位置
  • 本地方法栈:与虚拟机栈类似,负责执行其他语言所编写的方法(比如调用C语言方法)
  • 虚拟机栈:一个栈贞对应一个方法,虚拟机栈就描述了方法执行时对应的JVM的内存模型(方法被执行引擎在虚拟机栈中执行)。

详细图例:
JVM运行时数据区
注意:JDK8后HotSpot虚拟机使用元空间代替了方法区,两者作用和结构基本相同!

JVM中的多线程处理机制:
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
Java线程与本地线程之间的关系:
在HotspotJVM里,每个线程都与操作系统的本地线程直接映射。当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run ()方法。当最后一个非守护线程结束,JVM也将运行结束。
运行时数据区的结构与现成的关系
如第一张图所示:
红色区域(堆区,方法区):线程间共享,一个JVM进程中只存在一份!
灰色区域(程序计数器,本地方法栈,虚拟机栈):为线程独立,也就是说,加入一个进程中有5个线程,则分别有5分程序计数器,本地方法栈,虚拟机栈。并随着线程的创建而创建,随着线程的销毁而销毁!

总结来说:
JVM的运行时数据区共有5部分组成,分别为:堆区、方法区、程序计数器、本地方法栈、虚拟机栈!
其中:堆区,方法区为线程间共享,而程序计数器,本地方法栈,虚拟机栈为线程独享!

2. 程序计数器

程序计数寄存器(Program Counter Register)简称程序计数器,PC寄存器。程序计数寄存器并不是硬件意义上的寄存器,而是对于物理寄存器的抽象(可以理解为程序钩子/行号指示器),他是整个程序中速度最快的内存区域,用于记录Java程序在当前线程运行的下一条指令的地址(下一行要执行代码的行号),执行引擎可以由此读取下一条指令执行。

总结来说:
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

注意:

  • 每个线程都有独立的程序计数器并线程私有,生命周期与线程一致!
  • 如果当前执行的是Native方法,则程序寄存器不起作用,值始终为undefined!
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 它是唯一一 个在Java虚拟机规范中没有规定任何OutOtMemoryError情况的区域。

问题:为什么要使用程序计数器来记录行号?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

问题2:程序计数器为什么要设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

举例:字节码中查看程序行号

public class TestPCRegister {
    
    
    public static void main(String[] args) {
    
    
        int a = 10;
        int b = 20;
        int c = a + b;
        System.out.println(c);
    }
}

反编译命令:

javap –v 文件名

这时我们就可以发现:字节码文件中的每一行代码前都有一个位置对应的行号,这些行号就是程序计数器中所存储的内容,当一行执行完毕时,执行引擎就会将下一行代码行号写入程序计数器,这样即使CPU时间片被抢夺,也能在继续开始执行时确定即将执行的代码位置,保证程序不会出错!
反编译结果

3. 虚拟机栈概述

Java虚拟机栈是什么:
Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用。虚拟机栈是线程私有的,生命周期和线程一致。
虚拟机栈主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

堆和栈的特点:
栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

虚拟机栈的优缺点:
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

栈帧简介:
栈贞是虚拟机栈中的基本单位,每一个方法都对应着一个栈帧,而JVM执行方法就是将一个栈贞进行入栈操作,然后由执行引擎进行执行,执行完成后,此栈帧将执行出栈操作

即:每一个方法都对应着一个栈帧,方法执行会对栈帧进行入栈操作,结束执行会对栈帧进行出栈操作!

虚拟机栈的执行过程:
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
也就是说:当JVM执行某一方法时,会先将该方法对应的栈帧压入虚拟机栈,依次执行期中的代码,当方法中调用了其他方法时==,会将调用的方法对应的栈帧压入虚拟机栈==,然后率先执行最后压入虚拟机栈的方法,当最上层方法执行完毕,会进行出栈操作,然后继续执行他的下一层方法!
最上层的方法叫“当前栈帧”,与之对应的方法就是“当前方法”,定义方法的类就叫“当前类”!
实战演示:

public class TestStack {
    
    

    public static void main(String[] args) {
    
    
        System.out.println("main方法开始执行");
        method1();
        System.out.println("main方法执行结束");
    }

    public static void method1()
    {
    
    
        System.out.println("method1方法开始执行");
        method2();
        System.out.println("method1方法执行结束");
    }

    public static void method2()
    {
    
    
        System.out.println("method2方法开始执行");
        method3();
        System.out.println("method2方法执行结束");
    }

    public static void method3()
    {
    
    
        System.out.println("method3方法开始执行");
        System.out.println("method3方法执行结束");
    }
}

执行结果:

执行结果

此时我们对程序进行Debug:
Debug结果
此时,我们定义了四个方法,每个方法对应一个栈帧,main方法开始执行时,main方法对应的栈帧就会被压入栈,这时调用了method1方法,这时虚拟机就会将method2方法压入栈,而main方法此时并没有执行完毕,所以main方法不会出栈,而是排在method1方法之后,此时,method1即为当前栈帧,程序会继续执行method1的代码,而method1又调用了method2………以此类推,当执行到method3时,虚拟机会将method3压入栈,而method1,method2,main方法并没有执行结束,所以此时虚拟机栈中有四个栈帧,当method3执行完毕后,method3将会进行出栈操作,此时虚拟机栈中只剩下了3个栈帧,method2为当前栈帧………以此类推,当method1方法执行结束后,虚拟机栈只剩下了main方法的栈帧,执行引擎会依次执行main方法剩下的代码!
详细描述
注意:

  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用
    另外一个线程的栈帧。
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果
    给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
    Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另
    外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

设置栈的大小与异常处理 :
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虛拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverFlowError异常。
如果Java虛拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError 异常。

也就是说:
因为没一个栈贞都是有大小的,栈帧的大小与方法的代码有关!
如果栈的大小不变,那么如果同时执行了太多方法,虚拟机栈空间就会被栈帧充满,这时如果继续调用方法,栈帧加入,就会导致栈的溢出,导致StackOverFlowError。
如果栈的大小动态变化,这时如果栈被填满,JVM会自动扩充栈的空间,但如果此时内存不足,就会导致内存溢出,就会报OutOfMemoryError!

举例来说:
如果栈大小固定,那么最简单引起StackOverFlowError的方法就是方法递归调用,每一次调用都会将一个新的栈帧压入栈!当递归次数太多,栈帧充满虚拟机栈,就会抛出异常!

	public class TestStack {
    
    
    public static int i=0;
    public static void main(String[] args) {
    
    
        System.out.println(i++);
       main(args);
    }
}

输出结果:
输出结果
此时我们看到,我们因为错误的逻辑,导致了程序递归,栈帧充满了虚拟机栈,最终在第9773次栈帧入栈时虚拟机栈再也无法容纳新的栈帧,抛出异常!

设置虚拟机栈大小:
可以看到,我们前方因为逻辑错误导致异常的发生,那么如果程序运行时必须同时执行很多方法,那我们就必须设置虚拟机栈的大小以避免异常的发生!

设置虚拟机栈大小指令:

-Xss *k

示例:将上方程序设置不同的虚拟机栈
设置栈的大小
结果
此时:我们将虚拟机栈设置为108k(最小要求),发现此时只能同时执行978个方法!

设置虚拟机栈的大小
结果

而将虚拟机栈大小设置为4096k,就可以执行更多的方法!

猜你喜欢

转载自blog.csdn.net/qq_42628989/article/details/104360779