JVM调优系列3-虚拟机中对象的创建
1.对象创建的过程
当new一个对象时:
(1)虚拟机首先执行相应的类加载过程
(2)然后需要为对象分配内存。为对象分配内存就是在堆中把一块确定大小的内存划分出来。这时候会有一个问题:java堆是否规整
- 如果堆规整,所有用过的内存放在一边,没用过的内存放在另一边,中间放一个指针作为分界线。这时候分配内存就是讲指针向空闲的一边挪动一段与对象大小相等的空间。这种分配方式叫做指针碰撞。
- 如果堆不规整已使用的内存和未使用的内存相互交错,就无法使用指针碰撞。JVM需要维护一个空闲列表,记录哪些内存块可用。分配内存的时候,就在列表中找到一块足够大小的内存给对象,并更新列表。
- 选择哪种分配方式由堆是否规整决定,堆是否规整又由采用的垃圾回收器是否带有压缩整理功能决定。
这里还有另外一个问题,对象内存的划分是一个多线程的过程,这里存在并发安全的问题,针对这个问题,有两宗解决方案: - 对分配内存的动作进行同步处理,JVM采用的是CAS重试保证更新操作的原子性。
- 对每个线程就行预分配内存。也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。
TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。
(3)内存分配完成后,JVM需要将分配的内存进行初始化(如int值为0,boolean值为false),这样保证了对象的实例字段可以不赋初始值就可以使用。
(4)接着JVM需要对对象信息必要的设置,这个类是哪个对象的实例,怎么找到类的元数据,对象的hash码,对象的分代年龄等。这些信息放在对象头中。
(5)此时,从JVM角度看对象已经创建完毕,但从程序角度看,对象还未创建成功,接着会按照程序的代码进行对象的初始化。此时一个对象创建完毕。
2.对象的内存布局
在HotSpot虚拟机中,对象在内存中的布局可以分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头中包含两部分信息:第一部分存放对象运行时的数据,比如hash值,分代年龄,锁。另一部分存放的是类型指针,用于说明该对象是哪个类的实。
- 实例数据:这个不用多说
- 对齐填充:对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对对象的大小必须是8字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。
3.对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。
- 使用句柄:java在堆中创建一块区域作为句柄池。句柄中包含对象的实例数据和类型数据的地址。reference中保存的是句柄的地址。
- 直接指针:reference中存放的是对象的地址。对象中又保存了类型的地址。
两种方式的区别:
由于存在垃圾回收整理,使用句柄的话,reference本身不需要修改,地址是稳定。直接指针少了一次定位数据所带来的开销。
对Sun HotSpot而言,它是使用直接指针访问方式进行对象访问的。