HotSpot虚拟机对象探秘(对象创建,对象内存布局,对象访问定位)
以常用的HotSpot虚拟机和JAVA内存区域堆为例,探讨对象的创建,对象的内存布局以及对象的访问定位
一.对象的创建
1)类加载:虚拟机遇到一条new指令时,先检测这个指令的参数能否在常量池中定位到一个类的符号的引用,并检查这个符号代表的类否收以及被加载,解析和初始化,如果没有那么就必须执行相应的类加载过程
2)分配内存:如果java堆的内存是绝对规整的,那么采用指针碰撞的方式分配内存,即所有空闲的内存放一边,不空闲的内存放一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把那个指针向空闲的空间那边挪动一段与对象大小相等的距离,如果java堆的内存是不规整的话,那么我们必须维护一个空闲列表,记录哪些内存块是可用的,在分配的时候从列表中找到一个足够大的空间划分给对象实例,并更新列表上的记录
3)初始化:都初始化为0(不包括对象头),这一步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用
4)对象头的设置:对象是那个类的实例,如何找到类的元数据,对象的哈希码,对象的GC分代年龄,以及是否启动偏向锁等
5)程序员按照自己的意愿进行对象初始化
思考:对象的创建在虚拟机中是频繁的行为,如何保证并发情况下的线程安全呢?
比如正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针分配内存
解决方案:
1)对分配内存空间的动作进行同步:实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
2)把内存分配的动作按照线程的不同划分在不同的空间进行:每个线程在java堆中预先分配一块内存,称为本地线程分配缓存TLAB,那个线程需要分配内存,就在那个线程的TLAB上分配,只用TLAB分配完并分配先新的TLAB时,才需要同步锁定
二.对象的内存布局
在HotSpot虚拟机中,对象在内存区域中存储布局可以分为3个区域:对象头,实例数据,对齐补充
对象头:又分为Mark Word和类型指针,Mark Word存储哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
,类型指针即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例
实例数据:对象真正储存的有效信息,也就是在程序代码中所定义的各种类型的字段内容,无论是从父类继承来的,还是在子类中定义的,都需要记录来
对齐补充:并不是必然存在的,因为HotSpot要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的整数倍,所以当对象的实例数据部分没有对齐时,就需要通过对齐补充来补全
三.对象的访问定位
java程序通过栈上的reference数据来操作堆上的具体对象,由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位从而访问堆中的对象元素的具体位置,所以对象的访问方式是取决于虚拟机实现的,目前主流的方式有使用句柄和直接指针两种
句柄方式:堆中划分一块内存作为句柄池,reference中储存的就是对象的句柄地址,而对象的句柄地址中包含了对象的实例数据与类型数据各自具体的地址信息
直接指针:reference中储存的就是对象地址,java堆对象的布局需要考虑如何放置访问类型数据的相关信息
优劣分析:
1.使用句柄的最大好处就是reference中储存的是稳定的句柄地址,在对象被移动(垃圾收集时对象移动是非常普遍的行为)时只会改变句柄中实例数据指针,而refrence本身不需要修改
2.直接指针访问最大好处就是速度更快,节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,积累下来也是一笔很大的时间开销
参考书籍:深入理解JAVA虚拟机 java高级特性与最佳实践 第二版