一、创建
Java是一门面向对象的语言,做Java程序运行过程中,无时无刻都会有对象创建出来。
当虚拟机遇到一条new指令时,经过几个过程:
- 在常量池中定位到类的符号引用
- 检查该符号引用对应的类是否已经被加载、解析、初始化
- 在Java堆中为新生对象分配内存(对象所需内存在类加载时已经确定)
完成了以上几个过程后,我们又开始有疑问了,对象在内存中是如何分配的呢?
- 先判断是否需要在TLAB(本地线程分配缓冲)中分配
- 如果不需要则会在Java堆中分配
至于如何在Java堆中分配,会涉及到JVM实例启动时所采用到的垃圾回收算法。
- 如果使用Serial,ParNew等带有压缩过程的收集器时,Java堆的划分就为规整,则会使用“指针碰撞”的分配方式。
- 如果使用CMS这种基于MAark-Sweep算法的收集器时,通常采用“空闲列表”分配方式。
明白了对象的堆中的分配情况,我们还需要了解的是对象的内存空间划分 是需要保证线程安全的。
比如有这种情况,当对象a使用指针碰撞划分了一块内存空间,但是还未来得及修改指针,这时对象b也要过来分配空间,拿着旧的指针分配了一块内存地址.
这个时候对象a和对象b的分配就会出现错误了。那么虚拟机是如何解决这个问题呢?
实际上,虚拟机使用采用2中方式来解决这个问题,一是使用CAS方法+失败不断重试的机制;二是使用TLAB本地线程分配缓冲,在Java线程栈中分配。这两种方式就能够保证内存分配的并发问题。
下面解释了“指针碰撞”和“空间列表”的内存分配方式:
指针碰撞
规整的内存空间,即所有用的内存放一边,空闲都内存放另外一遍。中间放着一个指针作为分界点的指示器。分配内存的过程就是把分界点指针往空闲的内存空间移动大小等于对象大小的内存空间,这种内存分配方式成为指针碰撞
空闲列表
如果Java堆中的内存是不规整的,也就是使用过的内存和空闲内存相互交错,那么这种方式没办法用指针碰撞来分配。这时一个列表,该列表维护了内存的使用情况,包括已经使用的内存地址和空闲内存的内存地址
二、对象的数据结构
上面说完了对象的创建过程和内存分配方式,接下来说说对象在内存中的布局。
在HotSpot虚拟机中,对象在内存中存储的布局结构可以分为对象头、实例数据、对其填充
1. 对象头
对象头包含两部分信息
第一部分用于存储对象自身的运行时数据
包括哈希吗,GC分代年龄,锁状态,线程持有的锁,偏向线程ID,偏向时间戳等。
第二部分类型指针
即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2. 实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种数据类型的字段内容。无论从父类继承还是子类自己定义的,都需要记录起来。
实例数据的存储顺序会受到虚拟机分配参数和字段在Java源码中定义的顺序影响。
HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes,booleans,oops(ordinary object pointers), 相同宽度的字段总是被分配到了一起;除此之外,父类定义的变量会出现在子类之前。
3. 对齐填充
这部分的数据并不是必然存在的,也没用特别的含义,它仅仅起着占位符的作用。
由于HotSpot的自动内存管理系统要求对象起始地址必须是8字节的整倍数,换句话说,就是对象的大小必须是8字节的整数倍。
而对象头正好是8字节的整数倍(1倍或者2倍),因此当对象实例数据部分没有对其时,需要通过对齐填充来补全8字节的整数倍。
三、访问定位
在Java程序中通过栈上的reference数据来操作堆上的具体对象。
目前主流的虚拟机定位和访问堆中的对象的具体位置有两种方式:句柄和直接指针。
1. 句柄
如果使用句柄访问对象,虚拟机则会在Java堆中划分一块内存来做句柄池,reference中存储的就是对象的句柄地址。并且句柄包含对象实例数据和类型数据各自的地址信息。
这样通过句柄就可以找到类和对象的内存位置
2. 直接指针
如果使用直接指针访问,那么Java堆对象的布局中,就必须考虑如何防治访问类型数据的相关信息,而reference中存储的直接就是对象地址。
两种访问方式各有优势。
1. 句柄访问方式的好处在于当对象被移动时,只需要维护句柄池中的实例数据引用地址即可,不需要改变reference的值。
2. 直接指针访问方式好处在于节省了一次引用的定位时间。
HotSpot使用第二种方式访问,就是直接指针访问方式。