1. 运行时数据区域
Java虚拟机在执行Java程序时会将它管理的区域划分为若干个数据区域,如下图所示:
1.1 方法区
是所有线程共享的内存区域,用来存储已经被虚拟机加载的类信息、 常量、静态变量、即时编译器编译后的代码等数据。为了与堆区分开来也被称为“非堆”。当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError:PermGen space异常。常用配置参数有:-XX:PermSize:初始方法区大小,-XX:MaxPermSize:最大方法区大小
1.2 堆
是所有线程共享的,也是Java虚拟机管理的最大的一块内存区域,虚拟机启动时就会创建。堆用来存放对象实例,最开始是所有对象和数组都要堆上分配,而随着JIT编译器的发展以及逃逸分析的逐渐成熟,栈上分配、标量替换等技术导致了现在并不是所有的对象都必须在堆上进行分配。Java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”,垃圾回收相关细节后续文章会详细介绍。当堆中没有足够的内存来进行实例分配并且堆无法继续扩展时,将会抛出OutOfMemoryError:Java heap space异常。常用配置参数有:-Xms:初始堆内存大小,-Xmx:最大堆内存大小,-Xmn:年轻代(新生代)内存大小。
1.3 虚拟机栈
是线程私有的,用来描述Java方法执行的内存模形,每个方法执行的时候都会创建一个栈帧,方法的调用到完成的过程对应着栈帧在虚拟机栈中入栈到出栈的过程。栈帧会在后续文章详细介绍。当线程请求的栈深度大于虚拟机允许的最大深度会抛出StackOverFlowError异常;如果虚拟机栈支持动态扩展,但扩展时依然无法申请到足够的内存时,将会抛出OutOfMemoryError异常。常用配置参数有: -Xss: 栈内存大小
1.4 本地方法栈
本地方法栈与虚拟机栈的作用是相同的,不同的是虚拟机栈的作用对象是Java方法,而本地方法栈的作用对象是Native方法。本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。
1.5 程序计数器
是线程私有的一块较小的内存空间,用来记录当前线程所执行的字节码的行号。如果线程执行的是Java方法,那么计数器记录的就是字节码指令的地址吗;如果线程执行的是Native方法,那么计数器的值为空。此区域是唯一一个不会抛出OutOfMemoryError异常的区域。
2. HotSpot对象详解
2.1 对象的创建过程
在HotSpot虚拟机中,对象的创建过程如下图所示:
2.1.1 类加载检查
当虚拟机收到一条new指令时,会检查该指令的参数能否在常量池中定位到某个类的符号引用并检查该符号引用对应的类是否执行过类加载过程,如果没有则会先执行类加载,类加载过程会在后续文章详解。如果执行过则会进行分配内存操作。
2.1.2 分配内存
对象进行完类加载过程后所需的内存大小就能够确定下来(通过对象头中的类型指针找到对象的元数据并确定对象大小),因此分配内存的过程其实就是在堆中划分出一块确定大小的内存。而划分内存区域的方法根据堆内存是否规整(是否规整由使用的垃圾收集器是否具有压缩整理功能来决定)可以分为指针碰撞和空闲列表两种分配方式。由于对象创建在虚拟机中是一种很频繁的行为,所以分配内存也可能出现并发问题,解决并发问题有两种方案:对分配内存空间的动作进行同步处理(CAS+失败重试)或者把内存分配的动作根据不同的线程划分到不同的空间中进行(TLAB)。
2.1.2.1 分配内存的方式
- 指针碰撞
如果堆上空间是规整的,即所有用过的内存在一边,空闲的内存在一边中间使用指针当作分界点,分配内存就只是把这个指针向空闲内存方向移动对象大小的距离,这种分配方式就称为指针碰撞。
- 空闲列表
如果堆上空间不是规整的,即使用过的内存和空闲内存混在一次,虚拟机此时就需要维护一个列表来记录哪些内存区域是空闲的,分配内存时在列表中选定一块大小足够的内存区域分配给对象并更新列表,这种分配方式就称为空闲列表。
2.1.2.2 解决分配内存并发问题的方案
- CAS + 失败重试
保证更新操作的原子性。
- TLAB
每个线程在堆中预先分配一小块内存区域(本地线程分配缓冲,简称TLAB),需要分配内存的线程会在该线程的TLAB上进行分配,当TLAB使用完并分配新的TLAB时才进行同步锁定。通过-XX:+/-UseTLAB来控制虚拟机是否使用TLAB。
2.1.3 初始化
虚拟机将分配到的内存空间初始化为零值(不包括对象头),如果启用了TLAB初始化也可以提前到TLAB分配时进行。初始化保证了对象的实例变量可以不经过赋初始值就可以直接使用,程序可以访问到这些字段对应数据类型的零值。
2.1.4 设置对象头
将对象的一些必要设置信息存入对象的对象头中,对象头详细内容参照下文2.2.1。
2.1.5 执行()方法
将对象按照程序员的设置进行初始化。
2.2 对象的内存布局
在HotSpot虚拟机中,对象在内存中的布局分为:对象头、实例数据和对齐填充三个区域。
2.2.1 对象头
对象头一般分为Mark Word和类型指针两个部分,如果对象是Java数组,那么对象头中还有一块用来记录数组长度的部分。
2.2.1.1 Mark Word
Mark Word用来存储运行时对象的自身数据,在32位虚拟机中的数据长度为32bit,在未开启指针压缩的64位虚拟机中的数据长度为64bit。具体存储内容如下图所示:
2.2.1.2 类型指针
类型指针是指向它的类元数据的指针,虚拟机可以通过这个指针来确定对象是哪个类的实例。
2.2.1.3 记录数组大小部分
如果对象是数组,那么对象头中还会有该部分来记录数组的长度。因为虚拟机能够通过普通Java对象的元数据信息来确定对象的大小,但是无法通过数组对象的元数据信息来确定数组的长度。
2.2.2 实例数据
存储对象中成员变量内容。
2.2.3 对齐填充
不是必然存在的,为了保证对象的大小是8字节的整倍数。当对象头以及实例数据部分的大小不是8字节的整倍数时才会有对齐填充部分,该部分没有其他含义仅仅起了占位符的作用。
2.3 对象的访问定位
Java程序通过栈上的reference数据来操作堆上的对象实例。reference类型只是一个指向对象的引用,因此如何通过这个reference数据来访问对象实例是由虚拟机来实现的,目前主流的访问方式有两种:使用句柄和使用直接指针。
2.3.1 句柄
使用句柄访问时,堆中会划分出一块内存当作句柄池,reference数据存储的是对象在句柄池中的地址,对象在句柄池中的句柄包含了指向对象实例数据(堆中)的指针以及指向对象类型数据(方法区中)的指针。该方法的优势是reference数据比较稳定,当对象被移动时(垃圾收集过程中会经常移动对象)不需要改变reference的数据只需要修改句柄中的实例对象指针既可。
2.3.2 直接指针
使用直接指针访问,堆中的对象实例数据中会存储指向对象类型数据(方法区中)的指针,reference数据存储的时对象在堆中的对象地址。该方法的优势就是访问速度快,较使用句柄的方式减少了一次指针定位操作。