Fork me on GitHub

JVM对象探秘

对象的创建

对象创建过程

在语言层面上,创建对象(例如克隆,反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(本文讨论仅限普通java对象,不包括数组和Class对象等)的创建是一个怎样的过程呢?

  1. 虚拟机遇到new指令时,首先检查指令的参数是否在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已被加载、解析和初始化过。若有则进入下一步,否则必须执行相应的类加载过程。
  2. 在类加载检查通过后,为对象分配内存,内存所需大小在类加载完成后便可确定。
  3. 将分配到的内存空间初始化为零值(不包括对象头)。
  4. 对对象进行必要的都设置(如对象头信息)————对JVM来说对象创建完成。
  5. 执行对象的init方法对字段进行初始化————产生一个真正可以的对象。

内存分配原则

  • 内存分配方法
    1. 指针碰撞: 若Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离。
    2. 空闲列表: 若Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
  • 分配原则:选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。Serial、ParNew等带Compact过程的收集器采用指针碰撞;CMS这种基于Mark-Sweep算法的收集器采用空闲列表。

保证内存分配线程安全

对象的创建非常频繁,所以分配内存时改变指针的位置不是线程安全的。

  • 解决方案一: 对分配内存的动作进行同步—实际上虚拟机采用的CAS算法保证原子性
  • 解决方案二: 把内存分配的动作划分到不同的空间进行,即每个线程在java堆预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)。每个线程在自己的TLAB上分配内存,当TLAB用完并分配新的TLAB时,才需要同步锁定。是否使用TLAB,通过-XX:+/-UseTLAB参数来设定。

对象的内存布局

对象内存布局分为3快区域: 对象头、实例数据、对齐填充。

对象头(主要包括两部分)

  • 运行时数据(哈希码、GC分代年龄、所状态标志、线程持有的锁、偏向线程ID、偏向时间戳)
  • 类型指针—即对象指向它的类元数据的指针
  • 若是数组,还有一块内存记录数组的长度

实例数据

存储代码中所定义的各种类型的字段内容(包括继承父类的)。存储顺序受到虚拟机分配策略参数(FieldsAllocationStyle)和代码中定义顺序的影响。默认策略为:longs/doubles、ints、shorts/chars、bytes/booleans、oops。

对齐填充

并不是必然存在的,仅仅起占位符的作用,HotSpotVM要求对象起始地址必须是8字节的整数倍,所以当实例数据部分没有对齐时,需要通过对齐填充来补全。

对象的的访问定位

句柄访问

Java堆将会划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的地址信息,如图: 句柄访问
优势:存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改。

直接指针访问(HotSpot使用次方式)

reference中存储的直接就是对象地址,如图:
直接指针访问
优势:访问速度快,节省了一次指针定位的时间开销。

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!