深入理解Java虚拟机之运行时数据区

运行时数据区

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

在这里插入图片描述

程序计数器

	程序计数器(Program Counter Register)是 一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号器.字节码解释器在工作时就说通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖这个计数器完成的.
	每个线程都有个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存.
	若线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若正在执行的是Native方法,这个计数器值则为空.此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.

Java虚拟机栈

	Java虚拟机栈也是线程私有的,它的声明周期与线程相同.虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个**栈帧**用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每个方法从调用直到执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程.
	
	在Java虚拟机规范中,对这个区域规定了两种异常状况:若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;若虚拟机栈可以动态扩展(大部分虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),若扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常.

栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 |
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 | 虚
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 | 拟
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 | 机
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 | 栈
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 |

栈帧

	每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。
	
    一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧 (Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。

两个栈帧之间的数据共享

在这里插入图片描述

局部变量表

	局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_stacks数据项中确定了该方法需要分配的最大局部变量表的容量。

操作数栈

	操作数栈也常被称为操作栈,它是一个后入先出栈。操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的 max_stacks 数据项中。操作数栈的每一个元素可以是任意Java数据类型,包括long和double。
	
	当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。

动态链接

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

方法返回地址

	当一个方法被执行后,有两种方式退出这个方法。
	第一种方式是执行引擎遇到任意方法返回的字节码指令,这时可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型根据遇到返回指令来决定,这种退出方法方式称为正常完成出口。
	
	另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,就会导致方法退出,这种退出方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。
	
	无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上 层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是 要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
	方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

附加信息

	虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

本地方法栈

	本地方法栈与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用的Native方法服务.
	在虚拟机规范中并没有强制规定本地方法栈中方法使用的语言、使用方式与数据结构,因此虚拟机可以自由实现它.甚至有些虚拟机(如 HotSpot)直接把本地方法栈和虚拟机栈合二为一.与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常.

Java堆(Java Heap)

	Java堆是Java虚拟机所管理的内存中最大的一块.Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存.
	
	Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为"GC堆".从内存回收角度来看,由于现在收集器基本都采用分带收集算法,所以Java堆还可以细分为:新生代和老年代.从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区.
	根据Java规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可.如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常.

方法区

	方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
	
	HotSpot虚拟机设计团队选择把GC分带收集扩展至方法区,或者使用永久代来实现方法区,这样HotSpot的垃圾回收器就能像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作.使用永久代来实现方法区并不好,这样更容易遇见内存溢出问题.因此,在Java8中,永久代已经被移除,被一个称为"元数据区"(元空间)的区域所取代.
	
	元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元数据和永久代最大区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小受本地内存限制,类的元数据放在native memory,字符串常量池和类的静态变量放入Java堆中,这样加载多少类的元数据都不会再受MaxPermSize控制,而是系统实际可用空间来控制.
	Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集.垃圾收集行为在这个区域比较少,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载.
	根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常.

运行时常量池

	运行时常量池是方法区的一部分.Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放.
	
	运行时常量是相对于常量来说的,它具备一个重要特征是:动态性。当然,值相同的动态常量与我们通常说的常量只是来源不同,但是都是储存在池内同一块内存区域。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)
	Java虚拟机对Class文件每一部分(包含常量池)的格式都有严格规定,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行.但对于运行时常量池,Java虚拟机规范没有做任何细节要求,不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中.
	当常量池无法再申请到内存时会抛出OutOfMemoryError异常.

字面量

在计算机科学中,字面量是用于表达源代码中一个固定值的表示。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
	int a = 10; // 10 为字面量
	String str = "hello world" // hello world 为字面量

符号引用

	符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可. 例如, 在Java中, 一个Java类将会编译成一个class文件. 在编译时, Java类并不知道所引用的类的实际地址, 因此只能使用符号引用来代替.
	
	比如org.meili.Test类引用了org.meili.B类, 在编译时Test类并不知道B类的实际内存地址, 因此只能使用符号"org.meili.B"来表示B类的地址.

直接引用

	直接指向目标的指针(指向方法区中类对象,类变量和类方法的指针),相对偏移量(指向实例的变量,方法的指针),一个间接定位到对象的句柄.
	
	就是程序运行时可以定位到东西(类,对象,方法,变量)的引用

直接内存

	直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁使用,而且也可能导致OutOfMemoryError异常出现.
	
	在JDK1.4中新加入的NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作.这样能在一些场景显著提高性能,避免了在Java堆和Native堆中来回复制数据.
	
	本地直接内存的分配不会受到Java堆大小的限制,但还是会受到本机总内存的限制,若各大内存区域总和大于物理内存限制,会导致动态扩展时出现OutOfMemoryError异常.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值