java虚拟机学习笔记(2) 对象的创建、分布、访问

对象的分配、布局、访问 (HotSpot)

Ps:本篇内容中提到的对象只限于普通Java对象,不包括数组和Class对象。


1对象的创建(可大致分为5个阶段)

当虚拟机遇到new指令之后发生了什么


1)检查这个指令的参数是否能在常量池中定位到一个类的符号引用。再检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则执行相应的类的加载过程。


2)虚拟机在Java堆种为对象分配内存,其大小在类加载之后便可以完全确定。具体的分配方法就两种:

第一种称为“指针碰撞”,假设Java堆中的内存是规整的,虚拟机将Java堆分成用过的内存和空闲的内存,并在分界上维护一个指针。当需要分配内存时,指针向空闲的内存那边移动相应的距离。

第二种称为“空闲列表”,假设Java堆中的内存不是规整的,虚拟机就需要维护一个列表,来记录哪些内存块是空闲的。当需要分配内存时,虚拟机将在列表中找一个足够大的空间划分给对象。


Ps:如果虚拟机使用具有压缩整理(Compact)功能的垃圾收集器(Serial、ParNew)时,使用“指针碰撞”。当使用基于标记-清除算法的收集器时(CMS),使用“空闲列表”。

(几乎涉及到内存分配的地方都会有这两种方法吧233333。个人理解:通过指针来管理的话则需要经常对内存进行压缩,有时间消耗。而使用列表来管理的话则有可能会有一些小的内存片段浪费。总之,这里就不多说了哈哈)

为了保证分配内存时线程安全,有两种解决方法:

第一种,为实现分配内存空间的动作同步,虚拟机可以采用CAS配上失败重试的方法保证原子性。

第二种,每个线程各自有个TLAB来分配内存,用完时在同步锁定,


3)虚拟机把分配的内存空间都初始化为零值(类似默认值),来保证对象的实例字段在不设初始值就直接使用。如果使用TLAB,这一工作也可提前至TALAB分配时进行。


4)虚拟机对对象进行设置。例如:该对象的类、累的元数据、对象的哈希码、GC分代年龄。主要存放在对象头里面。


5)执行对应的<init>方法,把对象按照要求初始化。

至此,一个真正可用的对象就产生了。

 

这里贴一段HotSpot中bytecodeInterpreter.cpp的代码,来源《深入理解Java虚拟机》,这段代码仅供参考,在实际执行时很少会用到。

 

 

// 确保常量池中存放的是已解释的类

if(!constants->tag_at(index).is_unresolved_klass()) {

  // 断言确保是klassOop和instanceKlassOop(这部分下一节介绍)

  oop entry = (klassOop)*constants->obj_at_addr(index);

  assert(entry->is_klass(), "Should beresolved klass");

  klassOop k_entry = (klassOop) entry;

 assert(k_entry->klass_part()->oop_is_instance(), "Should beinstanceKlass");

  instanceKlass* ik = (instanceKlass*)k_entry->klass_part();

  // 确保对象所属类型已经经过初始化阶段

  if ( ik->is_initialized() &&ik->can_be_fastpath_allocated() ) {

    // 取对象长度

    size_t obj_size = ik->size_helper();

    oop result = NULL;

    // 记录是否需要将对象所有字段置零值

    bool need_zero = !ZeroTLAB;

    // 是否在TLAB中分配对象

    if (UseTLAB) {

      result = (oop)THREAD->tlab().allocate(obj_size);

    }

    if (result == NULL) {

      need_zero = true;

      // 直接在eden中分配对象

retry:

      HeapWord* compare_to =*Universe::heap()->top_addr();

      HeapWord* new_top = compare_to +obj_size;

      // cmpxchg是x86中的CAS指令,这里是一个C++方法,通过CAS方式分配空间,并发失败的话,转到retry中重试直至成功分配为止

      if (new_top <=*Universe::heap()->end_addr()) {

        if (Atomic::cmpxchg_ptr(new_top,Universe::heap()->top_addr(), compare_to) != compare_to) {

          goto retry;

        }

        result = (oop) compare_to;

      }

    }

    if (result != NULL) {

      // 如果需要,为对象初始化零值

      if (need_zero ) {

        HeapWord* to_zero = (HeapWord*) result+ sizeof(oopDesc) / oopSize;

        obj_size -= sizeof(oopDesc) / oopSize;

        if (obj_size > 0 ) {

          memset(to_zero, 0, obj_size *HeapWordSize);

        }

      }

      // 根据是否启用偏向锁,设置对象头信息

      if(UseBiasedLocking) {

       result->set_mark(ik->prototype_header());

      } else {

       result->set_mark(markOopDesc::prototype());

      }

      result->set_klass_gap(0);

      result->set_klass(k_entry);

      // 将对象引用入栈,继续执行下一条指令

      SET_STACK_OBJECT(result, 0);

      UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);

    }

  }

}

 

2.对象的内存布局

在HotSpot虚拟机中,对象在内存中可以大致分为3个部分:对象头(header)、实例数据(Instance Data)、对齐填充(Padding)。

 

1)        对象头

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

另一部分则是类型指针,即对象指向它的类元数据的指针,虚拟机通过这一指针来确定这个对象是哪个类的示例。但这不是一定的,查找对象的元数据不一定要通过对象本身(下面会说到)。


2)        实例数据

这里是对象真正存储的有效信息,即代码里面定义的各种类型的字段内容,也包括从父类继承下来的。有意思的是,这部分的存储顺序会受到虚拟机的分配策略和字段在代码中定义的顺序影响。HotSpot默认的分配顺序为:longs/double、ints、shorts/chars、bytes/booleans、oops(Ordinary ObjectPointer)。一般父类的属性会在子类之前,另外,如果把CompactFields(又是压缩)设为true,在子类中比较小的变量有可能会插入的父类变量的空隙中。


3)        对齐填充

这部分并不是必须的。由于HotSpotVm的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。而对象头已经是8字节的整数倍了(32bytes 或 64bytes 取决于32位还是64位的虚拟机),当实例数据部分不对齐时,就需要对齐填充。

 

3.对象的访问定位

      为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前有两种主流的访问方式:句柄、直接指针。

1)        句柄访问


如果使用句柄访问,reference中存储的就是对象的句柄地址,句柄地址中存储了指向对象实例数据的指针和指向对象类型数据的指针。好处是当对象移动(GC时)时只会改变句柄中的实例数据指针,reference本身不用修改。


2)        直接指针访问

使用直接指针访问,reference直接存储对象地址。而对象里存放指向对象类型数据的指针。这样的好处是节约了一次指针定位的时间开销。在HotSpot中采用直接指针访问。



下一篇是关于OutOfMemoryError异常的,我准备做死在自己电脑上试试23333,祝我好运~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值