运行时数据区域
从设计角度看:栈代表了处理逻辑,堆代表了数据
程序计数器
PC(Program Counter Register)
- 线程私有
- 是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器;在虚拟机的概念模型里(尽是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 由于Java虚拟机的多线程时通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行未知,每条线程都需要一个独立的PC,个线程之间的PC互不影响,独立存储,称这类内存区域为“线程私有”的内存;
- 如果线程正在执的是一个Java方法,该PC记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是Nativ方法,该PC值为空(Undefined)。此内存区域是唯一一个在JVM规范中没有规定任何OutOfMerroryError情况的区域;
Java虚拟机栈(Stack)
- 线程私有;用于方法执行时数据存储;基础单位是栈帧;
- 运行时栈帧结构
本地方法栈(Native Method Stack)
本地方法就是一个java调用非java代码的接口;一个Native Method是这样一个方法:该方法的实现由非java语言实现;
- 与虚拟机栈类似,不同是虚拟机栈为虚拟机指定Java方法(即字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务;
- 虚拟机规范中对该栈所使用的语言、方式及数据结构没有强制规定,可以自由实现,有时也会将其与虚拟机栈合并;
java堆(Heap)
- 所有线程共享;
- 在启动时创建,唯一目的是用于存放对象实例,几乎所有(随着发展,并不能保证所有对象,所以是“几乎”)的对象实例都在这里分配内存;
- 堆是虚拟机管理的内存中最大一块,也是垃圾收集器管理的主要区域所以也称为“GC堆”(Garbage Collected Heap)
方法区(Method Area或Non-Heap)
- 线程共享;
- 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
- 在规范中将其描述为堆的一个逻辑部分;
- 该区域的内存回收主要是针对常量池的回收的对类型的卸载;
运行常量池(Runtime Constant Pool)
- 方法区的一部分;
- Class文件中有一项信息是常量池信息(用于存放编译期生成的各种字面量和符号引用),这部分内容将在类加载后存放在方法区的运行时常量池中;
- 运行期也可将新的常量放入池中;
- 存放的是字符串常量和基本类型常量(final修饰的);
直接内存
- 不是虚拟机运行时数据区的一部分,也不是规范中定义的内存区域;
- 在JDK1.4中新加入了NIO(New I/O)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的医用进行操作,这样能在一些场景中显著的提高性能,因为避免了在Java堆和Native堆中来回复制数据;
HotSpot虚拟机对象探秘
对象的创建
虚拟机检测到一条new指令后,就会触发创建对象动作,流程基本如下:
-
类加载检查
1. new指令的参数是否能在常量池中定位到一个类的符号引用; 2. 该符号引用所代表的类是否已被加载、解析和初始化过;
-
分配内存
-
内存分配方式
这时又分两种情况:
- Java堆中的内存是绝对规整时(即用过的放在一边,没用过的放在另一边,中间防止一个指针作为分界点):使用“指针碰撞”,即将分界指针向空闲边挪动一段与大小相等的距离;
- 不规整时:使用“空闲列表”,即虚拟机必须维护一个用于记录可用的内存块的列表,在分配时从列表中查找一块足够大的孔家分配给对象;
采用哪种方式由内存是否规整决定,是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial,ParNew等带有Compact过程的收集器时采用的是指针碰撞;使用CMS这种基于Mark-Sweep算法的收集器时采用的是空闲列表;
-
原子性
比如正在对对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;为此有两种解决方案:
-
同步
对内存分配空间的动作进行同步处理,即虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
-
缓冲
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有TLAB用完才分配新的TLAB,才需要同步锁定。虚拟机是否启用TLAB,可以通过-XX:+/-UserTLAB来设定;
-
-
-
初始化
内存分配完之后,虚拟机需要将分配到的内存空间都初始化为数据类型对应的零值(不包括对象头)。如果使用TLAB,这一过程可以提前至TLAB分配时进行,这一步操作保证了实例字段在代码中不可以赋初始值就可以直接使用;
-
设置对象头
虚拟机对对象进行必要的设置,比如对象所属类,如何找到元数据信息,对象的哈希码等;这些信息都在对象头中;
-
实例化
此时,一个新对象已产生,但所有字段均为零;一般来说(有字节码中是否跟随invokespecial指令所决定),执行完new指令后接着执行init方法,来按照程序的意愿进行初始化,(可以理解为实例化,就是将对象中的变量赋值)。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可分为三块:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding);
对象头
包括2部分;
-
Mark Word
用于存储对象自身的运行时数据,如哈希码等;数据长度与虚拟机位数一致(32位的虚拟机就是32位长);
对象需要存储的运行时数据很多,其实已经超出了32或64位,但对象头信息 本身是与对象自身定义的数据无关的一个额外存储,所以该部分被设计为非固定数据结构的会根据对象状态复用自身的存储空间,其实就是根据对象的状态存储不同的内容;
标志位 | 状态 | 存储内容 |
---|---|---|
01 | 未锁定 | 哈希码,分代年龄 |
00 | 轻量级锁定 | 指向所记录的指针 |
10 | 膨胀(重量级锁定) | 指向重量级锁的指针 |
11 | GC标记 | 空,不需要记录信息 |
01 | 可偏向 | 偏向线程ID,偏向时间戳,分代年龄 |
-
类型指针
即对象指向它的类元数据的指针,虚拟机通过该指针来确定这个对象所属的类。
并不是所有的虚拟机实现都必须在对象数据上保留该指针,也就是说参照对象的元数据信息并不一定要经过对象本身;
如果对象是数组,那在对象头中还必须有一块用于记录数组长度的数据;
实例数据
对象真正存储的有效信息,即代码中所定义的字段值,包括从父类继承的。
其存储顺序受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响;HotSpot的默认策略是longs/doubles, ints, short/char,byte/boolean, oop(Ordinary Obejct Pointer)。即相同宽度的字段分配到一起。
对齐填充
当实例数据部分没有对齐时,需要补齐,因为要去对象起始地址必须是8字节的整数倍数。