一、运行时数据区域
1. 程序计数器
- 它是线程私有的内存。
- 它是 Java 虚拟机规范中唯一一个没有规定 OOM 异常的区域。
- 它看作是当前线程所执行的字节码的行号指示器。字节码解释器就是通过改变这个计数器来选取下一条需要执行的指令。
- 如果线程执行的是 Java 方法,程序计数器记录的就是正在执行的虚拟机字节码的指令地址,如果是本地方法,这个计数器的值为空。
作用:
- 控制分支、循环、跳转、异常处理。
- 控制线程恢复。
2. Java 虚拟机栈
- 它是线程私有的内存,生命周期与线程相同。
- 虚拟机栈是描述 Java 方法执行的线程内存模型:每个方法执行的时候,Java 虚拟机会同步创建一个栈帧,用于存储局部变量、操作数栈、动态连接、方法出口等信息,方法调用到执行完成的过程,对应着一个栈帧在虚拟机中从入栈到出栈的过程。
- 局部变量表:存放基本数据类型、对象引用(指针或句柄)
- 操作数栈:方法执行过程中的中间计算结果保存,也类似一个栈结构。
- 动态连接:由于运行时常量池中的方法是符号引用,动态连接就是将符号引用转换成调用方法的直接引用。
- 方法出口:包括正常出口和异常出口。
- 如果栈帧的深度大于虚拟机栈允许的最大深度,则抛出 StackOverFlowError, 如果虚拟机栈容量可以动态扩展,当栈无法申请到足够的内存,则会抛出 OutOfMemoryError。
3. 本地方法栈
本地方法栈与 Java 虚拟机栈作用类似,区别在于本地方法栈是为本地方法调用服务的,而虚拟机栈是为 Java 方法调用服务的。它们抛出异常的情景也类似。
4. Java 堆
- 堆是 Java 虚拟机管理的最大的一块内存区域。
- 堆内存是所有线程共享的区域。
- 它的目的是存放对象实例和数组。
- 如果堆中没有足够的内存完成实例分配,并且无法再扩展时,会抛出 OOM异常。
5. 方法区
-
方法区是所有线程共享的区域。
-
它用于存储:类型信息、常量、静态变量、即时编译后的代码缓存等数据。
-
如果方法区没法满足新的内存分配需求时,将抛出 OOM 异常。
-
运行时常量池:
- 运行时常量池是方法区的一部分。
- Class 文件除了有类的版本、字段、方法、接口等描述信息,还有常量池表,用于存放编译器生成的字面量和符号引用,这部分数据在类加载后,存放在方法区的运行时常量池。
- 常量池无法再申请到内存时,也会抛出 OOM 异常。
-
字面量:是直接赋值的常量,例如:在Class中 int a = 123, 123 是字面量。
-
符号引用:用来引用常量值,被final修饰,例如:Class 中, final int a = 123, final String b = new String("b") 中,a 和 b 都是符号引用。
-
变量:可变值的量,例如:在Class中 int a = 123, a 是变量。
6. 直接内存
- 直接内存不是虚拟机运行时数据区域的一部分,也不是 JVM 规范中定义的内存区域。
- 它不受 JVM 限制,但是受到本机的物理内存限制,如果内存不够也会 OOM。
- JDK1.4 中的 NIO(new input/output)中通过
DirectByteBuffer直接操作堆外内存。避免了在 Java 堆和 Native 堆中来回复制数据。
二、HotSpot 虚拟机中的对象
1. 对象创建
当虚拟机遇到 new 指令时:
- 首先在常量池中检查,对应的类是否被加载、解析、初始化。如果没有,需要先执行类的加载过程。
- 对新对象分配内存,对象所需内存在类加载完成后就可以完全确定。
- 内存分配的方式:指针碰撞 和 空闲列表,指针碰撞用于规整的内存,空闲列表用于内存不连续规整的情况。
- 对象创建线程安全问题:可以采用 CAS 重试保证。或者为每个线程预分配一块内存。只有预分配内存使用完了,才会加锁再分配内存。
- 内存分配后,将分配到的内存除了对象头外,其他都初始化为零值。这可以保证对象在没有初始化值时,就能被访问到各种类型对应的零值。
- 设置对象头,对象头设置后,对 Java 虚拟机来说,一个新对象已经产生了,但是对于 Java 来说对象创建才开始,因为构造方法还没有被执行。
2. 对象的内存布局
- 对象在堆内存中可分为三个部分:对象头、实例数据、对齐填充。
- 对象头包含两部分数据:
- 对象自身运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据用在 32 位和 64 位虚拟机中分别用 32 个和 64 个比特位,官方称它为:Mark Word。
- 类型指针:指向对象的类型元数据的指针。如果对象是数组:还有一块用于记录数组长度的数据。
- 对象实例数据:存储对象真正有效信息,即我们在代码中定义的各种类型的字段内容。
- 对齐填充:虚拟机管理的内存起始地址必须是 8 字节的整数倍。如果数据没有对齐,就必须填充补全。
3. 对象的访问定位
对象创建后,我们会通过栈上指向对象的引用来访问堆上具体的对象。引入访问堆上数据主要有两种实现方式:
- 句柄访问:Java 堆会划分出一块内存作为句柄池,栈里的对象引用就是句柄地址。句柄中包含了对象的实例数据及地址。
- 直接指针:栈里的对象引用指向对象的地址。
两种方式的区别:
-
句柄访问方式:对象引用稳定。在对象被移动时,对象的引用不会改变,只会改变句柄指向对象的指针。
-
直接指针:少一次间接访问开销。