likes
comments
collection
share

jvm中的数据结构:Java Virtual Machine Specification Runti

作者站长头像
站长
· 阅读数 270

官网下载的jdk虽然自带java虚拟机,但是java语言规范并没有指定jvm实现,查阅了网络上关于jvm的资料,几乎没有关于jvm实现与jvm规范之间的异同点进行分别说明,大都将jvm规范中的内存结构与HotSpot jvm中实现的java堆中对象的生命周期混/内存模型为一谈,因此在理解jvm的过程中,以下两个问题已知困扰着我:

1、不同的虚拟机之间,java程序的内存模型一样吗?

2、不同的虚拟机之间接受的java参数是否相同?默认的hotspot虚拟机-XX系列参数在其它的jvm中是否也能够使用?

网上的各种教程对上述问题则是语焉不详,在这里要推荐一下周志明老师的《深入理解Java虚拟机:JVM高级特性与最佳实践》,数中详细解释了上述问题以及jvm中的各种细节。

以下内容是我对比周志明老师的《深入理解Java虚拟机:JVM高级特性与最佳实践》与官方的《JVM规范》中的内容后,对《JVM规范》Runtime Data Areas章节的翻译,并加入了个人的整理与理解。

注:本来想就《JVM规范》中的内存结构章节整理出JVM内存结构发展概况,但是在对比了java se6至java se17的相关标准后,发现《JVM规范》并没有对jvm内存结构做出过大的变动,因此本文内容是基于jdk17

JVM内存模型概述

jvm会将内存划分为若干个区域进行管理,这些区域有各自的用途以及生命周期。根据各个数据区的作用,可以将这些区域分为两大类,即线程间共享的数据区和线程间私有的数据区,其中方法区和堆是由所有线程共享的数据区域,虚拟机栈、本地方法栈和程序计数器为各个线程私有。

jvm中的数据结构:Java Virtual Machine Specification Runti

程序计数器(Program Counter Register)

PC Register是一块较小的空间,用于指示当前线程正在执行的指令的位置。在jvm中,字节码解释器的需要改变PC register的值来选取下一条需要执行的字节码指令,程序中的分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器来完成。当jvm执行类方法时,PC Register用于指示当前指令的位置。

jvm栈与PC Register一样是各个线程私有的,它的生命周期与线程一样,在线程创建时被创建,在线程结束时被销毁。jvm栈的功能与其它语言相同,用于储存局部变量表、操作数栈、动态链接、方法出口等。java程序每调用一个方法,就会产生一个栈帧(Frame),每个帧中,都会有各自的局部变量表、操作数栈、动态链接、方法出口等信息。

jvm栈中仅有两个动作:帧入栈及帧出栈,栈不直接操作帧,所以帧是可以分配 在堆上,且jvm栈可以是不连续的内存片段,可以是固定大小,也可以动态分配。

栈帧由线程在栈中创建,且不能被其它方法引用,在方法调用时产生,在方法结束或者中断后销毁。每个帧都有自己的局部变量表、操作数栈、常量池、动态链接、方法出口等信息。但是帧的数据结构以及帧大小取决于具体的jvm实现。一个线程,同时仅有一个帧在执行,这个帧被称为current frame,帧对应的方法被称为current method,当前方法所在的类称为current class。当前方法调用其它方法或者当前方法结束时,current frame停止工作。

局部变量表

局部标量表用于储存执行当前方法所需要的所有变量,因此局部变量表在编译期决定。局部变量表中共有十种数据类型:

单字节局部变量(a single local variable):boolean、byte、char、short、int、float、reference、returnAddress

双字节局部变量(a pair of local variables):long、double

帧中采取索引的方式定位局部变量,双字节变量采用低位作为索引。在局部变量表中,索引为零的位置,值固定为this。

操作数栈

操作数栈用于执行方法中的计算,遵循后进先出原则,且操作数栈的大小在编译期决定。

本地方法栈

本地方法栈与jvm栈的作用非常相似,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。本地方法栈可以是固定大小,也可以动态分配。

JAVA 堆

一个java程序中,java堆由所有线程共享,用于运行时给所有的类实体和数组分配内存。堆中的内存由GC管理,jvm也不指定GC,GC的具体实现由jvm自己选择。堆可以是固定大小也可以动态分配,堆空间可以是不连续的内存片段,但在逻辑上它应该被视为连续的。

方法区也是由各个线程共享的内存区域,用于储存已被虚拟机加载的类类型信息、常量、静态变量、即使编译器编译后的代码缓存等数据。方法区在逻辑上是堆的一部分,这意味着这一部分区域也是可以被进行垃圾回收的,但是jvm规范规定,简单的jvm实现可以选择在垃圾清理时跳过该区域。

方法区的具体位置,以及如何管理编译后的代码,由具体的jvm决定,与java堆和jvm栈相同,方法区可以是固定大小也可以动态分配,方法区在物理上可以是不连续的内存片段,但是在逻辑上应当被视为连续的,且允许用户控制这些区域的初始大小、最大大小以及最小大小等配置。

常量池(Runtime constant pool)是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一个常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的常量池中。

StackOverflowError

当jvm栈为固定大小,而线程需要更大的栈空间时,抛出StackOverflowError

当本地方法栈为固定大小,而线程需要更大的栈空间时,抛出StackOverflowError

OutOfMemoryError

java程序中堆、栈、方法区、常量池、本地方法栈需要扩大内存空间,而jvm剩余的内存空间不足时,抛出OutOfMemoryError

创建线程时,jvm的内存空间不足时,抛出OutOfMemoryError

需要jvm特殊支持的类库(部分)

  • 反射类类库java.lang.reflect
  • 类Class
  • 加载和创建类或接口,例ClassLoader
  • 类或接口链接的初始化
  • 安全性类库java.security和其它类如SecurityManafer
  • 多线程
  • 弱引用,例java.lang.ref

java堆中经常会出现“新生代”、“老年代”、“永久代”等空间结构概念,但是这些区域划分仅仅是java的默认虚拟机hotspot以及部分jvm的特性,《JVM规范》中并没有对堆进行更细致的划分,不少资料上都对此语焉不详,甚至写着“jvm堆内存分为新生代、老年代、永久代…”等容易让人误会的说法。

同时各java规范中仅提供简单且有限的java标准参数: [The java Command]而与jvm相关的参数更是依赖具体的jvm实现,在不同的虚拟机中并不通用。欢迎关注我的公众号:敲代码的老贾,回复“领取”赠送《Java面试》资料,阿里,腾讯,字节,美团,饿了么等大厂