JVM的内存分区

JVM的内存分区

(一)简述

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用户,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而创建和销毁。我们先来看一下JVM的内存分区(也被称为运行时数据区):

在这里插入图片描述

PS:JDK 1.8同JDK 1.7比,最大的差别就是:元数据区取代了永久代。元数据区的本质和永久代类似,都是对JVM规范中对方法区的实现。不过元数据区与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

下面放一张更详细的图,里面除了上面讲到的运行时数据区,还包括类加载器、字节码执行引擎等其他组件,方便了解更全面的细节:

在这里插入图片描述

(二)程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。在虚拟机的概念模型,字节码执行引擎工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

每条线程都有一个独立的程序计数器,为了线程切换后能恢复到正确的执行位置。java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪条指令。

(三)本地方法栈

本地方法栈是为JVM运行Native方法准备的空间,由于很多Native方法都是用C语言实现的,所以它通常又叫C栈。它与Java虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。

(四)java虚拟机栈

Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在这里插入图片描述

每一个栈帧都包括了局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。

一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。
在这里插入图片描述
由于Java虚拟机栈是与线程对应的,数据不是线程共享的,因此不用关心数据一致性问题,也不会存在同步锁的问题。

Java虚拟机栈规定的异常情况有两种:
1.线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2.如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。

1. 局部变量表

局部变量表是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在java编译成class文件的时候,就在方法的Code属性的max_locals数据项中确定该方法需要分配的最大局部变量表的容量。虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量(以变量槽为单位)。

局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放32位(4 字节)以内的数据类型( boolean、byte、char、short、int、float、reference和returnAddress八种)。对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。

Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值),也就是说不存在类变量那样的准备阶段。

2. 操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。

操作数栈的每一个元素可以是任意Java数据类型,32位及以内的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。

当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

3. 动态链接

每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

4. 方法出口

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

  1. 方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。

  2. 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。

无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

(五)堆

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。

java堆的唯一目的就是存放对象实例对象。几乎所有的对象实例都在这里分配。这一点在java虚拟机规范中这样描述:所有的对象实例以及数组都要在堆上分配,但随着JIT编译器的发展与逃逸分析技术的成熟,所有的对象都分配在堆上也变的不是那样“绝对”了。

java堆是垃圾收集器管理的主要区域,因此很多地方也称为“GC堆”,从内存回收角度看,由于现在收集器基本都采用分代收集算法,java堆中还可以细分:新生代、老年代。新生代再细分可分为Eden空间、From Survivor空间、To Survivor空间。

当前主流的虚拟机的堆空间都是按照可扩展来实现的,通过(-Xmx和-Xms控制)。堆无法扩展时,抛出OutOfMemoryError异常。

(六)元数据区

在jdk1.8中,JVM移除了永久代,取而代之的是元数据区,也称为元空间(Metaspace) ,也就是将本地内存用来存储,容量取决于是32位或是64位操作系统的可用内存大小。元数据区是方法区的一种实现,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

元数据区是线程共享的。 整个虚拟机中只有一个元数据区。元数据区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。因此内存回收效率低。主要回收目标是:对常量池的回收、对类型的卸载。Java虚拟机规范对方法区的要求比较宽松,和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。

2020年6月1日

猜你喜欢

转载自blog.csdn.net/weixin_43907422/article/details/106461434
今日推荐