JVM(三)运行时数据区概述及线程

运行时数据区概述及线程

简介

内存是硬盘和CPU的中间仓库和桥梁,承载着操作系统应用程序的实时运行。JVM的内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对内存的划分方式管理机制存在着部分差异。

如下图,主要流程是类加载子系统加载字节码文件到内存相应区域,然后执行引擎对字节码文件进行解释执行,将字节码翻译为机器指令,同时也能够调用本地方法接口和本地方法库来调用C语言的方法,并存储数据到本地方法栈里面。

image-20221215100410037

可以看到上面的五种内存区域分成了两种颜色,灰色是线程独有的,红色是多个线程共享的(也就是进程的,进而也就是虚拟机实例的资源)。所以红色部分会随着虚拟机的创建而创建,随着虚拟机的退出而销毁。灰色部分则是与线程一一对应,这些线程对应的数据区域会随着线程的开始和结束创建和销毁。

即每个线程独立包括:程序计数器、本地方法栈、虚拟机栈

线程间共享:堆、堆外内存(永久代或元空间:即方法区的实现、代码缓存)

image-20221215101135300
线程间共享的说明

一个JVM实例也就是一个JVM虚拟机对应着一个Runtime对象,而Runtime对象就相当于上面的一个运行时数据区,可以通过getRuntime()方法获取。

JVM中的线程说明
  • 线程是一个程序的运行单元,JVM允许一个应用有多个线程并行地执行

  • 在HotSpot JVM里面,每个Java线程都和操作系统的本地线程直接映射。

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

  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,就会调用Java线程中的run方法

  • 线程分为守护线程和普通线程,如果只剩下守护线程则本地线程会终止虚拟机

HotSpot JVM后台线程主要有下面几类:

image-20221215144907419

1 程序寄存器 Program Counter Register

JVM的程序计数寄存器命名起源于CPU的寄存器,用于存储指令相关的现场信息,CPU只有把数据装载到寄存器才能运行。JVM程序寄存器是对物理寄存器的一种抽象模拟,也称为是程序钩子,作用就是判断执行完上一行代码下一行该执行哪一行。

image-20221215153307081

作用:PC寄存器用来存储指向下一条指令的地址,也即是即将要执行的指令的代码,然后由执行引擎读取下一条指令。

  • 程序寄存器是内存中的一块很小的区域,几乎可以忽略不计,也是运行速度最快的区域

  • 在JVM规范中,程序寄存器是线程私有的,所以每个线程都有自己的程序寄存器,程序寄存器的生命周期和线程的生命周期一致

  • 任何一个时间一个线程都只会有一个方法在运行,这个方法即当前方法,程序寄存器会存储本地方法对应的JVM指令地址,当然如果这个方法是native的,即C语言的代码,就会不存储地址而是undefined

  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个指示器来完成

  • 字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

  • 它是唯一一个Java虚拟机规范中没有规定任何OOM情况的区域

GC:垃圾回收机制主要是针对于堆区和方法区

OOM:即内存溢出,栈区、方法区和堆区都会发生,程序寄存器则不会

写一段代码然后对class文件执行反编译:javap -verbose xxx.class

    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;

        String s = "abc";
        System.out.println(i);
        System.out.println(k);
    }
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: ldc           #2                  // String abc
        12: astore        4
        14: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: iload_1
        18: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        21: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_3
        25: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        28: return

反编译结果前面的数字即是程序寄存器存储的指令地址(偏移地址)执行引擎会从PC寄存器会根据地址取出后面是具体的操作指令,然后操作引擎使用操作局部变量表操作栈结构实现存取相加等等的操作,并将这些操作翻译成机器指令,交给CPU做具体的运算。

image-20221215162531307
为什么使用PC寄存器记录字节码指令地址?(为什么使用PC寄存器记录当前线程的执行地址)

因为CPU需要不停切换各个线程,所以切换回线程的时候需要知道从哪里开始继续执行。JVM的字节码解释器就需要改变程序寄存器的值来明确下一条执行那一条字节码指令。

image-20221215163832736
为什么程序计数器被设计成线程私有的

由于CPU时间片轮换的限制,多线程并发执行过程中,任何一个确定时刻,一个处理器或多核处理器的一个内核,只会执行某个线程中的一条指令。这样必然会导致线程经常中断或恢复,所以每个线程在创建之后都会产生自己的程序寄存器和栈帧,准确记录各个线程正在执行的字节码指令地址,保证线程在终端恢复后的正常执行。

CPU时间片即CPU分配给程序运行的时间

posted @ 2023-05-17 18:55  Tod4  阅读(47)  评论(0编辑  收藏  举报