JVM运行时数据区(基于JDK8,结合字节码以及自己的理解)

JVM和并发和Java中两个高级主题,今天来聊下JVM组成中的一部分,也是跟我们日常工作联系最紧密的JVM运行时数据区。
先来个JVM整体的结构图:
JVM组成
我们编写的源文件经过编译器(如Javac编译器,Java虚拟机也是跨平台的,其他语言如JRuby等其他语言可以由自己语言的编译器进行源码到字节码的编译)编译为存储字节码的Class文件后,后续的工作就由Java虚拟机接管了。类加载子系统把描述类的数据从Class文件加载到运行时数据区的方法区(JDK1.8之后的元空间),并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。执行引擎在执行Java代码的时候可能会有解释执行(通过解释器)和编译执行(通过JIT即时编译器)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎(解释器和即时编译器之外可能还会有提前编译器),执行引擎也会在JVM运行时,开启守护线程用于进行垃圾回收。
介绍完大致流程之后,回到我们本次的主题-运行时数据区:

虚拟机栈(对应例图中运行时数据区的栈区):
虚拟机栈是一种LIFO后入先出的栈结构实现,一个Java线程就对应一个虚拟机栈内存结构,虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接和方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。虚拟机栈因为随线程销毁而释放内存空间,所以不需要垃圾回收管理,不过如果方法调用深度很深,比如递归调用,则可能导致栈深度不足而出现StackOverflowError,可通过-Xss参数调整栈大小。
再来具体说下栈帧中各个部分:
1.1局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,如各种基本类型、对象引用和returnAddress。我们可以通过jclasslib插件查看Class文件中各方法对应的局部变量表,如下图:
在这里插入图片描述
对于method1我们可以从红圈标出来部分看到局部变量表中有3个,分别是this,length,num,length和num是我们自定义的变量名,而this是什么呢?其实这个this就是指向当前实例对象的变量,我们在实例方法中能够使用this关键字也是因为虚拟机帮我们在局部变量表中放入了该变量,而如果是static修饰的类方法,则其局部变量表中就没有这个this局部变量,我们也就无法在类方法使用this引用当前实例对象。有兴趣的小伙伴可以自己用jclasslib插件观察下static方法的局部变量表。
1.2操作数栈(Operand Stack)也常被称为操作栈,它也是一个后入先出LIFO栈。JVM指令基本上是一种基于栈的指令集架构(可对比一般物理机的基于寄存器的指令集架构),指令流中指令大部分都是零地址指令,它们依赖操作数栈进行工作。比如我们进行一个简单的int类型相加,如下图:
在这里插入图片描述
iconst_1指令将int类型常量1压入操作数栈,istore_1指令将int类型值存入局部变量1,iconst_2和istore_2也类似,常量2压入操作数栈然后再取出栈顶的常量2存入局部变量2。Java中方法调用指令也是类似,如字节码17行的aload 4指令就是把局部变量4(operandStack对象)压到栈顶,字节码19行的invokevirtual指令调用虚方法,运行时按照对象的类来调用实例方法,这也是Java中多态机制的字节码实现的一部分。
1.3动态连接(Dynamic Linking)是栈帧中指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。操作数栈那个例图中的字节码19行我们可以看到invokevirtual后面有个#4,这个#4是Class文件结构中的常量池中指向方法的符号引用,如果方法是非虚方法,比如static修饰的类方法(对应invokestatic指令)、private修饰的私有方法等(对应invokespecial指令),因为非虚方法不会有多态选择,所以这些符号引用可以在编译器就能确定方法的具体版本,而虚方法,如一般的实例方法,则需要在运行期才能确定方法的具体版本,这部分就称为动态连接。
1.4方法出口(returnaddress)可能存放的是调用该方法的程序计数器的值,方法可能有正常退出或异常退出两种,不过都需要方法出口来确定方法退出后要做什么操作,从哪里开始执行方法返回后的字节码指令。

二,本地方法栈:
本地方法栈用于管理Native本地方法的调用,是线程私有的,Native方法由C或C++语言实现,一般是与平台有关的实现,并且一般也是效率比较高的,在执行引擎执行natvie方法时需要加载本地方法库。

三,程序计数器(对应运行时数据区中的PC寄存器):
程序计数器也叫PC寄存器,可以看作是当前线程所执行的字节码的行号指示器。我们可以类比CPU中的一个也叫程序计数器的寄存器,CPU中程序计数器保存指令地址,CPU的控制器就是根据程序计数器的值从内存中读取指令进行译码和执行的,程序计数器决定这程序的流程。JVM运行时数据区中的程序计数器也是类似功能,在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器也是线程私有的,Java多线程使用同一个CPU内核来实现并发时,线程切换后要恢复到正确的执行位置就必须每条线程有一个独立的计数器,用于记录当前线程执行的字节码指令的行号,我们在描述操作数栈的例图中字节码指令左边红色的数字,就是字节码指令的行号。

四,Java堆:
Java堆(Java Heap)是所有JVM线程共享的内存区域,在虚拟机启动时创建,所有的对象和数组都分配在该运行时数据区。堆空间由自动的垃圾回收系统进行回收,不需要程序员显示的进行内存的分配和回收。堆空间可以是一个固定大小也可以是可扩展的,我们可以通过-Xms和-Xmx设置初始和最大堆内存,一般建议设置为一样的。如果堆中没有内存完成实例对象内存分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError。

五,方法区:
方法区(Method Area)也是被所有JVM线程共享的内存区域,在虚拟机启动时创建,用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器(JIT)编译后的代码缓存等数据。JDK1.8之前,HotSpot使用永久代来实现方法区,这种设计容易遇到永久代溢出问题,JDK1.8之后,开始使用元空间来实现方法区。如果方法区无法满足新的内存分配需求时,将抛出OOM异常,如经常动态生成大量Class的应用中,包括CGLib字节码增强和大量JSP等。
不足之处,欢迎指正,谢谢。
如果觉得有收获,点个赞呗,多谢!

发布了14 篇原创文章 · 获赞 3 · 访问量 929

猜你喜欢

转载自blog.csdn.net/sjz88888/article/details/104864256