17张图带你了解,JVM运行时数据区
开篇
众所周知,Java程序的执行需要依赖于JVM(Java 虚拟机)。JVM 会将Java源代码编译成字节码文件,然后使用类加载器将其加载到运行时数据区中执行,垃圾收集器也会针对运行时数据区进行对象回收的工作。今天就来说说JVM的运行时数据区。
运行时数据区概述
在计算机世界中,内存是十分重要的系统资源,它承载着操作系统和应用程序实时运行的责任。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,从而保证了JVM的高效稳定运行。
Java虚拟机在执行Java程序的过程中,会将涉及到的数据划分到不同的内存区域去管理,在这些数据区域,有些是随着虚拟机启动而创建,虚拟机关闭而销毁。还有一部分是随着线程生命周期创建销毁的。这部分区域就是接下来要讲的Java虚拟机的运行时数据区。
图1 运行时数据区
如图1所示,红色的部分就是运行时数据区,它包括:方法区、堆、虚拟机栈、本地方法栈以及程序计数器五个部分。
图1中标注为黄色的方法区和堆是线程间共享的,也就是说它们会随着虚拟机启动而创建,随着虚拟机退出而销毁。橙色部分为每个线程单独享有的,即它们与线程是一一对应的,会随着线程开始和结束而创建和销毁。在HotSpot JVM中,每个线程都与操作系统的本地线程直接映射,例如:有一个Java线程准备好执行时,就有一个操作系统的本地线程被创建并且与Java 线程对应,当Java线程执行终止后,本地线程也会被回收。同时操作系统负责线程调度,及分配对应的CPU执行线程,一旦操作系统的本地线程初始化成功,它就会调用Java线程中的的run()方法去执行Java线程。
褐色部分的执行引擎就负责读取指令并且交由CPU执行,它包括解释器、JIT(即时编译器),GC(垃圾回收器)。而另外一个褐色的本地库接口会提供Java程序调用的native方法。
另外,运行时数据区的划分也随着JDK的发展不断变迁,如图2 所示, JDK 1.6、JDK 1.7、JDK 1.8 的内存划分都会有所不同。
图2 运行时数据区的变迁
如图2 所示,在JDK 1.8 中加入了元数据区的概念,将原来保存在方法区中的运行时常量池和类常量池都包括其中。
虚拟机栈
上面介绍了JVM 运行时数据区的概念和组成,接下来一次介绍每个组成部分,首先从虚拟机栈开始。
每个Java线程都会对应一个虚拟机栈,换句话说多个线程就对应多个虚拟机栈。上面讲过了虚拟机栈是线程私有,虚拟机栈中包含多个栈帧(Stack Frame),每一个栈帧是为方法执行而创建的,栈帧中描述的是Java方法执行的内存模型。每个方法从调用开始直到完成的全过程都对应着一个栈帧。栈帧是用来管理Java程序的运行,并保存方法的局部变量、部分结果、并参与方法的调用与返回。在活动线程中,只有一个栈帧是处于活跃状态的,也就是说只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
如图3 所示,每个Java 方法都会对应一个栈帧,左边的四个方法就对应了四个栈帧,从下往上依次是方法调用的顺序,最终方法1 会调用方法4, 此时正在执行方法4 ,它对应的栈帧4 就是“当前栈帧”,就是出于活跃状态的,其包含了局部变量表、操作数栈、动态链接以及返回地址等信息。
图3 栈帧结构
局部变量表
它定义为数字数组,主要用于存储方法参数和定义在方体内的局部变量,包含基本数据类型,对象引用,以及returnAddress类型。它建立在线程的栈上,是线程的私有数据,因此不存在数据的安全问题。
局部变量表所需的容量在编译期间确定,在运行期间是不改变其容量。方法嵌套调用的次数由栈的容量来决定,例如图3就进行了4个方法的嵌套,也就是说栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,对应的栈帧就越大。因此,函数调用就会占用更多的栈空间。局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
操作数栈
它是一个后进先出的栈,在方法执行的过程中,根据字节码指令、往栈中写入或取出数据,即入栈/出栈。字节码指令将值压入操作栈,其余的字节码指令将操作数取出栈,进行操作之后再将结果压入栈。操作包括:复制、交换、求和等。
这样讲比较抽象,来看一个具体的例子。
如图4 所示,生成一个testAdd 方法,给变量i和j 分别赋值为1 和2 ,然后让其相加并且把结果赋值给k。
图4 操作数栈代码
使用jclasslib反编译上面的代码得到图5 的结果。
图5 jclasslib反编译结果
如图6 所示,当执行地址 0 的时候操作指令为bipush,此时程序寄存器的地址显示为0 ,bipush 命令将 1 压入到操作数栈的顶部。
图6
如图7 所示,当指令地址到2 的时候,程序寄存器显示为2, 此时执行istore_1 的指令,将栈顶的数字1 保存到局部变量表中。
图7
如图8所示,指令地址执行到3 的时候,程序寄存器为3 , bipush指令把2 压入到操作数栈的顶部。
图8
在指令地址为5 的时候,程序寄存器的值为5, istore_2指令将操作数栈中的2 保存到局部变量表中的2 的位置。
图9
如图10所示,指令地址为6 的时候,执行iload_1 指令获取局部变量表中 位置为1 的值,也就是1 并且把它放到操作数栈的顶部。
图10
如图11所示,指令地址为7 的时候,执行iload_2 指令,从局部变量表2 的位置取出值2 放到操作数栈的顶部。
图11
如图12 所示,在指令地址为8 时,执行iadd 指令,将操作数栈的两个数字1和2 相加结果为3,并且将其放到操作数栈的顶部。
图12
如图13 所示,接着执行指令地址 9 , istore_3 执行之后将操作数栈顶的3 保存到局部变量表3 的位置,完成相加的操作,最后通过指令地址10 中的return指令返回方法。
图13
图片新闻
技术文库
最新活动更多
-
即日-12.26立即报名>>> 【在线会议】村田用于AR/VR设计开发解决方案
-
1月8日火热报名中>> Allegro助力汽车电气化和底盘解决方案优化在线研讨会
-
1月9日立即预约>>> 【直播】ADI电能计量方案:新一代直流表、EV充电器和S级电能表
-
即日-1.14火热报名中>> OFweek2025中国智造CIO在线峰会
-
即日-1.20限时下载>>> 爱德克(IDEC)设备及工业现场安全解决方案
-
即日-1.24立即参与>>> 【限时免费】安森美:Treo 平台带来出色的精密模拟
推荐专题
发表评论
请输入评论内容...
请输入评论/评论长度6~500个字
暂无评论
暂无评论