目录
一、运行时数据区
在Java虚拟机中管理的内存包含以上几个运行时数据区, 其中灰色背景的区域是所有线程共享的数据区, 而白色背景的区域是线程隔离的数据区, 下面简要介绍一下各个区域。
1. 程序计数器
一块较小的内存空间,用于存放当前线程正在执行的字节码指令的地址。程序的流程控制都依赖程序计数器完成。由于java虚拟机中的多线程是通过线程轮转、分配处理器执行时间来实现的,所以为了线程切换后能够恢复到正确的执行位置,所以每个线程都需要有独立的程序计数器,所以它是“线程私有”的。
2. Java虚拟机栈
每个方法被执行的时候,java虚拟机会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出入口等信息。一个方法从调用到执行完毕的过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。对于java内存中常提及的“堆”和“栈”来说,“栈“”通常即为虚拟机栈,更多情况下指虚拟机栈中的局部变量表。
局部变量表存放了编译期可知的基本数据类型(boolean, byte, char, short, int, float, long, double)、对象引用(reference类型,句柄 / 指针)和returnAddress类型(指向一条字节码指令的地址)。
3. 本地方法栈
与虚拟机栈作用类似,区别在于:虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机使用其他语言的本地方法服务。一些虚拟机甚至将两者合二为一。
4. Java堆
虚拟机管理内存中最大的一块,用于存放对象实例,在虚拟机启动时创建,线程共享,所有的对象实例都在此分配内存,也是垃圾收集器管理的内存区域。
5. 方法区
线程共享,用于存放被虚拟机加载的类型信息、常量、静态变量。运行时常量池也是方法区的一部分。
6. 直接内存
直接内存并非是虚拟机运行时数据区的一部分。NIO类可以使用Native函数库直接分配堆外内存,并通过存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
二、虚拟机对象
1. 对象的创建
对于java程序来说,创建对象(除数组和Class对象)仅仅使用new关键字即可,对于虚拟机来说,创建对象又是怎样的过程呢?
对虚拟机来说,创建对象大概可以分为一下几个阶段:类加载 → 分配内存 → 初始化 → 设置对象头。
- 类加载
当Java虚拟机遇到一条字节码new指令时, 检查是否已被加载, 若没有则执行相应的类加载过程。 - 分配内存
在执行过类加载后,对象所需内存大小即可完全确定,分配内存的过程可以理解为将一块确定大小的内存块从堆中划分出来。根据堆是否“规整”,分配方式可分为“指针碰撞”和“空闲列表”两种。如果堆中内存是绝对规整的,即已使用的内存分配在一边,未使用的在另一边,中间放着一个指针作为分界点,那仅需要将指针向空闲区域挪动相应的距离即可,这样的分配方式称为“指针碰撞”。
如果堆中的内存不是规整的,已使用的内存和未使用的内存交错在一起,那虚拟机就必须维护一个列表,用于记录空闲内存块。当分配内存时,从该列表中找到足够大的空间分配给对象实例,并更新列表。这样的分配方式称为“空闲列表”。
不论是指针碰撞还是空闲列表,在并发情况下都不是线程安全的,所以在分配内存的时候还需要解决线程安全的问题。解决这个问题有两种可选方案。一种是对分配内存空间的动作进行同步处理,另一种给每个线程在堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),每个线程都在自己的TLAB中给对象分配内存,当TLAB用完了才向堆申请新的缓冲区,这时才需要同步锁定。 - 初始化
虚拟机将分配到的内存空间(除对象头之外)都初始化为零值。如果使用了TLAB,此步骤也可提前至TLAB分配时进行。所以代码中不赋初始值的话,可以获取到该数据类型对应的零值。 - 设置对象头
在对象头中对 对象进行必要的设置:对象的哈希码、对象的GC分代年龄等。
完成以上步骤之后,对虚拟机来说,一个对象产生了,但是对java程序来说,构造函数还未执行,即Class文件中的()方法还没有执行。一般来说,在new之后会紧接着执行()方法对对象进行初始化,这样一个程序员真正需要的对象才被完全构造完成。
2. 对象的内存布局
对象在队中的存储布局可分为:对象头,实例数据,对齐填充。
- 对象头
对象头包括两类信息:一类是用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳等。另一类是类型指针,用于确定该对象是哪个类的实例。此外,如果对象是java数组,对象头中还必须有一块用来记录数组长度的数据。 - 实例数据
对象真正存储的有效信息,即在java程序中定义的内容。 - 对齐填充
非必要,起到占位符的作用。任何对象都必须是8字节的整数倍,如果不是,则通过对齐填充补全。
3. 对象的访问定位
为了使用对象,程序会通过栈上的reference用来操作堆上的具体对象。主流的访问方式有使用句柄和直接指针两种方式。
- 句柄
堆中划分除一块内存作为句柄池,reference中存储的即为对象的句柄地址,句柄中包含了对象的实例数据和类型数据的地址信息。
- 直接指针
reference中存放的就是对象地址。
使用句柄的好处是:当对象被移动时,只会修改句柄中的实例数据指针,而不会修改reference本身。
使用直接指针的好处是:如果只是访问对象本身的话,就不需要多一次间接访问的开销。
三、结合Java程序来看
public class Test {
int a;
int b;
public static void main(String[] args) {
Test test = new Test();
}
}
-
类加载
-
分配内存
-
初始化
-
设置对象头
对Java程序来说,在创建对象中并不关心这一部分。感兴趣可查阅:Java对象头信息 -
调用构造函数
对于示例中的构造函数,即为默认的空参构造器,并不会对堆中对象的属性值进行修改。 -
对象引用
经过以上步骤,程序员获得其期望的对象。
本文根据《深入理解JAVA虚拟机 - JVM高级特性与最佳实践》第三版 以及 个人学习理解 整理而成,如有错漏之处还请各位大佬指出,感谢~