地基篇 - JVM 中的 运行时数据区

271 阅读9分钟

前言

我们知道JVM的大致执行过程是:

前提: 0- JVM系语言进过编译期,从源代码文件(.java、.kt )变成中间语言文件(.class)【这一步和JVM无关,这一步是通过Java 前端编译器(javac)完成的】如下图一所示:

java运行过程-2.jpg

1- .class 文件,通过类加载系统进行初始化

2- 然后将初始化之后的信息传递给运行时数据区。这个区域约定了不同变量和参数的储存位置。

3- 运行时数据区存储着要执行的字节码,执行引擎按行执行。垃圾回收机制也会在这个环节起作用。对运行时数据区的堆进行对象垃圾回收。

4- 如果存在Native 方法的执行,则通过本地接口和本地方法库来执行运行时数据区的Native Method Stack 里面的方法。

步骤1到步骤4,如下图二所示:

JVM的示意图.png

本节,我们来重点聊一聊JVM的运行时数据区

学习的作用

了解JVM的执行过程可以帮我们理解程序究竟是如何跑起来的。

定义

运行时数据区域,就是JVM代码执行的过程中数据存在的区域。

可以分成两个部分。

一部分和JVM的创建与销毁保持一致,叫做线程共享区域。

另一部分和线程的生命周期保持一致,叫做线程私有区域。

运行时数据区.png

疑问一:JVM的生命周期

回答:

  1. 虚拟机的启动 java虚拟机的启动是通过引导类加载器(boostrap class loader)创建一个初始类来完成,这个类是由虚拟机的具体实现指定的。如图二的第一行。
  2. 虚拟机的运行 执行一个java程序的时候,其实执行的是一个java虚拟机进程。如图二的其他部分
  3. 虚拟机的退出

有如下几种情况:

  • 程序正常执行退出
  • 程序在执行中遇到异常或者错误而异常终止
  • 由于操作系统出现错误导致Java虚拟机进程终止
  • 某线程调用Runtime类或者System类的exit方法,或者Runtime类的halt方法,并且Java安全管理器也允许这次exit或者halt操作
  • 除此之外,JNI规范描述用JNI Invocation API来加载或者卸载Java虚拟机时,Java虚拟机的退出情况

疑问二:线程的生命周期

回答:线程的生命周期包括五个状态:创建状态、就绪状态、运行状态、阻塞状态、死亡状态。 如下图三所示:

java-thread.jpg

具体的五个区域

一 、PC 寄存器(线程独享)

由于JVM支持多线程编程,所以当CPU暂停线程A,把时间片交给线程B的时候,就需要一个区域来记录当前线程A的执行进度和当前状态。

这个区域就是PC寄存器。

如果执行的是方法不是native方法 pc寄存器则保存指向当前执行字节码的指令地址,如果执行的是native方法 pc寄存器会保存undefined(不明确的)。

总结: PC寄存器只是记录Java字节码的指令地址(执行进度),无法记录不明确的本地方法的执行记录。

二、本地方法栈 (线程独享)

存放native方法的区域。

如果Java 语言想要调用其他语言的话,就需要使用这个区域。JVM 的实现就使用到了C Stack (C 语言方法栈) 来实现native 方法。

native 方法:一个Native Method就是一个java调用非java代码的接口。 该方法的实现由非java语言实现,比如C。

JNI : Java Native Interface的缩写,通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。

补充:JNI 不仅仅可调用C/C++ 的动态库。其他语言也可以,比如:go 语言。

补充:JNI 在Android 中主要是用来调用C/C++ 的动态库。Android 还有官方专门的NDK 可以做这件事。

补充:JNI 和JNA

JNI技术,不仅可以实现Java访问C函数,也可以实现C语言调用Java代码。

JNA只能实现Java访问C函数,作为一个Java框架,自然不能实现C语言调用Java代码。 此时,你还是需要使用JNI技术。 JNI是JNA的基础,是Java和C互操作的技术基础。

补充:什么是动态库?

C/C++程序编译流程:
预处理->编译->汇编->链接
库:编译好的二进制文件(注意,按照上面的流程,上面所说的“编译”只是得到了汇编文件,而这里的编译是指完整的翻译过程,即高级语言翻译成机器代码的完整过程。对应上面的流程,可以认为是前三步的统称。)

静态库和动态库的区别就是最后一步,链接(Link)的区别。
静态库:在Link 的时候会直接把.lib(Windows)或者.a(Linux和Mac)复制到目标程序中去,形成.exe文件。
优点:不需要执行前面三步。
缺点:目标程序可执行文件更大。
动态库:在Link 的时候不会直接将.dll(Windows)、.so(Linux)、.dylib(Mac)加载到目标程序的可执行文件中去,只是会保留需要调用的部分的引用。
当程序运行的时候,目标代码才会从动态库中加载进来。

注意:这个部分不是必须实现的,完全取决于虚拟机的实现。

三、栈(线程独享)

线性表,里面储存栈帧(Frame)。

栈:一个端口进行删除和插入的线性集合。【LIFO:Last in, First out,后进先出】

方法的调用和返回就是基于栈帧实现的。

栈帧

定义: 栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接 , 方法返回,异常分派等工作。

栈帧的生命周期是跟方法一致的,随着方法的调用而创建,方法的结束或者异常而销毁。【所以,方法的执行遵从先进后出的原则。】

组成:每个栈帧都由局部变量表,操作数栈,动态链接组成。

  • 局部变量表 一个储存局部变量的列表。一个局部变量可以存储一个基本数据类型的数据或者一个引用类型对象的引用(通过这个引用,返回指向的对象)。double 和long 需要占据两个局部变量。

在JVM要使用局部变量时。对于类方法,局部变量是按0序列进行存储的。对于类的实例方法,0序列是用来存储当前对象引用的,局部变量会从1序列开始存储。

对于需要占据两个局部变量的double 和 long 来说。如果在n和n+1两个序列位置存储了这两个类型的值,那么取用的序列选择n。

在编译期就决定了局部变量表的长度。

  • 操作数栈

这是栈帧中的栈。全名叫做当前栈帧的初操作数栈。

执行栈帧按操作数栈顺序执行。操作数栈从局部变量表中拿到局部变量,进行执行。最终的方法返回值也会出现在这里。

区别三个概念:栈、栈帧、操作数栈

栈:运行时数据区的一个区域,是线程独享的,负责执行方法。

栈帧:就是一个方法的具体数据结构,存在于栈中。

操作数栈:

创建时操作数栈是空的。

虚拟机提供一些指令从局部变量表把一些常量或者变量值加载到操作数栈,也提供了从操作数栈取走数据的指令。

(程序中)调用方法时操作数栈用来准备调用方法参数以及接受方法的返回结果

  • 动态链接 动态链接是用来完成运行时绑定操作的。

在.class文件里一个方法要是调用其他方法或者类里面的其他成员变量,则需要通过符号引用来表示。

每个栈帧中都有一个指向方法区运行时常量池的当前方法的一个引用。

持有这个引用就是为了完成动态链接。

Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

仔细回想一下: 动态库和静态库的区别和此处动静态链接是否非常相似。

什么才是符号引用?

可以暂时理解为C里面的指针。

四、堆 (线程共享)

用来存放类实例和数组对象,即为对象存放点。

虚拟机启动就会根据相关参数创建堆。

堆内对象不是显式回收的。而是由垃圾回收机制来处理。

为了配合垃圾回收机制的特性,堆内分为年轻代和老年代。

年轻代还可以分为Eden 和Survivor(from/to)

主要是为了配合垃圾回收算法。

五、方法区和运行时常量池 (线程共享)

简单理解,这里就是程序代码的元数据存储的地方。

元数据包括运行时常量池、字段和方法数据。

构造函数和普通函数的字节码内容。

运行时常量池

定义:.class 文件中每个类或者接口里面的常量存在的地方。

包括:

编译期可以知道的数值字面量。

运行时才能解析获得的方法或字段引用。(猜测:是注解)

后记

这次修改主要是修改了一处本文的错误。

地基篇暂时写到这里,之后我有更加深入的理解之后,我会继续巩固地基篇。下一篇文章,我要进入实用篇了。希望自己越写越好。