前言:系列文章来自于本人学习《深入理解Java虚拟机》笔记,其中的小章节名称严格对应于原书,方便大家对应到书中去详细学习,同时缩略了一些章节,例如第一章、第六章等,但是不妨碍学习。
Java 与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
1.定义
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
2.作用
在 Java 虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空( Undefined)。此内存区域是唯一一个在《Java 虚拟机规范》中没有规定任何OutOfMemoryError 情况的区域。
1.定义
虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候, Java 虚拟机都会同步创建一个栈帧( Stack Frame)用于存储局部变量表**、操作数栈、动态连接、方法出口等信息。
经常有人把 Java 内存区域笼统地划分为堆内存( Heap)和栈内存( Stack),其中, “堆”在稍后笔者会专门讲述,而“栈”通常就是指这里讲的虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。
2.局部变量表
存放内容:
这些数据类型在局部变量表中的存储空间以**局部变量槽( Slot)**来表示,其中 64 位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小,这里说的“大小”是指变量槽的数量。
3.异常情况
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。
1.定义
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
2.目标
针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。
3.异常
根据《Java 虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
1.定义
运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表( Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。(注意这些是编译器产生的)
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性, Java 语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern()方法。
2.异常
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。
这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
1.加载类,第七章详细说明
2.为新生对象分配堆内存
需要考虑的问题:对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。
方案:
一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用 CAS 配上失败重试的方式保证更新操作的原子性;
另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
虚拟机是否使用 TLAB,可以通过-XX: +/-UseTLAB 参数来设定。
3.内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了 TLAB 的话,这一项工作也可以提前至 TLAB 分配时顺便进行。
4.Java 虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java 程序的视角看来, 对象创建才刚刚开始——构造函数,即 Class 文件中的
‘划分为三部分:
1.对象头
包含两类信息:
2.实例数据
实例数据部分是对象真正存储的有效信息, 即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数( -XX: FieldsAllocationStyle 参数)和字段在 Java 源码中定义顺序的影响。
3.对齐补充
这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
创建对象自然是为了后续使用该对象,我们的 Java 程序会通过栈上的 reference 数据来操作堆上的具体对象。
如何去定位和访问到堆中对象的具体位置,主流的访问方式主要有使用句柄和直接指针两种: