Java虚拟机的内存结构(详解与学习思考)

本来内存结构这篇文章我是没准备写的,但考虑到这个确实很重要,理解好Java内存结构对Java的学习有极大的好处并且能一定的提升代码质量,所以还是开始了这篇文章,文章最后一部分的“学习思考”相关内容指得一看,这是我积累的一系列关于Java内存结构相关问题的学习思考与解答,随着时间累积,相信足以在该领域能够越来越深入,从而驰骋职场。

一、Java程序的执行流程

1.具体流程图如下:
Java程序的执行流程
2.具体过程描述如下:
  我们编写的Java源代码经过Java编辑器javac编译成虚拟机可识别的class源文件(字节码文件)后,经过类加载器加载完毕,再由JVM执行引擎执行,执行过程中会有些地方调用到本地接口,调用本地的方法库。
  其中在class文件加载完毕到执行过程中,JVM会将程序执行时用到的数据和相关信息存储在运行时数据区(Runtime Data Area),这个区域也就是我们常说的JVM内存结构,垃圾回收也是作用在这个区域(主要作用在堆上)。

二、JVM运行数据区细分

1.图解入下
在这里插入图片描述

我们知道JDK用的虚拟机是HotSpot,JDK8的运行时数据区划分与虚拟机规范定义是有区别的:
(1)Java虚拟机栈和本地方法栈合二为一统称为栈;
(2)元数据空间取代方法区,并且元数据空间不在Java虚拟机中,而是在本地内存中;
(3)运行时常量池由方法区中移到了堆中。

注意:
  Java程序是多线程运行的,上述描述的线程共享区域是多线程共享同一片区域,而其他的区域是线程私有的,这里的线程私有指的是针对每一个线程,JVM都会从内存中指定的区域中取出一部分用于这个线程,其他线程来了也是这种操作,例如栈里面会针对每个线程开辟出一个与该线程对应的栈帧供其使用,程序计数器同样如此。

三、具体介绍

主要内容大家也都是了解的,后续有时间再补充吧!这里先介绍下栈帧中存储的信息。

下面的内容可参考:
https://www.cnblogs.com/ysocean/p/9345622.html
https://www.pianshen.com/article/22921622751/
(1)局部变量表(Local Variable Table)
  存放了编译期可知的各种Java虚拟机基本数据类型(八种基本数据类型)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)、returnAdress类型(指向了一条字节码指令的地址)。
  这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余数据类型只会占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小(指的是变量槽的数量)。虚拟机真正使用多大的内存空间由虚拟机自行决定,在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量(最大Slot数量)。
  
(2)操作数栈(Operand Stack)
  也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
  操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
  当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

注意
上面提到的方法的Code属性其实就是class字节码文件中的属性表(attribute_info)中的Code属性,该属性的使用位置正是方法表。
字段max_stack表示这个方法运行的任何时刻所能到达的操作数栈的最大深度
字段max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
另有很多字段值这里暂不赘述,后续会另起博文主要写class字节码文件格式。

(3)动态链接(Dynamic Linking)
  在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池
  Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)
  这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接

(4)方法返回地址
当一个方法开始执行时,可能有两种方式退出该方法:
1)正常完成出口
  指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定
2)异常完成出口
  指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
  无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态
  方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
  一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
  简单理解就是:方法返回地址中存放着方法被调用后返回的一切相关数据(执行指令,返回值等等)。

四、学习思考

我的习惯是会在文章后面补充一些疑问并解答,即是对知识点的学习巩固,也相当于罗列文章中知识点的面试题。

1.设计程序计数器的初衷(作用)是什么?
  Java程序是支持多线程运行的,程序技术器就是用于记录程序的执行指令,若程序运行过程中,出现CPU时间片切换的情况,之前的程序执行被中断,后续再得到CPU时间片重新开始执行时能够从中断的位置继续往下执行。
  程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基本功能都需要依赖这个计数器来完成。
  
2.堆与方法区和栈之间的关系?
  堆与方法区和栈之间都是指向和被指向的关系,实例对象都存在于堆中,而方法区中的常量存放对象在堆中的地址从而指针指向堆中的对象,栈中的对象引用也是存放对象的地址指向堆中引用对应的对象。
  
3.执行引擎会做些什么?
  字节码文件被类加载器加载后会存放于方法区(元数据空间)中,执行引擎就是执行加载后的字节码文件;修改程序计数器的相关参数;调用垃圾收集器线程也是执行引擎干的。

4.字节码解析器是如何工作的?
  就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

5.运行时数据区中各区域创建时间?
  线程私有的区域是线程(方法)执行时同步创建,如虚拟机栈、本地方法栈、程序计数器;
  线程公有的区域是在虚拟机启动时创建,如方法区和堆。

6.Java堆是由什么管理的?
  堆由垃圾收集器管理的内存区域,因此也会被称为“GC堆”(Garbage Collected Heap)。

7.运行时数据区中各区域物理存储方式?
  Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现处于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
  方法区除了和堆一样不需要连续的内存和可以选择固定大小或者可拓展外,甚至还可以选择不实现垃圾收集。

8.方法区中的垃圾回收怎么理解?
  少见但并不表示没有,方法区中的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。
  可参考《Java GC的疑问解答(面试点归纳)》中的第22条。

9.Class文件中有哪些信息?
  Class文件中除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
注意
(1)符号引用翻译出来的直接引用(符号引用替换成直接引用就是类加载过程中的解析(Preparation))也存储在运行时常量池中。
(2)Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性用的比较多的就是String::intern()(将字符串强制放入常量池中)

附:
直接引用(Direct References)
  直接引用是可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用时和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
符号引用(Symbolic References)
  符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量(对象字面量是指直接以一常量给对象赋值,而不是在堆空间new出一个对象实例),只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。

10.Java中使用到的直接内存是如何工作的?
  在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
注意:这里涉及到堆外内存,可参考《Java中的堆外内存和堆内内存》

11.运行时数据区中各区域常见的内存相关异常?
  Java虚拟机规范定义的运行时数据区中,只有程序计数器不会出现OOM(内存溢出,OutOfMemory),其他的都会出现,条件是线程请求的内存容量大于系统所能提供的最大容量就会出现内存溢出异常;
  虚拟机栈和本地方法栈对应的还会出现栈溢出异常(StackOverflowError),条件是线程请求的栈深度大于虚拟机所允许的最大深度。

12.运行时数据区中各个组成部分的相关虚拟机参数有哪些?
以HotSpot为例:
(1)堆
最大值:-Xmx
最小值:-Xms

(将-Xmx和-Xms设置为一样即可避免堆自动拓展
-XX:+HeapDumpOnOutOfMemoryError:让虚拟机再出现内存溢出异常的时候Dump出当前的内存堆转存储快照以便进行事后分析。
(2)栈
栈容量:-Xss
(本地方法栈大小:-Xoss,实际没有效果,因为HotSpot中不区分虚拟机栈和本地方法栈 )
(3)元空间
-XX:MaxMetaspaceSize
设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
(JDK7之前的方法区容量-XX:MaxPermSize)
-XX:MetaspaceSize
指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间 ,那么不超过-XX:MacMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
-XX:MinMetaspaceFreeRatio
在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:MaxMetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
(4)直接内存
-XX:MaxDirectMemorySize
指定直接内存的容量大小,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致

梧高凤必至,花香蝶自来。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值