Java线程运行原理详解

Java
276
0
0
2023-05-31
标签   Java多线程

一、概念介绍

  • 方法区 :是虚拟机中一块 线程 共享的内存区域,用于存储类信息、常量池、静态变量、编译后的字节码等。
  • :是虚拟机中最大的一块线程共享的内存区域,堆是 java 内存管理的核心区域,所有的对象实例和数组都在堆中分配内存。
  • 虚拟机栈 :是线程私有的内存区域。虚拟机栈的内存空间是给线程使用的,每启动一个线程,虚拟机都为其分配一块栈内存空间,虚拟机栈中可以存在多个栈帧。
  • 栈帧 :每个线程分配的虚拟机栈内存区域由多个栈帧(Frame)组成,栈帧对应着每个方法调用时所占用的内存;每个栈帧是由局部变量表、操作数栈、动态链接、方法返回值地址等组成。
  • 程序计数器 :是一块内存很小的线程私有的内存空间,每个线程都有自己的程序计数器。任何时间一个线程都只有一个方法在执行,程序计数器会记录当前执行方法中的 JVM 指令地址,用于控制程序的正确执行。程序的分支、跳转、循环、异常以及线程切换都需要依靠程序计数器来完成。

二、栈与栈帧关系

Java Virtual Machine Stacks ( Java 虚拟机 栈)

JVM 中由堆、栈、方法区所组成,其中栈内存是给线程使用的,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
  • 每个栈帧是由局部变量表、操作数栈、动态链接、方法返回值地址等组成。

虚拟机栈与栈帧的关系如下:

三、栈帧 Debug

3.1 代码示例

 /**
 * Created by lilinchao
 * Date 2022/10/6
 * Description 方法调用时栈帧变化
 */public class TestFrames {
    public  static   void  main(String[] args) {
        method1(10);
    }

     private  static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }
} 

3.2 DEBUG 步骤详解

(1)在 method1(10); 方法上打断点,然后通过Debug方式运行程序

Java线程运行原理详解

  • Frames中包函所有的栈帧
  • Variables中是每个栈帧中对应的局部变量

刚启动程序,只有一个main线程,main 方法的参数是一个String 数组,参数名称为 args ,此时 Variables 变量表中有一个 args={String[0]@620},数组对象的大小为 0。

(2)通过 Step into (快捷键F7)进入到 静态方法 method1(10)

Java线程运行原理详解

此时线程栈栈帧表 Frames 中有两个栈帧, method1 的栈帧中有一个局部变量 x=10 , 局部变量 初始值是从main方法中传递而来。

(3)在继续 Step into 执行代码 int y = x + 1;

Java线程运行原理详解

此时在栈帧 method1 中增加一个局部变量y,并被赋值为11

(4)在继续 Step into ,程序进入到 method2() 方法

Java线程运行原理详解

此时Frames中增加一个栈帧 method2 , method2 的栈帧中有一个局部变量 n,该局部变量是在 method2 中实例化的 Object 对象。

从这我们就可以看出,程序每调用一个方法,就会在栈帧表中多增加一个相应的栈帧

(5) Step into 继续向下执行

Java线程运行原理详解

method1 中调用 method2 方法结束,此时 Frames 中只有 method1 和 main 方法两个栈帧, method2 方法由于运行结束方法返回后,就会弹栈(出栈)。并且 method2 方法将自身创建的Object对象作为参数返回给 method1 方法中的局部变量m。

(6)接下来,程序执行 System.out.println(m); 在控制台输出相应信息

(7)继续 Step into method1 方法调用结束

Java线程运行原理详解

此时Frames 中只有main方法一个栈帧, method1 方法由于运行结束,就会弹栈(出栈)

(8)最后main方法运行结束出栈,程序运行结束

3.3 总结

从上面通过一步步Debug查看代码执行过程可以得出以下两个结论:

  • 程序每调用一个方法,就会在栈帧表(Frames )中增加一个相应的栈帧;
  • 当方法运行结束方法返回后,就会弹栈(出栈)。

四、栈帧图解

由与Debug的局限性,还是不能够深入的理解整个线程的运行流程,以及内部栈帧的组成,接下来,通过图解的方式,将上方示例代码运行过程在进行详细说明,方便对线程的运行流程进行理解。

(1)类加载过程

Java线程运行原理详解

  • 执行类加载,将 TestFrames 类中的字节码加载到 Java 的虚拟机 方法区 的内存当中;
  • 为了便于理解,上图的方法区当中没有放入java类的 字节码 ,而是放入了java的源代码;

(2)运行主线程

当类加载完成之后, cpu 会开始运行主线程当中的代码

  • 最开始执行 main 函数 ,此时虚拟机栈中会为 main 线程分配一块栈内存供 main 线程运行(main 线程栈), main 线程栈中会压入一个 main 函数栈帧;
  • main 函数拥有一个 String [] args 局部变量,因此局部变量表中 args 指向一个堆中的 String 数组(局部变量表会在方法运行之前就创建完成,分配好内存);
  • main函数 的返回地址就是程序退出地址,当程序运行结束会自动退出。

(3)method1方法调用

  • 主函数中调用 method1 方法,此时在main线程栈中会压入一个 method1 函数栈帧;
  • method1 函数中有三个局部变量,分别是 x、y、m, x 的值由方法传递已知,因此 x=10;
  • 向下执行程序 int y = x + 1; ,此时会将该行代码加载到程序计数器,局部变量y会被赋值为11;

(4)method2方法调用

  • 执行程序 Object m = method2(); ,此时调用 method2 方法,将 method2 函数栈帧压入main线程栈当中;
  • 程序向下执行 method2 方法中的代码 Object n = new Object(); ,将该行代码加载到程序计数器;
  • 此时在堆内存中创建了一个 Object 对象,并且将对象地址赋值给 n 引用。

(5)执行method2中的return方法

  • 程序计数器加载程序 return n; 将Object对象的引用地址传递给m,此时m的引用地址指向堆中的Object对象;
  • method2 调用结束,因此 method2 函数栈帧将会从main线程栈中出栈;
  • 程序重新返回到 method1 方法m的位置;

(6)method1方法执行结束

  • 当打印方法执行结束,此时method1中的方法也执行完毕, method1 函数栈帧从main线程栈中出栈;
  • 线程来到主函数。

(7)主函数执行结束,出栈,程序执行完毕

五、 多线程 执行逻辑

5.1 示例代码

 /**
 * Created by lilinchao
 * Date 2022/10/6
 * Description 多线程方法调用时栈帧变化
 */public class TestFrames {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                method1(20);
            }
        };
        t1.setName("t1");
        t1.start();
        method1(10);
    }

    private static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }
} 

5.2 执行步骤

(1)分别在主线程和 t1线程 method1 方法上打断点

(2)通过Debug模式启动程序

  • 在断点处鼠标右击,将程序的Debug模式由All切换成Thread,在点击Done确定修改

两个断点处都进行上述修改

(3)切换栈帧

  • main线程和t1线程两个线程的栈帧都是相互独立的,通过上图的下拉框可以切换到相应的栈帧进行Debug运行

六、线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、 yield 、wait、join、park、 synchronized 、 lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能