学习Java虚拟机我们需要先从Java虚拟机运行时的数据区开始学习,了解Java虚拟机在运行时各个数据区域的划分以及各自的职能职责是什么,这将为我们了解Java代码在Java虚拟机中如何加载打下坚实的基础。
一、JVM运行时数据区的组成
Java虚拟机运行数据区主要包括所有线程共享数据区和线程隔离数据区。线程共享数据区指的是所有线程共享的数据区,主要包括方法区和Java堆。线程隔离的数据区指的是本线程私有的数据区,主要包括Java虚拟机栈、本地方法栈和程序计数器。
Java虚拟机运行时数据区
1.方法区
方法区主要存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据
2.Java堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
3.Java虚拟机栈
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储
局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
4.本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
5.程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
6.运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
7.直接内存
非虚拟机运行时数据区的部分
二、HotSpot 虚拟机对象探秘
深入理解HotSpot 虚拟机在Java堆中对象是如何分配、布局和访问的全过程
1.对象如何创建
- 虚拟机遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
- 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(Java堆中的内存如果是规整的就采用“指针碰撞”的方式分配对象的内存空间,Java肚中的内存空间是否规整由其采用的垃圾收集器是否带有压缩整理的功能有关。如果Java队中的内存不连续交错排列,Java虚拟机就会维护一个空闲列表,空闲列表主要记录哪一块内存块是可用的,再给新对象分配内存空间时从列表中寻找一块足够大的空间,更新列表上的记录。Serial、ParNew等带compact过程的收集器,系统采用的是指针碰撞,而CMS这种基于Mark-Sweep算法的收集器,采用空闲列表的形式初始化对象)。
- 前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
- 内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
- 执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。(这时只是完成了虚拟机层面的对象创建,而在Java代码层面对象才刚刚开始,执行完new指令后还需要接着执行init();初始化方法,执行完init()方法后,一个Java对象才算完全的产生出来)
2.对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局主要分为3块区域:对象头、实例数据、和对齐填充。
对象头(Header)
对象头(数组对象除外)包含两部分:
- 第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。
- 第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
实例数据(Instance Data)
程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
对齐填充(Padding)
对齐填充并不是必然需要,主要是占位,保证对象大小是某个字节的整数倍
3.对象的访问定位
使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。
- 通过句柄访问
Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址,而句柄中包含对象实例数据和类型数据各自的具体地址信息。详情见图。
\
- 通过直接指针访问
reference 中直接存储对象地址
比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。虚拟机Hotspot在对象访问的实现上,采用了直接指针访问的方式
三、内存溢出(OutOfMemoryError)异常
内容待填