虚拟机在java堆中对象分配、布局和访问的过程

  1. 云栖社区>
  2. 博客>
  3. 正文

虚拟机在java堆中对象分配、布局和访问的过程

坎布里奇 2019-08-06 00:55:52 浏览181

虚拟机在java堆中对象分配、布局和访问的过程

一、 对象的创建

从java程序,new指令开始

类加载

类加载通过后,内存分配

  • 对象所需内存的大小在类加载完成后就可以完全确定,为对象分配空间的任务等于把一块确定大小的内存从Java堆中划分出来。
  • 两种方法:

    • 指针碰撞

    假如,java堆中内存是绝对规整的,所有用过的内存和空闲的内存分为两部分,中间放一个指针作为分界点的指示器,那分配内存

    • 空闲列表

    假如,java堆中的内存并不是规整的,已使用的内存和空间的内存相互交错,如此虚拟机就必须维护一个列表,记录上哪些内存块是可用的,

  • 分配方式的选择取决于:java堆是否规整;Java堆是否规整取决于:所采用的垃圾收集器是否带有压缩整理功能
  • 实例

    • 在使用Serial、ParNew等带有Compact过程的收集器时,系统采用分配算法为:指针碰撞;
    • 使用CMS,基于 Mark-Sweep算法的收集器时,系统采用分配算法为:空闲列表。

对象创建的线程安全问题

  • 问题描述

    对象创建在虚拟机中的行为非常频繁,即使只是修改一个指针所指向的位置,在并发情况下也并不是线程安全的,有可能出现正在给对象A分配
    内存,指针还没有修改,对象B又同时使用了原来的指针来分配内存的情况。

  • 解决方法,有两种:

    • 1 对分配空间的动作进行同步处理

    实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;

    • CAS(比较与交换,Compare and swap)是一种有名的无锁算法。(ConcurrentHashMap也用到了此算法)
    • 2 本地线程分配缓冲

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Tlab)。

  • 虚拟机是否使用Tlab,可以通过-XX:+/-UseTlab参数来设定

对象内存分配完成后,虚拟机把内存空间初始化

  • 虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用Tlab,初始化操作可以提前至Tlab分配时进行。
  • 目的:

    保证了对象的实例字段在java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的的零值。

修饰对象

  • 虚拟机对对象进行必要的设置。其实是对象头。
  • 对象头中包括:对象的哈希码、对象的Gc分代年龄、此对象是哪个类的实例、类的元数据信息的指示等;
  • 根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

从虚拟机的角度,一个新的对象已经产生。

从java程序的角度,对象创建才刚刚开始,init方法还没有执行,所有的字段都为零。

  • 一般来说(由字节码中是否跟随invokespecial指令所决定),执行new 指令之后会接着执行方法,
    把对象按照程序员的意愿进行初始化,这样一个真正可用的对象产生。

二、 对象的内存布局

对象在内存中布局分配分为三个部分

  • 对象头
  • 实例数据
  • 对齐填充

1 对象头

  • 如图:
    IMG_20190805_232723
  • 第一部分

    用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,

  • 第二部分

    类型指针,即对象指向它的类元素数据的指针,虚拟机通过该指针确定对象的实例是哪个类。

2 实例数据

  • 对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
  • 包括父类继承的,子类定义的;
  • 存儲順序: 虚拟机分配策略参数和字段在java源码中定义的顺序 ;
  • hotSpot虚拟机默认的分配策略为longs/doubles、ints、 shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),
    相同宽度的字段被分配到一起。在这个前提下,父类中定义的变量会出现在子类之前。

3 对齐填充

  • 不是必须存在的,只是占位符的作用。
  • 因为对象的大小必须是8字节的整数倍。对象头部分正好是8字节的倍数,当对象实例数据部分没有对齐时,通过对齐填充补全。

三、 访问

访问方式有2个

  • 句柄和指针

句柄访问

IMG_20190806_004934

  • 存储位置:java 堆中划分一块内存作为句柄池,reference中存储的就是对象的句柄地址;
  • 包含信息:对象实例数据与类型数据各自的具体地址信息。

指针访问

IMG_20190806_004950

  • reference 存储的直接就是对象地址。
  • 问题: 考虑java堆对象的布局中存放访问类型数据的相关信息。

优劣

句柄

  • reference中存储的是稳定的句柄地址,对象被移动时,只改变句柄中的实例数据指针,而reference本身不需要修改。(在垃圾收集时移动对象时普遍的)

指针

  • 速度更快,节省了一次指针定位的时间开销。--由于对象的访问在java非常频繁。(积少成多)

Sun HotSpot 采用第二种方式

参考: java虚拟机第二版