java对象模型


经典参考文章:
Java的对象模型——Oop-Klass模型(一)
深入理解多线程(二)—— Java的对象模型
【JVM】第四篇:JVM是如何实现一个对象的?浅谈OOP-Klass对象模型

oop-klass模型

虚函数表的相关介绍:Java多态(动态绑定)的底层原理:虚函数表
HotSpot虚拟机使用opp-klass模型来描述一个JAVA类,Klass模型表示java类的元数据和虚方法信息,而oop模型表示java类的实例数据信息。
(1)每个java类被JVM加载的时候,JVM都会在方法区创建一个Klass类型的对象,保存类的元数据和虚方法信息。类的元数据中包含了类加载器的引用。
(2)java代码中,使用new创建一个对象时,JVM会根据Klass对象在堆内存中创建一个oop类型的对象。
(3)Classloader类加载器加载一个类并把类的原数据和虚方法表保存到方法区后,会创建一个对应的Class类型的对象存放在堆区。
注意:Klass是在class文件在加载过程中创建的,OOP则是在Java程序运行过程中new对象时创建的。

JVM内部是使用C++实现的,每个java类及其实例加装到虚拟机后都会用C++的类来表示,分别使用C++的oop类保存java类的实例数据,C++的klass类保存java类的元数据和虚方法表示。垃圾回收时,GC既要回收oop存储的实例数据,也要回收klass存储的元数据。

为什么HotSpot要设计一套oop-klass model呢?

虽然简单的做法是为每个Java类生成一个c++类与之对应。如果每个对象都维护一张虚函数表,内存开销将会非常大,HotSopt JVM的设计者为了提升性能,不想让每个java对象像C++对应一样中都含有一个vtable(虚函数表),设计了oop-klass模型,使用oop保存实例数据(包括指向klass对象的指针),使用klass保存类的元数据和虚函数表,同一个类的所有对象使用同一张虚函数表。
简而言之,主要就是两个作用:
(1)模拟C++的虚函数表,实现java的多态机制
(2)避免了像C++一样每个对象保存一个虚函数表,让每个类的所有对象共用一个虚函数表,节省资源。

C++是如何实现Java程序通过new操作符来创建一个对象

通过下面这段C++代码可以发现: 先对klass对象进行初始化工作,然后再用Klass对象来创建出oop对象。

// hotspot/src/share/vm/interpreter/interpreterRuntime.cpp
...
// HotSpot中new操作符的实现函数
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
  Klass* k_oop = pool->klass_at(index, CHECK);
  instanceKlassHandle klass (THREAD, k_oop);
  // Make sure we are not instantiating an abstract klass
  klass->check_valid_for_instantiation(true, CHECK);
  // Make sure klass is initialized
  klass->initialize(CHECK);
  // At this point the class may not be fully initialized
  // ...
  oop obj = klass->allocate_instance(CHECK);
  thread->set_vm_result(obj);
IRT_END

多态(动态绑定)

Java多态(动态绑定)的底层原理:虚函数表

oop继承体系和Klass继承体系

oop:Ordinary object pointer(普通对象指针)
oop类是oop体系中的最高父类,oop体系有许多子类型,每一个类型的oop都代表一个在JVM内部使用的特定对象的类型。在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。

// hotspot/src/share/vm/oops/oopsHierarchy.hpp
...
// Oop的继承体系
typedef class oopDesc*                            oop;
typedef class   instanceOopDesc*            instanceOop;
typedef class   arrayOopDesc*                    arrayOop;
typedef class     objArrayOopDesc*            objArrayOop;
typedef class     typeArrayOopDesc*            typeArrayOop;

klass继承体系和OOP继承体系一样,klass是其他klass类型的最高父类。

oop类

// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
// ====================java对象的对象头实际是C++的oop类型对象的如下数据==================
 ...
 private:
  // 用于存储对象的运行时记录信息,如哈希值、GC分代年龄、锁状态等(markOop实际不属于OOP体系)
  volatile markOop  _mark;
  // Klass指针的联合体,指向当前对象所属的Klass对象
  union _metadata {
    // 未采用指针压缩技术时使用
    Klass*      _klass;
    // 采用指针压缩技术时使用
    narrowKlass _compressed_klass;
  } _metadata;
 ...
 //====================java对象的字段和方法信息实际也是C++的oop类型对象的如下数据:===================================
 // 返回成员属性的地址
  void*     field_base(int offset)        const;
  // 如果成员是基础类型,则用特有的方法
  jbyte*    byte_field_addr(int offset)   const;
  jchar*    char_field_addr(int offset)   const;
  jboolean* bool_field_addr(int offset)   const;
  jint*     int_field_addr(int offset)    const;
  jshort*   short_field_addr(int offset)  const;
  jlong*    long_field_addr(int offset)   const;
  jfloat*   float_field_addr(int offset)  const;
  jdouble*  double_field_addr(int offset) const;
  Metadata** metadata_field_addr(int offset) const;
  // 同样是成员的地址获取方法,在GC时使用
  template <class T> T* obj_field_addr(int offset) const;   
 ...
  // instanceOop获取和设置其成员属性的方法
  oop obj_field(int offset) const;
  volatile oop obj_field_volatile(int offset) const;
  void obj_field_put(int offset, oop value);
  void obj_field_put_raw(int offset, oop value);
  void obj_field_put_volatile(int offset, oop value);
  // 如果成员时基础类型,则使用其特有的方法,这里只列出针对byte类型的方法
  jbyte byte_field(int offset) const;
  void byte_field_put(int offset, jbyte contents);
}

由C++代码可知:
(1)OOP类中有如下属性:运行时元数据、类的类型指针、数组长度(仅数组对象有)
(2)每个field在oop中都有一个对应的偏移量(offset),oop通过该偏移量得到该field的地址,再根据地址得到具体数据。因此,Java对象中的field存储的并不是对象本身,而是对象的地址。由此可见,如果成员属性属于普通对象类型,则oop类只存储它的地址。

oop类表示java类的实例信息,主要由三部分组成:
(1)对象头
(2)实例数据
(3)对齐填充
其中对象头又分为:
(1)mark word
(2)指向方法区类的元数据和虚函数表的指针【实际就是指向方法区中Klass类型的对象】
(3)数组长度(只有数组对象才有)

markword

在这里插入图片描述

  • markword中主要存储对象的HashCode值、持有当前对象的锁的线程ID、GC分代年龄、锁标志为等信息

  • markword在不同的锁状态下,存储的信息略有不同

  • 锁的四种状态:
    (1)无锁:一个对象没有加锁的状态
    (2)偏向锁:偏向某个线程,当只有一个线程访问锁对象时,不需要线程同步。
    当一个线程访问同步块并获取锁时,会在对象头的 Mark Word 和栈帧中的锁记录里存储锁偏向的线程ID。以后该线程在进入和退出该同步块时,直接检测当前 Mark Word 中存储的线程ID是否指向当前线程,如果是,则表示已获取锁。否则,如果 Mark Word 中偏向锁标识已被设置为1,则尝试使用CAS将 Mark Word 中线程ID指向当前线程。

    偏向锁不会主动释放,当有其他线程尝试获取锁时,才会释放锁。偏向锁的撤销,需等到全局安全点。首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活动,如不处于活动状态,则将 Mark Word 设置为无锁状态;否则,拥有偏向锁的栈被执行,栈中的锁记录和 Mark Word 中的锁状态恢复到无锁或者升级为轻量级锁,最后唤醒暂停的线程。
    (3)轻量级锁:当多个线程竞争时,偏向锁就会膨胀为轻量级锁,轻量级锁使用CAS实现,避免了用户态和内核态的切换。
    (4)重量级锁:当某个线程尝试获取轻量级锁失败的次数达到阈值(默认为10次,但JVM可以自动优化,用户也可以设置指定),就会向操作系统申请互斥锁,此时需要进行用户态和内核态的切换,性能消耗非常高。

实例对象存储在内存中的哪个位置(重点理解)

  • 如果实例对象存储在堆区,实例对象的数据存储在堆区,实例的引用存储在栈上,实例对应的类的元数据和虚函数表存储在方法区,其中静态成员变量存储在方法区,类加载后根据方法区类的元数据在堆中创建对应的Class类型字节码对象。
  • 实例对象不一定存储在堆区,也可能存储在栈区中。JIT编译器会对代码进行编译,如果创建的对象没有逃逸,不会被其他线程

逃逸分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值