JVM - Java 虚拟机结构

本贴最后更新于 1800 天前,其中的信息可能已经时移世易

了解 jvm 的过程就像从喜欢到恋爱的过程,刚开始懵懵懂懂,然后心生胆怯,最后沉迷于此。


Java 虚拟机结构

这里将会从 class 文件格式、JVM 中的数据类型讲到 JVM 的运行时数据区

class 文件格式

“ Write once, run anywhere”,这句话对 Java 开发者来说都是无比的熟悉了。那么这种说法是怎么来的呢,为什么 Java 会有这样的口号呢?这其中的奥秘就在与 class 文件。在 Java 中,源文件(.java)首先会被编译为字节码文件(.class),然后再将字节码文件通过 jvm 去执行。在字节码的格式中,精确定义了类与接口的表示形式,包括了在平台相关的目标文件格式中的一些细节上的惯例,列如字节序(byte ordering)等。

这里请不要认为此处的平台相关的目标文件格式是指在特定平台编译出的 class 文件无法再其他平台中使用。相反,正是因为强制,明确的定义了本来会跟平台相关的细节,所以才达到了 Java 语言是平台无关的效果

也就是说 Java 语言的确是平台无关的。但这种平台无关性,正是由于 class 文件的平台有关有 JVM 的平台有关造就的。

数据类型

同 Java 语言中的数据类型相似,JVM 可以操作的数据类型也分为两大类:原始类型(primitive type,也经常被翻译成原生类型或者基本类型)和引用类型。与之对应,也存在原始值(primitive value)和引用值(reference value)两种类型的数值。他们可用于变量赋值,参数传递,方法返回和运算操作。

Java 虚拟机希望可能多的类型检查能在程序运行之前完成,换句话说,编译器应当在编译期间尽最大的努力完成尽可能的类型检查,使得虚拟机在运行期间无需进行这些操作,原始类型的值不需要通过特殊标记或额外识别手段来在运行期确定他们的实际数据类型,也无需刻意见他们与引用类型的值区分开。虚拟机的字节码指令本身就可以确定他的指令操作数的类型是什么,所以可以利用这种特性直接确定操作数的数值类型。
Java 虚拟值是直接支持对象的。这里的对象可以是指动态分配的某个类的实例,也可以是指某个数组。虚拟机中使用 reference 类型来表示某个对象的引用。关于 reference 类型的值,你可以想象成指向对象的指针。没一个对象都可能存在多个指向它的引用,对象的操作,传递和检查都通过引用它的 reference 类型的值来进行。

这里的 reference 类型与 int、long、double 等类型是同一层次的概念。reference 是前面提到过的引用类型(reference type)的一种。而 int、long、double 等则是前面提到过的原始类型(primitive type)的一种。前者是具体的数据类型,后者是某种数据类型的统称。

原始类型与值

Java 虚拟机所支持的原始数据类型包括数值类型,boolean 类型和 returnAddress 类型。
数值类型
byte、short、int、long、float、double、char。
boolean 类型
虽然虚拟机中定义了这种类型,但是却没有提供专门操作 boolean 的字节码指令,Java 语言表达式所操作的 boolean 值在编译之后都是用虚拟机中的 int 数据类型来代替。Java 虚拟机直接支持 boolean 类型的数组。boolean 类型数组的访问和修改共用 byte 数组的 baloadbastore 指令。在 Oracle 的虚拟机实现中。Java 语言中的 boolean 数组将会被编译层 Java 虚拟机的 byte 数组,每个元素占 8 位。不管是用 int 表示还是 byte,,虚拟机都会将映射后的 true 用 1 来表示。false 用 0 来表示。
returnAddress
returnAddress 类型是指向某个操作码的指针,此操作码与 Java 虚拟机指令相对应。在虚拟机所有支持的原始类型中,只有 returnAddress 类型是不能直接与 Java 语言的数据类型相对应。

引用类型与值

Java 中有三种引用类型,类类型(class type)、数组类型(array type 和接口类型(interface type)。这些引用类型的值分别指向动态创建的类实例,数组实例和实现了某个接口的类实例或数组实例。在引用类型中还有一个特殊的值 null,当一个引用不指向任何对象的时候,他的值就用 null 来表示。一个为 null 的引用,起初并不具备任何实际的运行期类型。但它可以转换为任意的引用类型。引用类型的默认值就是 null。
Java 虚拟机规范并没有规定 null 在虚拟机实现应该怎么用编码来表示。

运行时数据区

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机的启动而创建,随着虚拟机的退出而销毁。另外一些则则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

Java 运行时数据区图示如下:

QQ20180624150918.png

程序计数器(Program Counter Register)

Java 虚拟机可以支持多条线程同时执行,每一条 Java 虚拟机线程都有自己的程序计数器。在任意的时刻,一个 Java 虚拟机线程只会执行一个方法的代码,这个正在被执行的方法被称为当前方法,因此为了能够在切换线程后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条程序计数器之间互不影响,独立存储。我们层这类内存区域为“线程”私有的。如果这个方法不是 native 的,那么 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址。如果该方法是 native 的,那 PC 寄存器的值是空(Undefined)。PC 寄存器的容量至少应该保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 的区域。

Java 虚拟机栈(Java Virtual Machine Stack)

每一台哦 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,这个栈与线程同时创建,用于存储栈帧(Frame),Java 虚拟机栈的作用与传统语言中的栈非常相似,用于存储局部变量与一些尚未计算好的结果,另外,他在方法的调用和返回中也扮演了重要的角色,因为除了栈帧的出栈与入栈之外,Java 虚拟机不会再受其他因素影响,所以栈帧可以在堆中分配(从虚拟机的实现来看),Java 虚拟机栈所使用的内存不需要是连续的。
更多详细内容请移步详解 jvm 中的 Java 虚拟机栈

本地方法栈(Native Method Stack)

Java 虚拟机实现可能会用到传统的栈(通常称为 C Stack)来支持本地方法(除了 Java 以为其他方法编写的方法)的执行,这个栈就是本地方法栈。与 Java 虚拟机栈的功能类似,只不过一个是为了 Java 方法而服务的,一个是为了本地方法服务的。
在 Java 虚拟机规范中有提到,Java 虚拟机实现应当提供给程序员或者最终用户调节本地方法栈初始容量的手段,对于长度可动态变化的本地方法栈而言,则应当提供调节其最大最小容量的手段。
另外值得一提的是,HotSpot 虚拟机将 Java 虚拟机栈和本地方法栈的实现合并在一起!

Java 堆

这里请注意不要混淆StackHeapJVM StackJava Heap 的概念。Java 虚拟机的实现本质上是由其他语言所编写的应用程序,Java 语言程序里分配在 Java Stack 中的数据,从实现虚拟机的程序角度来看则可能分配在 Heap 之中。

在 Java 虚拟机中。堆(Heap)是可供各个线程共享的运行时内存区域。也是提供几乎所有类实例和对象分配内存的区域。

这里在 Java 虚拟机规范中描述的是,所有类实例和数组分配的内存的区域。但是随着 JIT 编译器的发展与逃逸分析技术日益陈述,栈上分配,标量替换等优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不那么绝对了。
Java 堆在虚拟机启动的时候就被创建,他存储了动态内存管理系统(automatic storage management system),也就是常说的 garbage collector(垃圾收集器)所管理的对象。Java 堆的容量可以是固定的,也可以是随着程序执行的需求动态扩展,并在不需要过多空间是自动回收。Java 堆所使用的内存同样不要求是连续的。当实际所需要的堆超过了能提供的最大容量时,系统会抛出一个 OutOfMemoryError
从内存回收的角度来看,由于现在的收集器采用分代收集算法,所以 Java 堆还可以细分为:新生代和老年代,再细致一点的有 Eden 空间,From Survivor 空间,To Survivor 空间等
从内存分配的角度来看,线程共享的 Java 堆中可能划分中多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器所产生的本地代码数据等。
更多内容请穿越至走进方法区的前世今生

运行时常量池

运行时常量池(runtime constant pool)是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表现,它包括了若干种不同的常量,从编译期可知的数字字面量到必须在运行时期解析后才能获得的方法或字段引用。每一个运行时常量池都在 Java 虚拟机的方法区中分配,在加载类和接口到虚拟机后,就创建对应的运行时常量池。在创建类和接口的运行时常量池时,可能会发生如下异常。

  • 当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那么 Java 虚拟机将会抛出 OutOfMemoryError 异常。

以上就是关于 Java 虚拟机结构的一些内容了,需要注意的是,运行时数据区就是我们常说的 Java 内存结构(JVM 内存结构),而不是 Java 内存模型,更不是 jvm 内存模型(根本没有这个概念)。除此之外还有一个概念叫做 Java 对象模型,如果你对这几个东西还不甚了解的话,可以看下 JVM 内存结构 VS Java 内存模型 VS Java 对象模型


本文参考:
《深入理解 Java 虚拟机》
《Java 虚拟机规范(Java SE 8 版)》

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...