JVM运行时数据区域
- 程序计数器(Program Counter Register)
程序计数器可以看作是当前线程的字节码的行号指示器。该内存区域线程私有。
如果执行的是Java方法,这个计数器记录的是正在执行的指令的地址;如果执行的是Native方法,这个计数器值则为空(Undefined)。
- Java虚拟机栈(Java Virtual Machine Stacks)
Java虚拟机栈是线程私有的。
描述了Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
如果栈深度大于虚拟机所允许的深度,抛出StackOverflowError
。如果动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError
。 - 本地方法栈(Native Method Stack)
与虚拟机栈类似,本地方法栈为Native方法服务。具体的实现由虚拟机厂商自定义。 - Java堆(Java Heap)
Java堆是所有线程共享的内存区域,在虚拟器启动时创建。
此内存区域的唯一目的就是存放对象实例。Java虚拟机规范中规定所有的对象实例都要在堆上分配。然而,现实中并不一定如此。
Java堆是垃圾收集器管理的主要区域。从内存回收角度来看,可细分为新生代和老年代,跟进一步可分为Eden空间、From Survivor空间、To Survivor空间等。
Java虚拟机规范规定,Java堆可以处于物理不连续的内存空间。在实现时,可实现成固定大小的,也可实现成可扩展的,当前主流的虚拟机都是按照可扩展实现的。
如果堆中没有内存,也无法扩展时,抛出OutOfMemoryError
。 - 方法区(Method Area)
方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError
。 - 运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。方法去常量池分为运行时常量池和Class文件常量池。
运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,编译期置入Class文件的常量进入Class文件常量池,运行期间的常量放入运行时常量池中。
常量池无法申请到内存时,抛出OutOfMemoryError
。 - 直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,但被频繁的使用。
Java引入NIO后,可使用Native函数库直接分配堆外内存。这部分内存称为直接内存。
HotSpot虚拟机对象
对象的创建
- 类加载:在常量池中定位类符号的引用,检查类是否被加载、解析和初始化,如果没有,先执行类加载过程。
- 分配内存:对象所需内存的大小在类加载完成后可完全确定。虚拟机通过指针碰撞或空闲列表的方式把一块确定大小的内存从Java堆中划分出来。
指针碰撞(Bump the Pointer):Java堆内存绝对规整的话,分配内存就是把标记已用内存和空闲内存分界线的指针挪动一段距离。
空闲列表(Free List):Java堆内存不规整的话,已用内存和空闲内存相互交错,虚拟机就必须维护一个列表用来记录空闲内存。
分配内存的线程安全问题有两种解决方案,一种是进行同步处理——虚拟机采用CAS+失败重试实现,另一种是先按照线程划分本地线程分配缓冲*Thread Local Allocation Buffer,TLAB),然后线程在各自的TLAB上分配内存,分配TLAB时才需要同步锁定。
- 初始化内存空间:将所有内存空间初始化为零值(不包括对象头),如果使用TLAB,可提前至TLAB分配时进行。这保证了对象实例字段的默认值。
- 设置对象:将对象的基本信息设置在对象头(Object Header)中。
- 初始化对象:调用
<init>
方法,按照程序员的意愿初始化对象。
对象的内存布局
对象在内存中分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:对象头包括两部分信息,一部分用于存储对象的运行时数据,例如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,长度固定位32bit或64bit(32位和64位的虚拟机不同),官方名称为“Mark Word”。另一部分是类型指针,如果是Java数组,对象头中还有一块用于记录数组长度的数据。
并不是所有的虚拟机实现都必须在对象数据上保留类型指针。
- 实例数据:对象真正存储的有效信息,也是代码中定义的各类型的字段内容。父类继承的和子类定义的,都需要记录起来。
- 对齐填充:HotSpot VM规定对象起始地址必须是8字节的整数倍。
对象的访问定位
Java程序需要通过栈上的reference数据操作堆上的具体对象。主流的定位对象有两种方式,句柄访问和直接指针访问。
- 句柄访问:Java堆中划分一块内存作为句柄池,栈中reference存储句柄地址,句柄存储对象实例数据地址和类型数据地址。
- 直接指针访问:栈中reference存储对象地址,对象地址中存放类型数据地址。