JVM运行时数据区

JVM内存区域的划分

和C/C++开发不同,在从事JAVA的开发过程中,我们对内存区域的关注相对较轻,但是了解和掌握JAVA的内存结构会帮助我们做出合理的优化决策。首先,先大体的了解一下JAVA虚拟机运行时的内存结构:
在这里插入图片描述
在这里插入图片描述

从线程的角度来看,总体分为私有和共享的两部分。共享的数据区为方法区,堆,本地库接口,以及每个JVM虚拟机中的JVM执行引擎,而线程私有的数据区则为虚拟机栈,本地方法栈,程序计数器。
java虚拟机在运行时会将内存空间划分为不同的数据区域。每个区域都有各自的用途以及生命周期。有些区域伴随着JVM进程的存在而存在,有些区域“随线程而生,随线程而死”。

一、方法区

方法区和java堆一样,也是线程共享的区域。
作用:存储已被虚拟机加载的类信息(class文件中的完整信息,如运行时常量池数据,方法和构造方法的字节码等)、即时编译器(JIT)编译后的代码等数据。
当jvm使用类装载器装在某个类时,它首先要定位到对应的class文件,然后读入这个class文件,最后提取该文件的内容信息,并将这些信息存储到方法区,最后返回一个class实例。

方法区是系统分配的一个内存逻辑区域,是一块所有线程共享的内存区域,用来存储类型信息(类型信息可以理解为类的描述信息(类的全限定名,访问修饰符,字段,方法等)),方法区的大小决定了系统可以包含多少个类,如果系统类太多,方法区内存不够会导致方法区溢出,虚拟机同样会抛出内存溢出信息。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
方法区的大小不必是固定的,jvm可根据应用需要动态调整,同时,方法区也不一定是连续的
在这里插入图片描述
在这里插入图片描述
当你写好一个Java程序后,由Java编译器编译成字节码,而不是编译成与某个特定的处理器硬件平台对应的指令代码, 处理器需要翻译成对应的机器指令才能执行,而JIT可以把会多处执行的代码提前编译为处理器识别的机器指令,存放在方法区,提高代码执行的效率。
运行时常量池是方法区的一部分。.class文件中除了类信息外,还有一项就是常量池,用于存放编译器形成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。
方法区存放内容:

1.类的全限定名(类的全路径名)。

2.类的直接超类的全限定名(如果这个类是Object,则它没有超类)。

3.类的类型(类或接口)。

4.类的访问修饰符,public,abstract,final等。

5.类的直接接口全限定名的有序列表。

6.常量池(字段,方法信息,静态变量,类型引用(class))等
7.属性等
即class文件中描述类的完整内容
8、热点代码指令

二、堆

在Java中我们最熟悉的就是对象,在内存中用来存放对象实例的区域称之为堆(Heap),此区域由线程共享,在进行垃圾回收时,此区域是垃圾回收器重点关注的地方,因此我们也称之“GC堆”。JAVA堆(JAVA HEAP):是JAVA虚拟机所管理的内存中最大的一块。JAVA堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

可能会抛出OutOfMemoryError:当没有足够的区域实现对象实例的分配,并且该堆也没法实现扩展。
在这里插入图片描述

对象:
对象在内存中的存储的布局可以分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头
对象头包括两部分信息:
第一部分为Mark Word,用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据在32位和64位虚拟机(未开启指针压缩)分别为32bit和64bit,Mark Word是一个非固定长度的数据结构,可以根据对象的状态复用自己的存储空间。
另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据。

实例数据
对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容。无论是从父类继承的还是子类定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数和字段在java源码中定义顺序的影响。

对齐填充
这部分不是必须的,仅仅是为了满足jvm的字节对齐要求

三、程序计数器

这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能保证能恢复到正确的位置)。
程序计数器,这是JVM规范中唯一一个没有规定会导致OutOfMemory(内存泄露,下文简称OOM)的区域。
在这里插入图片描述
指令寄存器用来存放即将执行的指令,cpu从指令寄存器中获取指令执行,指令寄存器从程序计数器中获取到下一行执行的指令地址,并加载到指令寄存器中,然后程序计数器加1,即存放下一行将执行的指令的地址。(处理器任何一个确定的时刻,一个都只会执行一条程序的指令)
注意:
如果正在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址。
如果是native方法,则计数器值为空(native 方法 指得就是Java程序调用了非Java代码,算是一种引入其它语言程序的接口)。

四、虚拟机栈

代码业务逻辑的实现就是通过栈来实现的,体现了方法的调用和退出。
每个JVM的线程在创建的时候,都会创建一个栈。一个栈包含很多栈桢。JVM的栈好比传统语言C的栈,它维持(存储)本地变量和部分结果,并在方法 调用和返回中(被)使用。
Java虚拟机栈是线程私有的,它的生命周期与线程相同。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在栈中从入栈到出栈的过程。
每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口等信息。
在这里插入图片描述

java虚拟机栈,规定了两种异常状况:
如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
栈:是一种先进后出的数据结构,是指以栈帧为单位先进后出
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码中“1234”是字面量,在类加载时,就直接存放在了方法区的常量池中,s1的指针直接指向该常量池的“1234”
局部变量表
方法调用一次,向栈中压入一个栈帧, 包含一个方法的局部变量表,如:
局部变量表存放了编译期可知的各种基本数据类型(boolean 、 byte 、char 、short、int 、 float 、 long 、 double ),对象引用(reference类型,它不等同与对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

–>局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种),long和double是64位的,占两个slot。reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
Java虚拟机是使用局部变量表完成参数值到Java方法参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
Slot是可以重用的,下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。

在这里插入图片描述
示例:
在这里插入图片描述
在这里插入图片描述
操作数栈
操作数栈也常被称为操作栈,同样是一个后进先出的数据结构。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。在做算术运算的时候是通过操作数栈来进行的,在调用其他方法的时候是通过操作数栈来进行参数传递的。JVM将操作数栈作为工作区。JVM没有寄存器,所有的参数传递和返回值都是基于操作数栈来完成的。
比如,执行引擎执行c = a + b时,会先被操作的参数a和b压入操作数栈,然后操作指令将他们弹出栈,并执行操作,将结果再压入栈。
Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池有存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
方法出口(返回地址)
当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

五、本地方法栈
与虚拟机栈作用类似,区别是虚拟机栈为执行字节码服务,而本地方法栈为虚拟机使用到的native方法服务。由于虚拟机规范中没有对本地方法栈中使用的语言、数据结构做出强制规定,具体的虚拟机可以自由实现。Sun HotSpot虚拟机直接把虚拟机栈和本地方法栈合二为一。本地方法栈也会抛出stackOverFlow和OOM异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值