JVM Runtime Data Area——运行时数据区

JVM Runtime Data Area——运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域各有各自的用途,以及创建和销毁时间,有的区域随着虚拟机在进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

在这里插入图片描述

一、程序计数器(重点)

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器

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

CPU是多线程切换的执行机制,为了保证线程切换后能恢复到正确的执行位置,每个线程都需要独立拥有一个程序计数器,各个线程间的计数器互不影响,独立存储,我们称这类内存是“线程私有的内存”

虚拟机的运行,类似于这样的循环:

while( not end ) {

​ 取PC中的位置,找到对应位置的指令;

​ 执行该指令;

​ PC ++;

}

二、Java虚拟机栈——JVM Stack(重点)

1、程序基于栈的执行过程

Java Virtual Machine Stack——线程私有,生命周期与线程相同

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法出口等信息

每个线程对应着一个JVM Stack,线程中的每一个方法对应着一个 Stack Frame。

方法从被调用到执行完毕的过程,就对应着一个个栈帧在虚拟机栈(JVM Stack)中从入栈到出栈的过程

程序的运行其实就是方法的不断执行,方法的执行、调用其实就是不断进行栈帧的压栈、出栈

2、栈帧——Stack Frame

JVM以方法作为最基本的执行单元,”栈帧“则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行数据区中的虚拟机栈的栈元素。简单来说就是——每个method对应一个栈帧

栈帧可以分为如下四个部分:

1、Local Variable Table(重要)

Local Variable Table——局部变量表,相当于寄存器

我们平常讲”堆“、”栈“,”栈“通常就是指虚拟机栈,或者说更多情况下就是指虚拟机栈中的局部变量表部分

局部变量表存放了8种数据类型boolean、byte、char、short、int、float、reference类型(对象引用)和returnAddres(指向了一条字节码指令的位置)

前6种很好理解,这里需要重点讲解第7种——reference类型

reference类型表示对一个对象实例的引用,JVM可以通过这个引用做到两件事:

​ 1、根据引用直接或间接的查找到对象在Heap中的数据存放的起始位置或索引

​ 2、根据引用直接或间接的查找到对象所属数据类型在方法区中的存储的类型信息

总结:局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,当方法被调用后,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递

2、Operand Stack

Operand Stack——操作数栈

当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和读取内容,也就是入栈和出栈操作

例如整数相加的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入两个int类型的数值,当执行这个指令时,会把两个int值出栈并相加,然后讲相加的结果再入栈

操作数栈中元素的类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器必须要严格保证这一点,在类校验阶段还要再次验证

比如上面的iadd指令,如果操作数栈顶两个元素不全是int类型,有一个为long类型,就不能相加,会出错

总结:Java虚拟机的解释执行引擎被称为 “ 基于栈的执行引擎 ”,里面的 “ 栈 ” 就是操作数栈

3、Dynamic Linking

Dynamic Linking——动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接

我们已经知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接

参考:《深入理解Java虚拟机》第8章

4、return address

return address——方法返回地址

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

1、正常调用完成

​ 也就是正常结束,执行引擎遇到表示方法返回的字节码指令,是否有返回值根据具体方法来决定

2、异常调用完成

​ 也就是执行过程中遇到了异常,并且异常没有在方法体内得到妥善处理,就会导致方法退出

无论采用何种方式退出,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续运行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态

a() -> b():方法a调用了方法b,,b方法的返回值放在什么地方,以及回到a方法之后应该从哪里继续执行

在这里插入图片描述

每一个方法对应一个栈帧,方法的执行对应着栈帧的入栈,方法的退出对应着栈帧的出栈

每个方法执行的时候,各种参数、变量都存放在局部变量表中,执行的字节码都存放在操作数栈

在这里插入图片描述

在这里插入图片描述

三、本地方法栈

Native Method Stack——本地方法栈

虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

因为是JVM自动管理,我们很难人为干预,不是重点,了解即可。

四、Java堆——Heap(重点)

在这里插入图片描述

对于Java应用程序来说,Java堆(Heap)是虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是为了存放对象实例,在Java中“几乎”所有的对象实例都在这里分配内存

Java堆是垃圾收集器管理(GC)的内存区域,因此也被称为“GC堆”。从回收内存的角度看,由于业界绝对主流的Hotspot虚拟机垃圾收集器全部都是基于分代收集理论,所以 ** Java堆经常被表述分为“新生代”、“老年代”、“Eden区” **等名词

由于Heap是所有线程共享的,当高并发情况下,可能有多个线程同时想要使用同一内存块,造成线程争用,效率变低。所以从分配内存的角度看,所有线程共享的Heap内存(更细节来说其实是Heap中的Eden区)中可以划分出每个线程私有的分配缓冲区(Thread Local Allocation Buffer,简称TLAB),减少线程争用,以提升对象分配时的效率

无论如何划分,都不会改变Java堆中存储内容的共性——无论哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了GC(垃圾收集器)更好的回收内存,以及更快的分配内存

对象的分配:

尝试栈上分配–》TLAB分配–》堆上分配(新生代、老年代)

五、方法区——Method Area(重点)

1、方法区概念

Method Area——方法区,和Java堆一样被所有线程共享。它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码缓存等数据

方法区是一种逻辑上的概念,从时间轴来划分,先后有两种具体实现:

1、JDK1.8之前,方法区实现为Perm Space,即永久区

比较膈面试官有可能会问到的问题

  • 字符串常量位于PermSpace
  • FGC不会清理
  • 大小启动的时候指定,运行之后不能改变,经常会产生内存溢出线程

2、JDK1.8之后,方法区实现的名称为Meta Space,即元数据区

  • 字符串常量位于堆,不再放在Method Space
  • 会触发FGC清理
  • 如果不设定的话,最大就是物理内存,也可以指定大小,满了之后就会触发FGC

2、运行时常量池

Runtime Constant Pool——运行时常量池,是方法区的一部分

我们在前面的学习中知道Class文件结构中有一项信息是常量池表(存放编译器生成的字面量和符号引用(静态概念)),在运行之后(动态概念),这部分内容在类加载完成之后存放到方法区的运行时常量池中。

运行时常量池除了保存Class文件中描述的符号引用外,符号引用翻译出来的直接引用也存储在运行时常量池中

七、直接内存——Direct Memory

直接内存并不是虚拟机运行时数据区的一部分,但是也算是JVM管理的内存,而且这部分内存被频繁的使用,也有可能导致OOM,与运行时数据区关系紧密,所以放在这里一起讲解

为了增加IO效率,在JDK1.4之后增加了直接内存(Direct Memory),实现了从JVM内部能够直接访问操作系统管理的内存,即用户空间能够直接访问内核空间

JDK1.4之后引入了NIO类,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆(Heap)中的DirectByteBuffer对象作为这块内存的引用进行操作。这样避免了在Java堆(Heap)和Native堆中来回复制数据(即零拷贝)
在这里插入图片描述


补充、常用指令

在这里插入图片描述

bipush:把byte类型扩展成int类型,然后push到操作数栈中(b(byte)i(int)push),即把基本数据类型压栈

istore_n:把操作数栈顶的元素出栈,然后赋值给局部变量表中下标值为n的变量

iload_n:把局部变量表为n位置的变量值放到操作数栈顶,即压栈

每个字节码指令的详细信息参考官方文档

一个程序翻译成字节码指令集的详细执行过程参考:《深入理解Java虚拟机》P330

猜你喜欢

转载自blog.csdn.net/qq_42583242/article/details/107827175
今日推荐