《深入理解Java虚拟机》:对象创建、布局和访问全过程

《深入理解Java虚拟机》:Java内存区域
对象的创建虚拟机遇到一条new指令时 , 1、检查这个指令的参数能否在常量池中定位到这个类的符号引用;
2、检查这个符号引用代表的类是否已被加载、解析和初始化过;
3、如果没有 , 先执行相应的类加载过程;
4、类加载检查通过后 , jvm为新生对象分配内存 , 类加载完成后就能确定new一个对象所需的内存大小 , 即可从堆中划分一块确定大小给对象;
5、如果堆中内存是规整的 , 已被使用的内存和未使用的内存之间有一个作为分界点的指针 , 只需移动指针位置便可分配内存 , 这种分配方式称为“指针碰撞”;
6、如果堆中内存不是规整的 , jvm需要维护一张表 , 记录哪些内存块是可用的 , 在分配的时候从表里找一块足够大的空间划分给对象实例 , 并且更新列表记录 , 这种分配方式称为“空闲列表”;
选择哪种分配方式由java堆是否规整决定 , 而java堆是否规整是由垃圾回收算法决定的 。 在serial、parnew等带compact(整理)过程的垃圾收集器时 , 系统采用分配算法是指针碰撞 , 而使用CMS这种基于Mark-Sweep(标记-清除)算法的收集器时 , 通常采用空闲算法 。
分配内存时的线程安全问题:jvm在堆中创建对象时 , 即使只是移动指针 , 也可能发生线程安全问题 。 因为查找内存地址和移动指针不是一个原子操作 , 可能出现在给对象A分配内存 , 指针还没来的及修改 , 对象B又同时使用了原来的指针来分配内存 。
解决方案:一种是基于CAS(Compareand Swap , 比较并交换) , 先拿到内存值 , 赋新值的时候 , 拿预期值和内存值作比较 , 相等就赋值 , 不相等说明被别的线程修改过 , 失败重试直到成功 。
另一种是把内存分配的动作划分在不同的空间进行 , 即每个线程在java堆中预先分配一小块空间 , 称为本地线程分配缓存(Thread Local Allocation Buffer TLAB) 。 哪个线程要分配内存 , 就在哪个线程的TLAB上分配 , 只有TLAB用完并分配新的TLAB时 , 才需要同步锁定 。
对象的内存布局对象在内存中的布局分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(padding).
对象头包括两部分内容:第一部分用于存储对象自身运行时数据 , 如HashCode、GC分代年龄、锁标记状态、线程持有的锁、偏向锁ID、偏向时间戳 , 这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit , 称为“Mmark Word” , 下图是在网络上找的 , 可作参考 。
《深入理解Java虚拟机》:对象创建、布局和访问全过程文章插图
对象头的另一部分是类型指针 , 即对象指向方法区(元数据)的指针 , jvm通过这个指针来确定这个对象是哪个类的实例 。
除此之外的第三部分 , 对齐填充不一定是必然存在的 。 因为hotspot要求对象起始地址必须是8字节的整数倍 , 当对象实例数据部分没有对齐时 , 就需要通过对齐填充来补齐 。
对象的访问定位建立对象是为了使用对象 , 我们需要通过虚拟栈上的reference数据来操作堆上的实例对象 。 reference只规定了一个指向对象的引用 , 所以对象访问的具体方式取决于jvm的实现 。 目前主流访问方式有使用句柄和直接指针两种 。
第一种使用句柄 , java堆中会划分一块做句柄池 , reference中存的就是句柄池的地址 , 而句柄中包含了对象实例数据与类型数据各自的具体信息 。 如图:
《深入理解Java虚拟机》:对象创建、布局和访问全过程文章插图
第二种使用直接指针访问 , 那么java堆中对象的布局中就必须考虑如何放置访问类型数据的相关信息 , 而reference中存储的直接就是对象地址 。 如图:
《深入理解Java虚拟机》:对象创建、布局和访问全过程文章插图