您的位置:首页 > 编程语言 > Java开发

Java 虚拟机基础篇(一)—— JVM 内存区域划分

2018-08-26 21:07 267 查看

因为最近在看 「Java 虚拟机」(下称 JVM)相关的书籍与其他相关的课程或博客,所以今天开启一个新的分享系列『 JVM 基础篇』以巩固自己的学习。分享的过程当然伴随着碰撞,若文章中有理解有误之处还望指出,以免影响到其他人。璞玉是需要打磨的,文章亦是如此。这个系列我会持续更新,也会持续改进,所以还请初学者们在阅读此系列文章时以阅读相关书籍为主,以辩证角度看待此系列文章。废话就说这么多吧,下面首先开始对 Java 虚拟机的内存管理机制进行介绍。

JVM 在运行时的数据区域划分

程序计数器

对于程序计数器(Program Counter Register)想必大家都不会陌生,一块较小的内存空间,用来指示当前线程所执行的「字节码」的行号。在处理分支、循环、跳转、异常处理、线程恢复等功能时,字节码解释器通过改变程序计数器的值来选取下一条需要被执行的字节码指令。

字节码:一般由编译器从 .java 文件编译而来,保存在 .class 文件中,是虚拟机可以直接识别并执行的一系列指令

在图内已经标记出,程序计数器是线程隔离的,这个也不难理解,由于 JVM 的多线程是通过线程切换并分配处理器时间实现的,在同一时刻,同一处理器(或多核处理器中的同一内核)只能执行一条线程中的指令,而每条线程在线程切换操作后为了恢复到先前执行的位置,只能各自独立拥有一个拥有独立内存空间的程序计数器,才不会影响到其他线程的执行情况。

此外,在线程执行「本地方法」时,计数器的值为空;当然在线程执行 Java 方法时计数器会记录当前虚拟机正在执行的字节码指令的地址。

本地方法:与虚拟机工作平台相关,对应文件格式也与平台相关,一般通过 C/C++ 实现。当然,Java 是与平台无关的,但当你通过 Java 与本地方法交互,一般也意味着你的代码丧失了跨平台性

Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型。

每个方法在执行的同时会创建一个「栈帧」用于存储「局部变量表」、「操作数栈」、「动态链接」、「方法出口」等信息。每一个方法的执行都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧

虚拟机在单个方法被调用时分配出的一块内存,包括:

  • 局部变量表

    用来保存栈帧对应方法的参数与方法内部的局部变量,容量以变量槽(Variable Slot)为最小单位,通过访问索引的方式进行数据访问。这个地方保存的变量类型包括「基本类型」(boolean、byte、char、short、int、float、long、double)和「对象引用」(reference)以及「returnAddress 类型」。局部变量表占用内存空间大小在编译器就已确定,运行期间不会改变。

    基本类型:64 位长度的 long 和 double 会占用2个 slot,其他基本数据类型只占用一个

    reference 类型:不等同于对象本身,可能是指向对象的指针,也可能是指向代表对象的句柄(指针的指针,这个和对象的访问定位有关,放在下一篇详细说明),或其他与此对象相关的位置

    returnAddress 类型:指向一条字节码指令的地址

  • 操作数栈

    也可以称之为表达式栈,通过标准的入栈、出栈的操作来完成一次数据访问。当一个方法被调用,新的栈帧被创建,此时栈帧中的操作数栈为空,只有在方法执行过程中,才会根据字节码指令对操作数栈中的操作数值执行:(入栈 →) 出栈 → 运算 → 入栈的操作。

  • 动态链接

    存储一个指向运行时常量池的引用,持有这个引用的目的是为了支持运行时的动态链接,详细会在第四篇说明。

  • 方法返回地址

    方法退出时需要的信息,如调用方法指令的下一条指令的地址,或需要压入方法调用者栈帧的操作数栈的返回值。

本地方法栈

与虚拟机栈作用类似,不同之处就是 Java 虚拟机栈用于执行 Java 方法,而本地方法栈用于执行本地方法。

Java 堆

对大多数应用来说,Java 堆是虚拟机所管理内存中最大的一块。Java 堆是被所有线程共享的一个区域,此区域的唯一目的就是存放对象实例,几乎所有的「对象实例」都在这里分配内存,是垃圾收集器管理的主要区域,Java 堆可以是物理不连续的,只要逻辑上是连续的即可。Java 堆中虽然也会因为一些标准而被划分为不同区域,但无论怎么划分存的还是对象实例,因为此篇主要讲述各个内存区域的作用,所以 Java 堆中具体的内存分配与回收详情会在第三篇中详细介绍。在这里先介绍一下对象实例的内存布局。

对象实例

Java 堆中,一个对象被创建后得到的内存空间分为三个区域:「对象头」、「实例数据」、「对齐填充」。

  • 对象头

    包括两部分内容:

    一部分用于存储对象自身的运行时数据,如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向时间戳等。

    另一部分是类型指针,即对象指向它的类「元数据」的指针,通过这部分数据虚拟机可以确定这个实例对应的类。

    类的元数据:定义数据的数据,保存在方法区中。

    此外,要注意如果对象是一个数组,对象头中还需要有一块用于计算数组长度的数据。因为 Java 虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但不能从元数据中确定数组的长度。

  • 实例数据

    这部分是对象真正存储的有效信息,包括从父类继承下来的,子类中定义的。

  • 对齐填充

    并不是必然存在的。这部分仅仅起着占位符的作用。如 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。所以由这部分数据来把每个对象实例所占内存的大小补齐为 8 字节的倍数。

方法区

同样是一个所有线程共享的内存区域。用于存储已被虚拟机加载的「类信息」、「运行时常量池」、「静态变量」、「即时编译器编译后的代码」等数据。类当中与对象实例无关的数据都保存在这里。

类信息:类的元数据信息

静态变量与即时编译器编译后的代码:静态变量与静态代码块因为不随对象不同而改变,所以在这里保存

运行时常量池:class 文件常量池加载到内存中之后的版本,包括字面量与符号引用。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: