目录:
一、Java 运行时数据区域
1、程序计数器
2、Java 虚拟机栈
3、本地方法栈
4、Java 堆
5、方法区
二、Java 虚拟机对象的探秘
1、对象的创建
2、对象的内存布局
3、对象的访问定位
一、Java 运行时数据区域
根据《Java 虚拟机规范(Java SE 7 版)》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域(图片来自网络)。 Java虚拟机运行时数据区Java 虚拟机在执行 Java 程序的过程中会把所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
1、程序计数器
①、什么是程序计数器:
- 程序计数器是一块较小的内存,它可以看作是当前线程所执行的字节码文件(class)的行号指示器。
②、程序计数器的作用:
- 在虚拟机概念模型里,字节码解析器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
③、程序计数器的特性:
- Java 虚拟机中的多线程是通过线程轮流切换并分配处理器(cpu)执行时间的方式来实现的(即时间片轮转)。对于单核处理器来说,在任何一个确定的时间,处理器都只会执行一条线程中的指令。因此,为了线程被切换后恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,这样各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。因此它的生命周期和线程相同。
- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址(位置,对应字节码文件中的具体行号);如果正在执行的是一个 native 方法,这个计数器值则为空。
- 此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2、Java 虚拟机栈
与程序计数器一样,Java 虚拟机也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
①、栈帧中局部变量表的作用
- 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、 对象的引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他于此对象相关的位置) 和 returnAddress 类型(执行了一条字节码指令的地址)。
- 局部变量表是以变量槽(Variable Slot,下称 Slot)为最小存储单位,每一个 Slot 都可以存储一个 32 位以内的数据类型,Java 中 32 位以内的数据类型有 boolean、byte、short、char、int、float、reference 和 returnAddress 8 种类型,那么 64 位的 long 和 double 类型是怎样存储呢?对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。
- 在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是 实例方法(即非静态方法),那局部变量表的第 0 个索引的 Slot 默认是用于传递方法所属对象实例的引用, 在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从 1 开始 的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。
②、Java 虚拟机栈的两种异常
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
- 如果虚拟机栈可以动态扩展(当前大部分都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
3、本地方法栈
虚拟机栈和本地方法栈的区别?虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。与虚拟机一样,本地方法区也会抛出 StackOverflowError 和OutOfMemoryError 异常。
4、Java 堆
①、Java堆是Java虚拟机所管理的内存中最大的一块。
②、在虚拟机启动时创建。
③、Java堆内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(对象实例和数组)。
④、如果在堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出 OutOfMemoryError 异常。
5、方法区
①、方法区的作用:
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译期编译后的代码等数据
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
②、方法区中运行时常量池的作用:
- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池, 用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。 字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理 方法的概念,包括了三类常量:类和接口的全限定名(如com/np/model/Student)、字段的名称和描述符 以及 方法的名称和描述符。
- 运行时常量池相对于Class文件常量池的另一重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生, 运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
- 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制, 当常量无法在申请到内存时会抛出OutOfMemoryError异常。
二、Java 虚拟机对象的探秘
介绍完 Java 虚拟机的运行时数据区之后,我们大致知道了虚拟机内存的概况,我们了解了内存中放了些什么后,也许就会想更进一步了解这些虚拟机内存中的数据的其他细节,譬如它们是如何创建的、如何布局的以及如何访问的。对于这样设计细节的问题,必须讨论一下具体的虚拟机和集中在某一个内存区域上才有意义。基于实用优先的原则,这里以常用的内存区域 Java 堆为例,深入探讨 HotSpot 虚拟机在 Java 堆中堆想分配、布局和访问的过程。
1、对象的创建
Java 是一门面向对象的编程语言,在 Java 程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个 new 关键字而已,而在虚拟机中,对象(这里讨论的对象限于普通对象,不包括数组和 Class 对象等)的创建又是怎样一个过程呢?
对象创建的过程如下:
1、虚拟机遇到一个 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2、在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
3、内存分配完成之后,虚拟机需要将分配到的内存空间都初始化零值(不包括对象头)。这一步操作保证了对象的 实例字段 在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对象的零值。
4、接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄信息。这些信息存放在对象的对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。关于对象头的具体内容,将在下面讲解。
5、在上面的工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象的创建才刚刚开始 —— <init> 方法还没有执行,所有的字段都还为零值。所以,一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照码农的意愿进行初始化,这样一个真正可用的对象才算完成产生出来。
2、对象的内存分配
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
①、HotSpot 虚拟机的 对象头 包括两部分信息:
- 第一部分用于存储对象自身的 运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机中分别为 32bit 和 64bit,官方称它为“Mark Word”。例如,在 32 位的 HotSpot 虚拟机中如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0。
- 对象头的另一部分是 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。但并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并一定要经过对象本身。另外,如果对象是一个数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小。
②、实例数据存储布局
- 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
③、对齐填充存储布局
- 第三部分对齐填充并不是必然存在的,也没有特别的含义,他仅仅起着占位符的作用。对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
3、对象的方位定位
建立对象就是为了使用对象,我们的 Java 程序需要通过栈上的 reference 数据使用堆上的具体对象。由于 reference 类型在 Java 虚拟机规范中规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。
①、目前主流的访问方式有使用 句柄 和 直接指针 两种。
-
如果使用句柄访问的话,那么 Java 堆中将会划出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图所示。
通过句柄访问对象 - 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而 reference 中存储的直接就是对象地址,如下图所示。 通过直接指针访问对象
②、两种对象访问方式的优势
- 使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
- 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,对于对象的访问在 Java 中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。
最后,需要说明的是,该文章主要是对《深入理解 Java 虚拟机》一书中讲到的 Java 内存区域模块的总结,如有不妥之处,拍砖请轻拍。
网友评论