深入理解java虚拟机系列第三版读后小记:(二)jvm中的java对象
前言
上文中笔者仔细讲述了jvm内存区域的划分和其职责以及各内存可能存在的溢出报错方式,本文将探究一个java对象在jvm中是如何存在和被分配的。
java对象jvm中创建的过程概览
创建一个java对象通常两种方式,使用new 关键字或者反射。那么创建对象时的过程是什么,大概分为以下几步:
- jvm 检查到new指令时,检查这个指令的参数是否能在常量池定位打一个类的符号引用,并检查这个类的信息是否被加载,解析和初始化过,没有的话就调用类加载器进行初始化,类加载器不是本讲重点。
- 类加载检查和完成后,jvm就知道类对象所需的内存,就会为其分配内存。
- 内存分配完成后,jvm会对分配好的内存空间进行初始化零值,这个初始化不是通常我们所说的对象构造函数初始化。
- 初始化后,就开始对对象头进行一些必要设置,赋值对象的一些元数据,如哪个类的实例,gc的分代年龄,包括同步时的锁信息等等。
- 真正的执行构造函数初始化,之前的初始化只是给属性赋零值,这次初始化相当于init,按照程序员的意愿去进行初始化和赋值。
分配内存的两种方式
jvm中java对象的创建过程中,提到了类加载检查后,就会对其分配内存空间,分配内存空间主要有两种方式
指针碰撞
假设jvm内存空间是工整的,所有被使用的对象在一边,没有被分配的在另一边,中间放一个指针当显示器,那么分配内存就是,将指针向空闲的区域移动一块与对象大小相等的距离。
如图,假设内存为工整的区域,一边是划分的一边是空闲
如图,指针偏移来分配,就这就是指针碰撞进行分配空间
空闲列表
如果jvm内存不规整咋办,已分配和未分配的混在一起了,那就无法指针碰撞了,这时候就需要jvm维护一个列表去记录哪些空间是可用的,分配对象内存的时候,就去列表查符合条件的内存块去分配。然后更新表上的记录,这就是空闲列表的分配方式。
分配内存两种方式的选择取决于java堆是否规整,而java堆是否规整取绝于其垃圾收集器是否拥有空间压缩整理能力,垃圾收集器是否有压缩和整理能力取决于它的回收算法。
分配内存的并发安全方案
java是支持多线程的,而且对象在jvm创建时非常频繁的,所以即使修改一个指针的指向位置,并发下也是不安全的。针对安全问题有两种解决方案:
- 采用同步处理,jvm实际上用cas来保证原子性
- 本地线程分配缓冲TLAB(Thread Local Allocation
Buffer),即每个线程在堆内预先分配好各自的一小块空间,这样的话各个线程就相互独立,分配内存先在TLAB上分配,如果本地缓冲区用完了,那就进行同步锁定.,来分配新的缓冲区。
java对象在的内存布局
在HotSpot的jvm中,java对象的在堆内存中的布局主要分三个部分:对象头,实例数据,对齐填充
- 对象头
对象头存储两部分信息,第一部分叫make word。用来存储
对象自身运行的数据,如hashcode,gc的分代年龄,锁状态标志,线程持有的锁,偏向线程id等。
这块空间的大小根据虚拟机的位数有关,32位上的jvm里make word的长度位32位,64位的jvm里,不开启指针压缩的情况下,make word的长度位64位。
第二部分为类型指针,即对象指向它的类型的元数据指针,jvm通过指针来确定该对象时哪个类的实例。 - 实例数据
- 顾名思义,存储实例的具体信息
- 对齐填充,不是必须存在的,也没啥特殊意义,只是充当占位符作用,因为hotspot的jvm规定内存管理系统下的对象必须以8字节的整数倍,所以这个对齐填充,就是当对象不是8字节的整数倍,给它补齐让其成为8的整数倍。比如说一个对象,其对象头加实例数据为14字节,这时候就对齐填充两字节,其对象大小为16字节。
java对象的访问定位
java对象创建会在栈上创建一个引用,通过引用来访问堆上的具体对象。那这个引用访问具体对象有两种方式
句柄访问
所谓句柄访问,就是堆中可能划出一块区域作为句柄池,池内存储着实例池内对象的实例信息的地址和方法区内对象的类型数据,而栈上的引用则存储着池内具体对象的句柄地址。如图
直接指针访问
直接指针访问,栈上的引用存储的不是在对象对应的句柄地址,而是堆上的java对象地址,这样也无需堆上划分一块额外的内存去充当句柄池。这样的话,jvm就需要考虑对象的内存布局,如何处理方法区的类型信息,就需要额外一次访问。如图
两种访问方式的优缺点也很明显。句柄访问需要占用额外的一块空间,优点是在对象被移动的时候(gc时对象移动非常普遍),只需改变句柄池的指向对象的地址即可,无需改动栈内的引用。
直接指针的优点也很明显,就是更快,少了一次指针定位的开销。缺点移动对象也需要修改栈内的对象引用,无法直接说两种方式谁优谁劣,还是看各自需求吧,hotsport就采用第二种,每次节省一次指针定位的开销,极少成多。而其他软件和框架使用句柄访问也很常见。
总结
本文主要介绍了java在jvm中的创建和内存上的分配方式。