1、简介
本章主要内容是 运行时数据区、HotSpot虚拟机对象、对象访问定位等。
2、运行时数据区
通过上图,我们可以看到 运行时数据区 包括:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区等。
其中,在 程序计数器、Java虚拟机栈、本地方法栈 这三个区域中每个线程有一个专属的区域,线程和线程之间互不干扰,独立存储,这样的内存区域被称为 线程私有 内存。而 Java堆、方法区 是 所有线程共享的内存区域。
2.1、程序计数器
- 线程私有 的内存
- 是一块儿较小的内存空间,Ta 可以看做是当前线程所执行的字节码的行号指示器。白话讲,就是用来记录当前线程执行到哪里了。
- 如果线程执行的是一个Java方法,此时程序计数器记录的是正在执行的字节码指令的地址;如果是一个 Native 方法,那记录的就是空(Undefined)。
- 该区域是 Java虚拟机规范 中唯一一个没有规定任何 OOM 情况的区域。
这里我们有一个问题:
我们为啥要记录这个?
Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间来实现的。所以,在任何一个时间点上,一个处理器(或者是多核处理器上的一个内核)只会执行一个线程中的字节码指令。假设 T1、T2 两个线程轮流在一个处理器中执行。当 T1 切换到 T2 再由 T2 切换回 T1 的时候,字节码解释器需要知道当时 T1 执行到哪里了,程序计数器此时就派上用场了,因为 Ta 记录了当时 T1 线程当时(T1 切换到 T2 的时候)的状态。
2.2、Java虚拟机栈
- 线程私有 的内存
- 生命周期与线程一致。
- 描述了Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 在Java虚拟机规范中,此区域可能会出现两种异常:
– 如果线程请求的栈深度大于虚拟机允许的深度,将抛出 StackOverflowError 异常。
– 如果虚拟机的栈可以动态扩展,当扩展无法申请到足够的内存,将会抛出 OOM 异常。
我们经常会听到别人说 堆内存(Heap)和 栈内存(Stack)。这里的栈内存(Stack)指的就是 Java 虚拟机栈,再准确点来说,应该是 Java 虚拟机栈中的局部变量表。
2.3、本地方法栈
-
线程私有 的内存
-
与 Java虚拟机栈 的作用类似。它们之间的区别在于:
虚拟机栈是为了 虚拟机执行 Java方法(字节码) 而服务的,而本地方法栈是为了 虚拟机执行 Native方法 而服务的。
-
虚拟机规范并未对本地方法栈的实现有什么强制规定,各虚拟机可以自由的实现 Ta。甚至像 Sun HotSpot 将虚拟机栈和本地方法栈合二为一。
-
与虚拟机栈相同,此区域也会抛出 StackOverflowError 和 OOM。
2.4、Java堆
-
被所有线程共享的一块儿内存区域。
-
该区域是在虚拟机启动的时候创建。
-
唯一的目的就是用来存放对象实例,几乎所有对象实例都在这里分配内存。
这里我们想一个问题:
是所有的对象实例都会在堆上分配内存吗?
答案是:并不是。虽然 Java虚拟机规范 中的描述是:所有的对象实例以及数组都要在堆上分配。但随着 JIT编译器 的发展与逃逸分析技术的逐渐成熟,栈上分配、标量优化技术 将会导致一些微妙的变化,所有对象都分配在堆上,也变的不那么“绝对”了。
-
Java 虚拟机规范的规定,堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的就行,和磁盘空间一样。
-
如果堆中没有内存空间分配给对象实例了,并且也无法再扩展时,将会抛出去 OOM 异常。
2.5、方法区
- 被所有线程共享的一块儿内存区域。
- 用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 在 Java虚拟机规范中,方法区被描述为堆的一个逻辑部分,但 Ta 还有一个别名 Non-Heap(非堆),目的是与Java Heap 区分开来。
- Java虚拟机规范的规定,该区域的内存也可以在物理上是不连续的(和 Java Heap 一样)。
- 在 Java虚拟机规范 中对该区域的限制非常宽松,甚至该区域可以选择不实现垃圾收集。
- 当该区域无法满足内存分配的需求时,将会抛出 OOM 异常。
对于 方法区 与 永生代 的关系,大家可以移步到我的另外一篇Blog(JVM HotSpot 之 内存结构演进过程)去进行了解。
2.6、运行时常量池
- 该区域在 JDK1.7 中作为方法区的一个组成部分。但是在 JDK1.8 中,该区域被迁移到 堆(Heap)中。
- 当类加载后,会将类中的 常量池信息 存放到 运行时常量池 中。
- 在 JDK1.7 中,由于该区域属于方法区的一部分,自然就会受到方法区内存的限制,当常量池无法再申请到内存的时候,则会抛出 OOM 异常。
- 在 JDK1.8 中,由于该区域属于堆的一部分,自然就会受到堆内存的限制,当常量池无法再申请到内存的时候,则会抛出 OOM 异常。
3、HotSpot 虚拟机对象
-
对象的大小在类加载完成的时候就可以确定了。
-
为对象分配内存的方式,主要有以下两种:
名称 描述 指针碰撞(Dump the Pointer) 如果堆中的内存是 绝对规整的,所有用过的内存都放在一边,空闲的内存放在另外一边,中间放一个指针作为分界点的指示器,分配内存的时候就是把指针向空闲空间的方向移动一段儿与对象大小相等的距离。 空闲列表(Free List) 如果堆中的内存 不是规整 的(已使用的内存和空闲的内存交错),此时虚拟机维护一个列表,用来记录哪些内存块是可用的(空闲的),在给对象分配内存的时候从列表中找到一块儿足够大的空间划分给该对象实例,并更新列表上的记录。 -
对象的内存布局,分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Paddding)。对象的大小必须是8字节的整数倍,当 实例数据(Instance Data) 部分没有对齐的时候,就需要 对齐填充(Paddding) 来补全。
3、对象访问定位
Java 程序是通过 栈 中的 reference 数据来操作堆中的具体对象。目前主流有两种方式:句柄 和 指针。
3.1、句柄
通过上图,我们可以总结以下两点:
- 句柄池是在 Java堆 中划出的一个专门区域。
- reference 存放的是句柄的地址,而句柄中存放的是对象实例数据和类型数据的具体地址。
3.2、指针
通过上图,我们可以总结以下两点:
- 需要将 对象类型数据的指针 存放在了 对象实例数据 中。
- reference 直接存放 对象实例数据的指针。
3.3、比较句柄和指针
- 句柄最大的好处就是 reference 中存储的是稳定的句柄地址,当对象被移动的时候,只需要改变句柄中的对象实例数指针,栈中的 reference 无需改变。
- 比起句柄来说,指针的方式最大的好处就是更快速,因为节省了一次指针定位的时间开销。
- Sun HotSpot 采用是指针的方式访问对象。
上一篇:《深入理解JAVA虚拟机(第2版)》—— 学习笔记1
下一篇:《深入理解JAVA虚拟机(第2版)》—— 学习笔记3