文章目录
经典参考文章:
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
多态(动态绑定)
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编译器会对代码进行编译,如果创建的对象没有逃逸,不会被其他线程