Java虚拟机运行时数据区 详解
2.1 概述
本文参考的是周志明的 《深入理解Java虚拟机》第二章 ,为了整理思路,简单记录一下,方便后期查阅。
2.2 运行时数据区域
Java虚拟机在Java程序运行时会将内存区域划分成若干个不同的区域,各自负责不同的职责,这些区域都有各自的用途。
- Java虚拟机运行时数据区分为以下几个部分。
- 方法区、虚拟机栈、本地方法栈、堆、程序计数器,如下图所示:
图片来源于网络如有侵权请私信删除
2.2.1 程序计数器
程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码行号指示器。需要注意以下几点内容:
- 程序计数器是线程私有,各线程之间互不影响。
- 在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。
- 如果正在执行java方法,计数器记录的是正在执行的虚拟机字节码指令地址。
- 如果是native方法,则计数器值为空(native 方法 指得就是Java程序调用了非Java代码,算是一种引入其它语言程序的接口)。
- 程序计数器也是在Java虚拟机规范中唯一没有规定任何
OutOfMemoryError
异常情况的区域。
2.2.2 java虚拟机栈
- 可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置
- Java虚拟机栈是线程私有的,它的生命周期与线程相同。
- 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口等信息。
- 局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型、对象引用、
returnAddress
类型。 - 操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
- 动态连接:每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接
- 方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。
- 在方法运行期间不会改变局部变量表的大小。主要存放了编译期可知的各种基本数据类型、对象引用 (reference类型)、returnAddress类型)。
java虚拟机栈,规定了两种异常状况:
- 如果线程请求的深度大于虚拟机所允许的深度,将抛出
StackOverflowError
异常。 - 如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出
OutOfMemoryError
异常。
2.2.3 本地方法栈
- 可通过参数 栈容量可由
-Xss
设置
- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。
- 本地方法栈则是为虚拟机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
2.2.4 java堆
- 可通过参数
-Xms 初始堆大小
和-Xmx 最大堆大小
-Xmn 新生代` 设置
- Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建。
- Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里。
-
Java堆为了便于更好的回收和分配内存,可以细分为,新生代和老年代
**再细致一点的有Eden空间、From Survivor空间、To Survivor区**。
- 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1:1。
- 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- Survivor空间等Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可(就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的)。
- 据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出
OutOfMemoryError
异常。
2.2.5 方法区
- 可通过参数
-XX:MaxPermSize
设置
- 线程共享内存区域,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent Generation)。
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
- 如何实现方法区,属于虚拟机的实现细节,不受虚拟机规范约束。
- 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。
- 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
- 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池。
2.2.6 运行时常量池
- 可通过参数
-XX:PermSize
和-XX:MaxPermSize
设置 - 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。
- 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
- 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。
- 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。
- 字面量:文本字符串、声明为final的常量值等;。
- 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
- JDK1.6之前字符串常量池位于方法区之中。
- JDK1.7字符串常量池已经被挪到堆之中。
2.2.7 直接内存
- 可通过
-XX:MaxDirectMemorySize
指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样。 - 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致
OutOfMemoryError
异常出现。
2.3 hotspot虚拟机对象探秘
2.3.1 对象的创建
- 主要探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程
- 虚拟机遇到new指令时
- 首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查引用代表的类是否已被加载、解析和初始化过。如果没有,则执行类加载过程(第7章 虚拟机类加载机制)。
- 加载检查通过后,分配内存(内存在类加载完成后便可完全确定)。
- 内存分配完成后,虚拟机对对象进行必要的设置,如对象是哪个类的实例、如何找到类的元数据信息等(都放在对象的对象头中)。
- 从虚拟机角度看,一个新的对象产生了,但从java程序视角看,对象创建才刚刚开始,因为<init>方法还没有执行,,所有字段为零。执行new指令之后会接着执行<init>方法(构造方法),进行初始化,这样一个真正可用的对象才算完成产生。
2.3.2 对象的内存布局
对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充
对象头包含两部分(Header)
- 存储对象自身的 运行时数据,如哈希码、GC分代年龄等。长度在32位和64位的虚拟机中,分别为32bit、 64bit,官方称它为“Mark Word”。
- 类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
注:如果对象是一个java数组,对象头中还必须有一块记录数据长度的数据
实例数据(InstanceData)
- 对象真正存储的有用信息,也是程序中定义的各种类型的字段内容。
对齐填充(Padding)
- 由于HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,通俗的说,就是对象大小必须是8字节的整数倍。对象头正好是8字节的倍数。当实例数据部分没有对齐时,需要通过对齐填充来补全。
2.3.3 对象的访问定位
- Java程序通过栈上的reference数据来操作堆上的具体对象。
- 不同虚拟机实现的对象访问方式会有所不同,目前主流的访问方式有两种:使用句柄和直接指针。
- 使用句柄 是间接访问,优点是reference中存储的是稳定的句柄地址,对象移动时只会改变句柄中的实例数据指针。
- 使用直接指针 是直接访问,优点就是速度快。
最后上一张本章结构图
图片来源于网络如有侵权请私信删除