1、JVM运行时数据区域:
(1) 程序计数器:
内存空间小,线程私有。
字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。
(2) 虚拟机栈:
线程私有,生命周期与线程相同。
记录Java方法执行的过程:
方法执行——>创建栈帧(存储局部变量表、操作数栈、动态链接、方法出口)入栈——>执行完成(出栈)
局部变量表:存放基本数据类型(64位占2个局部变量空间,32位占1个)、对象引用和returnAddress(指向字节码指令的地址)类型。所需要内存编译时期完成分配,方法运行期间局部变量表大小不变。通过索引方式访问。
(3) 本地方法栈:
虚拟机栈为虚拟机执行Java方法,本地方法栈使用Native方法。
对本地方法栈中方法使用语言、使用方式和数据结构没有强制的规定。
(4) 堆:
线程共享。
虚拟机启动时创建。存放对象实例。GC堆。
从内存回收的角度,Java堆可以分为新生代与老生代。这种划分的方式,是为了更好的回收内存(老生代内存会被优先回收)。
堆中可能划分出多个线程私有的分配缓冲区,为了更好的分配和回收内存。逻辑连续内存可以不连续。堆中无内存或者无法扩展时候OutOfMemoryError。
(5) 方法区:
线程共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。较少出现GC,内存回收的主要目标是针对常量池的回收和对类型的卸载。
运行时常量池:存放编译期生成的各种字面量和符号引用,运行期间也可能将新的常量放入池中(动态性)。
同样会存在OutOfMemoryError异常。(OOM)
2、JVM对象的创建
(1) 类加载检查
当JVM遇到一条new的指令时,首先去检查这个指令是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经加载、解析和初始化过。如果没有,那么必须先执行类加载工作。
(2) 为对象分配内存
把一块确定大小的内存从Java堆中划分出来(大小由类去确定)。
两种分配方式:
① 堆空间规整——指针用来划分已用过和空闲的内存——移动指针——指针碰撞。
② 堆空间零散——已用内存和空闲内存交错——维护一个记录可用内存块的列表(空闲列表)——从列表中找到一块足够大的空间。(容易产生内存碎片)
实际中采用哪种分配方式是由虚拟机采用的垃圾收集算法决定的,主要取决于垃圾收集器是否带有压缩整理功能。
在使用Serial,ParNew等待Compact过程的收集器时,系统采用指针碰撞。
使用CMS这种基于Mark-Sweep算法的收集器时,采用空闲列表。
保证对象创建的线程安全性:
① 对分配内存空间的动作进行同步处理。采用CAS配上失败重试的方式保证更新操作的原子性。
② 把内存分配的动作按照线程划分在不同的空间之中进行。每个线程预先分配一块本地线程分配缓冲(TLAB),只有TLAB用完并分配新的TLAB时才需要同步锁定。
(3) 内存空间初始化
将分配到的内存空间初始化为零值。保证对象的实例字段在代码中可以不赋初值就可以使用。
(4) 设置对象头
对象所属的类、类的元数据信息、对象的哈希码、对象的GC分代年龄。
(5) 执行
3、 对象的内存布局
对象头+实例数据+对齐填充
(1) 对象头:存储对象自身的运行时数据+类元类型的指针(确定对象是哪个类的实例)。
Java数组对象还要有记录数组长度的数据。JVM可以通过普通Java对象的元数据信息确定Java对象的大小,但是无法从数组对象的元数据中确定数组的大小。
(2) 实例数据:存储代码中定义的各种类型的字段内容。
(3) 对齐填充:对象的大小必须是8字节的整数倍。
4、 对象的访问定位
Java程序通过栈上的引用数据操作堆上的具体对象。具体访问方式取决于虚拟机,主流有:
(1) 使用句柄:(对象被移动时候只需要调整句柄中的指针,reference本身不需要修改)
reference—>堆中的句柄池—>堆中的实例池(对象实例数据)/方法区(对象类型数据)
(2) 直接指针:(速度快)
reference—>对象地址