OOP-KLASS模型学习

谈起Java对象,Java中的每一个对象(不包括基础类型)都继承于Object对象。相信这也是大多数程序员对Java对象的初次印象,Object可以表示所有的Java对象。但是,这种理解仅仅是停留在语言层面,至于更深的JVM层面,对象还是用Object来表示吗?显然不是。JVM通常使用非Java语言实现,是用来解析并运行Java程序的,它有自己的模型来表示Java语言的各种特性,包括Object。下面我们以HotSpot为例,一起来探讨Java对象在JVM层面的Java对象模型。

从《深入理解Java虚拟机》一书中我们得知,在加载(Load)阶段,虚拟机需要完成3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口

有点懵,什么是运行时数据结构?什么是java.lang.Class对象?既然一时无法明白书上描述的内容具体含义是什么,那我们就从源码入手,看下能否得到一些启发。

 整理了一份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记的【点击此处即可】即可免费获取

Load

从Java类加载器双亲委派模型中可知,如果应用程序没有自定义过自己的类加载器,一般情况下,程序中默认的类加载器就是sun.misc.Launcher$AppClassLoader,通过追踪该类的类加载方法loadClass,其核心调用为defineClass方法。可以发现其中不同支路的defineClass都会定向为native方法defineClass0、defineClass1、defineClass2。这三个native方法的源码实现可见ClassLoader.c,最终都指向了JVM_DefineClassWithSource方法。

 

arduino

复制代码

//jvm.cpp JVM_ENTRY(jclass, JVM_DefineClassWithSource(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source)) JVMWrapper2("JVM_DefineClassWithSource %s", name); return jvm_define_class_common(env, name, loader, buf, len, pd, source, true, THREAD); JVM_END

其内部直接调用了jvm_define_class_common方法

 

scss

复制代码

static jclass jvm_define_class_common(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source, jboolean verify, TRAPS) { if (source == NULL) source = "__JVM_DefineClass__"; assert(THREAD->is_Java_thread(), "must be a JavaThread"); JavaThread* jt = (JavaThread*) THREAD; PerfClassTraceTime vmtimer(ClassLoader::perf_define_appclass_time(), ClassLoader::perf_define_appclass_selftime(), ClassLoader::perf_define_appclasses(), jt->get_thread_stat()->perf_recursion_counts_addr(), jt->get_thread_stat()->perf_timers_addr(), PerfClassTraceTime::DEFINE_CLASS); if (UsePerfData) { ClassLoader::perf_app_classfile_bytes_read()->inc(len); } // Since exceptions can be thrown, class initialization can take place // if name is NULL no check for class name in .class stream has to be made. TempNewSymbol class_name = NULL; if (name != NULL) { const int str_len = (int)strlen(name); if (str_len > Symbol::max_length()) { // It's impossible to create this class; the name cannot fit // into the constant pool. THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name); } class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL); } ResourceMark rm(THREAD); ClassFileStream st((u1*) buf, len, (char *)source); Handle class_loader (THREAD, JNIHandles::resolve(loader)); if (UsePerfData) { is_lock_held_by_thread(class_loader, ClassLoader::sync_JVMDefineClassLockFreeCounter(), THREAD); } Handle protection_domain (THREAD, JNIHandles::resolve(pd)); Klass* k = SystemDictionary::resolve_from_stream(class_name, class_loader, protection_domain, &st, verify != 0, CHECK_NULL); if (TraceClassResolution && k != NULL) { trace_class_resolution(k); } return (jclass) JNIHandles::make_local(env, k->java_mirror()); }

我们看到,jvm_define_class_common方法中调用了SystemDictionary::resolve_from_stream方法,创建了一个Klass类型的对象。

继续看SystemDictionary::resolve_from_stream方法

 

ini

复制代码

Klass* SystemDictionary::resolve_from_stream(Symbol* class_name, Handle class_loader, Handle protection_domain, ClassFileStream* st, bool verify, TRAPS) { // Classloaders that support parallelism, e.g. bootstrap classloader, // or all classloaders with UnsyncloadClass do not acquire lock here bool DoObjectLock = true; if (is_parallelCapable(class_loader)) { DoObjectLock = false; } ClassLoaderData* loader_data = register_loader(class_loader, CHECK_NULL); // Make sure we are synchronized on the class loader before we proceed Handle lockObject = compute_loader_lock_object(class_loader, THREAD); check_loader_lock_contention(lockObject, THREAD); ObjectLocker ol(lockObject, THREAD, DoObjectLock); TempNewSymbol parsed_name = NULL; // Parse the stream. Note that we do this even though this klass might // already be present in the SystemDictionary, otherwise we would not // throw potential ClassFormatErrors. // // Note: "name" is updated. instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name, loader_data, protection_domain, parsed_name, verify, THREAD); const char* pkg = "java/"; if (!HAS_PENDING_EXCEPTION && !class_loader.is_null() && parsed_name != NULL && !strncmp((const char*)parsed_name->bytes(), pkg, strlen(pkg))) { // It is illegal to define classes in the "java." package from // JVM_DefineClass or jni_DefineClass unless you're the bootclassloader ResourceMark rm(THREAD); char* name = parsed_name->as_C_string(); char* index = strrchr(name, '/'); *index = '\0'; // chop to just the package name while ((index = strchr(name, '/')) != NULL) { *index = '.'; // replace '/' with '.' in package name } const char* fmt = "Prohibited package name: %s"; size_t len = strlen(fmt) + strlen(name); char* message = NEW_RESOURCE_ARRAY(char, len); jio_snprintf(message, len, fmt, name); Exceptions::_throw_msg(THREAD_AND_LOCATION, vmSymbols::java_lang_SecurityException(), message); } if (!HAS_PENDING_EXCEPTION) { assert(parsed_name != NULL, "Sanity"); assert(class_name == NULL || class_name == parsed_name, "name mismatch"); // Verification prevents us from creating names with dots in them, this // asserts that that's the case. assert(is_internal_format(parsed_name), "external class name format used internally"); // Add class just loaded // If a class loader supports parallel classloading handle parallel define requests // find_or_define_instance_class may return a different InstanceKlass if (is_parallelCapable(class_loader)) { k = find_or_define_instance_class(class_name, class_loader, k, THREAD); } else { define_instance_class(k, THREAD); } } // Make sure we have an entry in the SystemDictionary on success debug_only( { if (!HAS_PENDING_EXCEPTION) { assert(parsed_name != NULL, "parsed_name is still null?"); Symbol* h_name = k->name(); ClassLoaderData *defining_loader_data = k->class_loader_data(); MutexLocker mu(SystemDictionary_lock, THREAD); Klass* check = find_class(parsed_name, loader_data); assert(check == k(), "should be present in the dictionary"); Klass* check2 = find_class(h_name, defining_loader_data); assert(check == check2, "name inconsistancy in SystemDictionary"); } } ); return k(); }

从方法注释可以看出,resolve_from_stream方法根据class文件的字节流,向系统中添加了一个klass对象,其中核心方法是调用了ClassFileParser(st).parseClassFile方法

 

scss

复制代码

instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name, ClassLoaderData* loader_data, Handle protection_domain, KlassHandle host_klass, GrowableArray<Handle>* cp_patches, TempNewSymbol& parsed_name, bool verify, TRAPS) { //... // We can now create the basic Klass* for this klass _klass = InstanceKlass::allocate_instance_klass(loader_data, vtable_size, itable_size, info.static_field_size, total_oop_map_size2, rt, access_flags, name, super_klass(), !host_klass.is_null(), CHECK_(nullHandle)); instanceKlassHandle this_klass (THREAD, _klass); //... // Allocate mirror and initialize static fields java_lang_Class::create_mirror(this_klass, protection_domain, CHECK_(nullHandle)); //... // preserve result across HandleMark preserve_this_klass = this_klass(); } //... }

简化parseClassFile方法主流程后,发现该方法中首先调用了InstanceKlass::allocate_instance_klass方法创建一个klass指针,并以该指针作为入参,调用了java_lang_Class::create_mirror方法,依据注释,是分配了一个mirror镜像,并初始化静态属性。

InstanceKlass::allocate_instance_klass方法中只是调用了InstanceKlass类的构造函数创建一个InstanceKlass对象,没有其他特殊逻辑,由此InstanceKlass对象创建完成,即本节开头提到虚拟机在加载(Load)阶段做的三件事中的第二件:将这个字节流所代表的静态存储结构转化为运行时数据结构,这个InstanceKlass对象就代表了该目标类运行时数据结构。

parseClassFile方法中是用的Klass类型作为了InstanceKlass::allocate_instance_klass方法的返回值,而InstanceKlass::allocate_instance_klass方法实际返回的类型是InstanceKlass,因此可以看出InstanceKlass是Klass的一个子类。

我们继续看下java_lang_Class::create_mirror方法

 

scss

复制代码

//src/share/vm/classfile/javaClasses.cpp oop java_lang_Class::create_mirror(KlassHandle k, Handle protection_domain, TRAPS) { //... if (SystemDictionary::Class_klass_loaded()) { // Allocate mirror (java.lang.Class instance) Handle mirror = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK_0); InstanceMirrorKlass* mk = InstanceMirrorKlass::cast(mirror->klass()); // 计算mirror镜像中的静态属性数量 java_lang_Class::set_static_oop_field_count(mirror(), mk->compute_static_oop_field_count(mirror())); // 初始化mirror镜像中的静态属性 // Initialize static fields InstanceKlass::cast(k())->do_local_static_fields(&initialize_static_field, CHECK_NULL); } return mirror(); } else { //... } }

这个方法中,主要做了两件事情:

  1. 分配mirror镜像

SystemDictionary::Class_klass()返回的是指向java.lang.Class类对应的InstanceMirrorKlass对象的指针,然后调用了InstanceMirrorKlass::allocate_instance方法分配mirror镜像,依据注释,该方法分配了一个mirror镜像,即创建了一个java.lang.Class类的实例,即本节开头提到虚拟机在加载(Load)阶段做的三件事中的第三件:在内存中生成一个代表这个类的java.lang.Class对象。

在加载 (Load)目标类的时候,除了会创建目标类的InstanceKlass实例,还会创建一个该目标类对应java.lang.Class对象。

  1. 初始化mirror镜像中的静态属性

表明类的静态属性都保存在了java.lang.Class对象中

InstanceMirrorKlass::allocate_instance方法

可以看到,主要是调用CollectedHeap::Class_obj_allocate方法在JVM堆内存上创建一个instanceOop对象。

java.lang.Class类对象在JVM中的类型为instanceOop

Class_obj_allocate中有两个入参需要注意一下:

1)KlassHandle klass

往回追溯一下调用次方法的地方,发现klass是一个指向java.lang.Class类对应的InstanceMirrorKlass对象的指针

2)KlassHandle real_klass

这个参数需要追溯到ClassFileParser(st).parseClassFile方法,real_klass代表了刚创建好的InstanceKlass类型的指针

Class_obj_allocate方法首先在堆内存空间中创建了一个oop类型的mirror对象,然后设置该对象的对象头。要注意,该对象头中的Klass Pointer指针指向了入参klass,即指向了java.lang.Class类对应的InstanceMirrorKlass对象

此后将创建的mirror对象中的某个指针指向real_klass,即指向了在ClassFileParser(st).parseClassFile方法中创建的InstanceKlass对象。然后再将在ClassFileParser(st).parseClassFile方法中创建的InstanceKlass对象中的java_mirror指针指向了刚创建的mirror对象(java.lang.Class对象)。

至此,完成了对类的加载(Load)流程的分析,我们发现,在整个整个加载(Load)流程中,主要都是在围绕Klass、InstanceKlass、InstanceMirrorKlass、oop这几个类型的对象进行操作,我们先根据已分析的源码逻辑,对这几个类型的对象进行简单梳理,在完成类的加载(Load)后,内存中对象间的关系大致如下图所示:

其中InstanceOop对象就是该目标类对应的java.lang.Class类的对象,该对象的对象头中的klass pointer指针指向了java.lang.Class类在内存中的运行时数据结构是一个InstanceMirrorKlass对象;另外该InstanceOop对象还有一个指针ptr,指向了该目标类在内存中的运行时数据结构是一个InstanceKlass对象;此外,该InstanceOop对象中还保存了目标类中的静态属性,该InstanceOop对象亦被称作目标类的Java Mirror对象。

InstanceKlass对象代表了该目标类在内存中的运行时数据结构。

这里提到的类的加载(Load)指的是非java.lang.Class类的加载,上图中的InstanceMirrorKlass对象就是由java.lang.Class类经过加载后,JVM创建的对象,代表了java.lang.Class这个类的运行时数据结构。

类的加载(Load)阶段,JVM创建了两个对象,分别是目标类的InstanceOop对象和InstanceKlass对象。

NEW

在虚拟机完成类加载(Class Loading)后,得到了可以被虚拟机直接使用的Java类型。在Java语言中最常用的创建对象的方式就是通过new关键字创建对象,想要了解Java对象在内存中具体的对象模型,就需要看下JVM在创建对象的时候都做了哪些事情(对象创建的完整过程在Hotspot中的源码可见bytecodeIntepreter.cpp)

当JVM读取到字节码指令new时,JVM执行流程如下:

首先从当前线程的栈帧中的操作数栈中获取要new的目标类的符号引用在常量池中的索引 (详见文末的注1),根据该索引,从常量池中找出对应的符号引用。根据该符号引用是否已经解析成直接引用,后续创建流程分为两个流程分支:类已解析 和 类未解析。

类已解析

确保该类已经完成初始化

首先校验该直接引用是否是Klass指针及InstanceKlass指针,并确保目标类已经完成初始化,并且可以使用快速分配的方式创建对象。否则会执行类未解析流程。

为新对象分配内存空间

获取目标类对象的大小(在编译期已经确定),如果开启了TLAB设置,则在TLAB区域分配对象内存。否则尝试在Eden区域分配对象内存。通过不断尝试占用当前未分配内存空间的起始地址往后的目标类对象的大小这段内存空间,为对象分配内存空间,即指针碰撞。

内存空间初始化及对象头设置

如果需要初始化内存空间,则将分配的内存空间都初始化为0,并设置对象头中的Mark Word 和 Klass Pointer。

可以看到,在类已解析流程中,主要是创建了一个oop对象,并设置该oop对象的对象头中的Mark Word及Klass Pointer。其中,Klass Pointer指针指向了k_entry,往回追溯一下,发现k_entry指向的是该目标类的InstanceKlass对象

类未解析

调用 InterpreterRuntime::_new 方法

第一步获取常量池中的直接引用,如果该目标类还未初始化,则先进行初始化(此部分代码跳过)。最后调用allocate_instance方法创建oop类型的对象。

看下allocate_instance方法

可以看到主要是调用的CollectedHeap类中的obj_allocate方法创建了一个instanceOop对象,继续跟踪此方法

该方法中主要执行了两个步骤:创建oop对象,并设置对象的对象头中的Mark Word和Klass Pointer。

至此,new字节码执行完成。在类未解析流程中,首先初始化目标类,然后通过common_mem_allocate_init方法,在堆内存中创建了目标类的一个oop对象,并设置对象的对象头中的Mark Word和Klass Pointer,这里可以看到,该oop对象的Klass Pointer指针指向的是入参klass,往回追溯一下,发现该klass是该目标类对应的InstanceKlass对象,这里和类已解析流程对应上了。

此外,跟一下common_mem_allocate_init方法,可以发现,不同的垃圾回收器,其分配内存空间的方式是不同的。

执行完new指令后,我们可以看到,JVM创建了一个oop对象,这个oop对象就是我们经常说的Java对象在JVM层面的呈现方式。此外,还将oop对象中的Klass Pointer指针指向了该目标类的InstanceKlass对象。

至此我们初步得到了InstanceMirrorKlass、InstanceOop、InstanceKlass这些对象在内存的相互关系,但是,这些对象到底是什么,他们的数据结构是什么样的,我们都还不清,下面就进行详细了解吧。

Oop-Klass模型

JVM 内部基于 oop-klass模型描述一个 Java 类,将一个 Java 类一拆为二分别描述,第一个模型是 oop,第二个模型是klass。

所谓 oop,并不是 object-oriented programming (面向对象编程),而是ordinary object point(普通对象指针),它用来表示对象的实例信息,看起来像个指针,而实际上对象实例数据都藏在指针所指向的内存首地址后面的一片内存区域中。

而 klass 则包含元数据和方法信息,用来描述 Java 类或者JVM内部自带的 C++类型信息。java 类的继承信息、成员变量、静态变量、成员方法、构造函数等都在klass保存,JVM据此便可以在运行期反射出 Java 类的全部结构信息。当然,JVM本身所定义的用于描述Java类的C++类也使用Klass描述,这相当于使用另一种面向对象机制去描述C++类这种本身便是面向对象的数据。

JVM 使用 oop-klass 这种一分为二的模型描述一个Java类,虽然模型只有两种,但是其实从3个不同的维度对一个Java类进行了描述。侧重于描述Java类的实例数据的第一种模型 oop主要为Java类生成一张“实例数据视图”,从数据维度描述一个Java类实例对象中各个属性在运行期的值。而第二种模型 klass 则又分别从两个维度去描述一个Java类,第一个维度是Java类的“元信息视图”,另一个维度则是虚函数列表,或者叫作方法分发规则。元信息视图为JVM在运行期呈现 Java类的“全息”数据结构信息,这是JVM 在运行期得以动态反射出类信息的基础。

Oop

  • klass模型是java类的元数据在jvm中的存在形式,
  • oop模型就是java对象在jvm中的存在形式。

oop(ordinary object pointer,普通对象指针)。是HotSpot用来表示Java对象的实例信息的一个体系,既在JVM层面,oop用于表示对象(oop本质上是一个指向内存中对象的起始存储位置的指针)。

HotSpot里的 oop指啥

Hotspot 里的 oop其实就是GC所托管的指针,每一个oop都是一种 xxxOopDesc*类型的指针。所有 oopDesc 及其子类(除神奇的 markOopDesc 外)的实例都由GC所管理,这才是最最重要的,是oop 区分 Hotspot 里所使用的其他指针类型的地方。

对象指针之前为何要冠以“普通”二字

对象指针从本质上而言就是一个指针,指向 xxxOopDesc 的指针也是普通得不能再普通的指针,可是为何在Hotspot 领域还要加一个“普通”来修饰?要回答这个问题,需要追溯到OOP(这里的OOP是指面向对象编程)的鼻祖——SmallTalk 语言。

SmallTalk 语言里的对象也由GC来管理,但是SmallTalk 里面的一些简单的值类型对象都会使用所谓的“直接对象”的机制来实现,例如 SmallTalk 里面的整数类型。所谓“直接对象”( immediate object)就是并不在GC堆上分配对象实例,而是直接将实例内容存在对象指针里的对象。这样的指针也叫做“带标记的指针”(tagged pointer)。

这一点倒是与markOopDesc 类型如出一辙,因为markOopDesc 也是将整数值直接存储在指针里面,这个指针实际上并无“指向”内存的功能。

所以在 SmallTalk 的运行期,每当拿到一个对象指针时,都得先校验这个对象指针是一个直接对象还是一个真实的指针?如果是真实的指针,它就是一个“普通”的对象指针了。这样对象指针就有了“普通”与“不普通”之分。

所以,在 HotSpot 里面, oop就是指一个真实的指针,而 markOop 则是一个看起来像指针但实际上是藏在指针里的对象(数据)。这也正是markOop实例不受GC托管的原因,因为只要出了函数作用域,指针变量就会直接从堆栈上释放掉了,不需要垃圾回收了。

在hotspot/share/oops/oopsHierarchy.hpp 文件中,对oop的定义如下:

 

arduino

复制代码

// 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; ...

在Java应用程序运行过程中,每创建一个Java对象,在JVM内部也会相应创建一个OOP对象来表示Java对象,OOP类的共同基类型是oopDesc。根据JVM内部使用的对象类型,具有多种oopDesc子类, 其中instanceOopDesc表示普通Java对象实例,arrayOopDesc表示数组对象实例。数组对象实例下又可以分为对象数组实例objArrayOopDesc和基础数据类型数组实例typeArrayOopDesc。

instanceOopDesc表示普通Java对象实例,arrayOopDesc表示数组对象实例。数组对象实例下又可以分为对象数组实例objArrayOopDesc和基础数据类型数组实例typeArrayOopDesc。

回看下Load与new这两节,可以发现,无论是在加载(Load)阶段生成java.lang.Class类的instanceOop即java mirror时,还是在执行new字节码创建生成java对象的instanceOop时,都是调用的collectedHeap中的方法进行的内存分配与创建,因此,instanceOop都是分配在了堆内存空间中

Java对象实例instanceOopDesc的存储结构如图所示:


其主要由对象头、实例数据以及自动对齐三部分组成。

对象类的顶级基类

oopDesc是对象类的顶级基类

源码:hotspot/src/share/vm/oops/oop.hpp

 

kotlin

复制代码

// oopDesc is the top baseclass for objects classes. The {name}Desc classes describe // the format of Java objects so the fields can be accessed from C++. // oopDesc is abstract. // (see oopHierarchy for complete oop class hierarchy) // // no virtual functions allowed ... class oopDesc { ... }

翻译过来就是这个意思:

 

arduino

复制代码

// oopDesc是对象类的顶级基类。 {name}Desc类描述了Java对象的格式,因此可以从C++中访问这些字段。 // oopDesc是抽象的。 // // 不允许使用虚拟函数

在前面讲Klass模型时候说过,将klass和oop模型分开来,就不用在每个对象中维护一个VPTR,所以普通的对象不允许有虚函数

java类的实例(非数组对象)

实例是用instanceOopDesc来描述的

源码:hotspot/src/share/vm/oops/instanceOop.hpp

 

kotlin

复制代码

// An instanceOop is an instance of a Java Class // Evaluating "new HashTable()" will create an instanceOop. class instanceOopDesc : public oopDesc { ... }

对象头

从oop.hpp源码来看,对象头中包含了两部分,一部分是markOop类型的对象,用于存储对象的运行时记录信息,如hashCode、GC分代年龄、锁状态等;另一部分是Klass指针的联合体,用于指向当前对象所属的Klass对象,如果开启了指针压缩(-XX:+UseCompressedOops),则使用压缩后的narrowKlass类型指针,如果没有启用指针压缩,则使用Klass类型指针。

此处不对对象头中的内容进行展开,但其中内容值得仔细研究,涉及到很多锁、GC方面的知识

 

arduino

复制代码

// hotspot/src/share/vm/oops/oop.hpp class oopDesc { ... private: // 用于存储对象的运行时记录信息,如哈希值、GC分代年龄、锁状态等 volatile markOop _mark; // Klass指针的联合体,指向当前对象所属的Klass对象 union _metadata { // 未采用指针压缩技术时使用 Klass* _klass; // 采用指针压缩技术时使用 narrowKlass _compressed_klass; } _metadata; ... }

源码里的 _mark 和 _metadata两个字段就是对象头的定义,分别表示对象头中的两个基本组成部分

  • _mark 是markOop类型的对象,用于存储对象的运行时记录信息,如hashCode、GC分代年龄、锁状态等
  • _metadata是个共用体(union),用于指向当前对象所属的Klass对象,如果开启了指针压缩(-XX:+UseCompressedOops),则使用压缩后的narrowKlass类型指针,如果没有启用指针压缩,则使用Klass类型指针。
数组对象

源码:arrayOop.hpp

 

kotlin

复制代码

// 数组Oops的布局是。 // // markOop // Klass* // 32位,如果压缩,但在LP64中声明为64位。 // length // 共享klass内存或在声明的字段后分配。 class arrayOopDesc : public oopDesc {..}

分为基本数据类型的数组对象和引用类型的数组对象

基本数据类型

通过TypeArrayDescOop来描述

源码:hotspot/src/share/vm/oops/typeArrayOop.hpp

 

arduino

复制代码

// A typeArrayOop is an array containing basic types (non oop elements). // It is used for arrays of {characters, singles, doubles, bytes, shorts, integers, longs} #include <limits.h> class typeArrayOopDesc : public arrayOopDesc { ... }

引用数据类型

通过ObjArrayOopDesc描述

源码:hotspot/src/share/vm/oops/objArrayOop.hpp

 

kotlin

复制代码

// An objArrayOop is an array containing oops. // Evaluating "String arg[10]" will create an objArrayOop. class objArrayOopDesc : public arrayOopDesc { ... }

oop提供4个方法来判断当前对象处于何种状态下:

 

arduino

复制代码

// hotspot/src/share/vm/oops/oop.hpp class oopDesc { ... bool is_locked() const; bool is_unlocked() const; bool has_bias_pattern() const; ... bool is_gc_marked() const; } // hotspot/src/share/vm/oops/oop.inline.hpp ... inline bool oopDesc::is_gc_marked() const { return mark()->is_marked(); } inline bool oopDesc::is_locked() const { return mark()->is_locked(); } inline bool oopDesc::is_unlocked() const { return mark()->is_unlocked(); } inline bool oopDesc::has_bias_pattern() const { return mark()->has_bias_pattern(); } ...

从上述代码可知,oop调用markOop的方法来判断当前对象是否已经加锁、是否是偏向锁,markOop则通过判断其存储结构中的标志位来实现,如下列代码所示:

 

csharp

复制代码

// hotspot/src/share/vm/oops/markOop.hpp class markOopDesc: public oopDesc { ... // unlocked_value = 1 // lock_mask_in_place = right_n_bits(2),is_locked()利用存储结构的最右边两位 // 来判断当前对象是否是加锁状态。值得注意的是,偏向锁并不属于加锁状态。 bool is_locked() const { return (mask_bits(value(), lock_mask_in_place) != unlocked_value); } // lock_mask_in_place = right_n_bits(3),is_unlocked()并不是简单地对is_locked() // 的结果取反,而是使用存储结构的最右边三位来判断。值得注意的是,偏向锁也并不属于无锁状态。 bool is_unlocked() const { return (mask_bits(value(), biased_lock_mask_in_place) == unlocked_value); } // marked_value = 3 // lock_mask_in_place = right_n_bits(2),当锁标志位的值为3(二进制为11)时返回true。 bool is_marked() const { return (mask_bits(value(), lock_mask_in_place) == marked_value); } // biased_lock_pattern = 5 // biased_lock_mask_in_place = right_n_bits(3),当存储结构的最后三位的值为5(二进制 // 为101)时返回true bool has_bias_pattern() const { return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern); } ... }

Mark word(_mark)

_mark 是markOop类型的对象,用于存储对象的运行时记录信息,如hashCode、GC分代年龄、锁状态等

Mark word是整个对象头中最复杂的部分。首先Mark word是一个非固定的数据结构,以便在最小的空间里存储更多的信息。它的格式就像图中展示的这样。

对象HashCode是一个25位的对象标识码,通过 System.identifyHashCode() 生成,我们知道Object类也有一个生成HashCode的方法hashCode() ,它们之间的区别是无论对象是否重写了hashCode方法,identityHashCode都会返回对象的HashCode。

对象分代年龄这个部分需要关注的是这个值占用的空间大小是4位,这意味着它的最大值是15。那什么时候用它呢?在JVM采用分代收集的垃圾回收算法时,它会记录对象在Survivor区被复制的次数。在YGC中,伴随着每次GC,对象在Survivor区被复制一次,这个值也会加1。当对象被复制的次数超过一定的阈值时,它就会被复制到老年代,这个阈值是通过 **-XX:MaxTenuringThreshold** 来设置的 。默认情况下并行GC时的阈值是15。

锁相关

偏向锁用来标识对象是否启用偏向锁标记,这个只占用 1 位二进制,0表示对象没有使用偏向锁,1 表示对象使用了偏向锁。

  • 自旋锁大意是指线程不进入阻塞等待,而只是做自旋等待前一个线程释放锁。不在对象头讨论范围之列,这里不做讨论。

  • 偏向锁,是针对只会有一个线程执行同步代码块时的优化,如果一个同步块只会被一个线程访问,则偏向锁标记会记录该线程id,当该线程进入时,只用check 线程id是否一致,而无须进行同步。锁偏向后,会依据epoch(偏向时间戳)及设定的最大epoch判断是否撤销锁偏向。

  • 轻量级锁:当某个资源在没有竞争或极少竞争的情况下,JVM会优先使用CAS操作,让线程在用户态去尝试修改对象头上的锁标记位,从而避免进入内核态。这里CAS尝试修改锁标记是指尝试对指向当前栈中保存的lock record的线程指针的修改,即对biased_lock标记做CAS修改操作。如果发现存在多个线程竞争(表现为CAS多次失败),则膨胀为重量级锁,修改对应的lock标记位并进入内核态执行锁操作。注意,这种膨胀并非属于性能的恶化,相反,如果竞争较多时,CAS方式的弊端就很明显,因为它会占用较长的CPU时间做无谓的操作。此时重量级锁的优势更明显。

  • 重量级锁:即对象监视器锁。Java在使用synchronized关键字对方法或块进行加锁时,会触发一个名为“objectMonitor”的监视器对目标代码块执行加锁的操作。当然synchronized方法和synchronized代码块的底层处理机制稍有不同。synchronized方法编译后,会被打上“ACC_SYNCHRONIZED”标记符。而synchronized代码块编译之后,会在同步代码的前后分别加上“monitorenter”和“monitorexit”的指令。当程序执行时遇到到monitorenter或ACC_SYNCHRONIZED时,会检测对象头上的lock标记位,该标记位被如果被线程初次成功访问并设值,则置为1,表示取锁成功,如果再次取锁再执行++操作。在代码块执行结束等待返回或遇到异常等待抛出时,会执行monitorexit或相应的放锁操作,锁标记位执行--操作,如果减到0,则锁被完全释放掉

元数据信息(_metadata)

_metadata是个共用体(union),这个地方存储的就是指向Klass的指针,如果开启了指针压缩(-XX:+UseCompressedOops),则使用压缩后的narrowKlass类型指针,如果没有启用指针压缩,则使用Klass类型指针。

通过这个指针,JVM 知道当前这个对象是哪一个类的实例。

实例数据

实例数据也叫做 Instance Data,这个地方维护着我们在代码中创建的对象的实例字段。在JVM中对象字段主要分为基本数据类型和引用类型两种。

JVM将Java对象的成员变量也保存在了oop对象中,并提供了一系列的get、set方法,如果成员变量是非基础类型,即普通对象,oop中保存的是其在内存中的地址。

自动对齐

JVM要求Java的对象占用的内存大小应该是8的倍数个字节,所以在对象头+实例数据所占的内存不满足8的倍数个字节是,会使用一定数量的字节将对象所占内存的大小补充至8的倍数个字节。

JVM是一个虚拟机,它的能力来源于底层的CPU、操作系统,同时这些底层基础设施的特性又会反过来影响JVM中的特性。

首先我们要明确一点,CPU是以字为单位读取数据的,而缓存中的数据是以cacheline为单位从内存中同步到CPU缓存中的。所以内存里数据的存储和存取方式会对CPU的读取产生影响。

计算机的内存是由连续的字节地址组成的,而CPU在读取或写入数据时,并不是以单个字节为单位操作的,而是以字(word)、双字(double word)或其他更大的数据块为单位。这种操作的最小单位称为CPU的“字长”。例如,许多现代CPU的字长是32位或64位,意味着它们最高效地处理的数据块大小是4字节或8字节。

数据对齐指的是数据在内存中的起始地址能够整除数据类型大小的情况。比如,一个4字节的整数(int)最好存储在地址能被4整除的位置上,这样CPU就可以一次性准确读取整数的所有字节,无需进行两次读取然后拼接。如果这个整数的地址不是4的倍数,比如从地址0x0002开始,CPU在尝试读取时,会先读取从0x0000到0x0003的4个字节,再把地址空间是0x0004-0x0007的部分读取出来,把不需要的数据去掉才能读取到需要的数据。然后只保留它实际需要的0x0002到0x0005的部分。这样做不仅浪费了时间(因为它读取了额外的数据),还可能导致性能下降,因为额外的处理步骤(丢弃不需要的数据部分)。

为了避免这种情况,操作系统和编译器通常会自动对数据进行对齐,确保数据存储的地址符合其自然边界。例如,字节、短整型(short,通常2字节)会被存储在偶数地址上,整型(int)则在4的倍数地址上。虚拟机(如Java虚拟机)也会遵循类似的对齐规则,以优化数据访问效率。

数据对齐是为了提高CPU访问内存的效率,减少不必要的读取和处理步骤,确保数据能够被快速、无损地读取或写入。这是系统层面的一种优化策略,通常对开发者透明,但对程序的性能有重要影响。

在HotSpot里这个值是8。HotSpot虚拟机要求所有的对象大小都是8字节的倍数,对象填充区域起到的就是补齐填充的作用,这也是一个很经典的以空间换时间思想的应用。

总结

OOP模型指的是 JVM 在内存中管理 Java 对象的方式。在 JVM 中,OOP 模型被用来实现 Java 程序语言的面向对象特性,包括封装、继承、多态等。JVM 中的 OOP 模型定义了内存布局和对象的生命周期,以及如何使用内存来实现 Java 对象的动态分配、初始化和销毁。在 JVM 中,Java 对象被存储在堆内存中,并由 JVM 负责管理和回收

Klass

Java的每个类,在JVM中都有一个对应的Klass类实例与之相对应,它是用C++实现的,用来存储类的元信息,比如常量池、属性信息、方法信息等。一个class文件被JVM加载之后,就会形成一个Klass对象存储在内存中

A Klass provides:

  1. language level class object (method dictionary etc.)
  2. provide vm dispatch behavior for the object

以上是HotSpot JVM对Klass的定义klass.hpp,可以看出,Klass类主要提供了两个功能:

  1. 用于表示Java语言层面的类的对象,其中保存了Java对象的类型信息,包括类名、限定符、常量池、方法字典等。一个class文件被JVM加载之后,就会形成一个Klass对象存储在内存中
  2. 为Java对象提供方法调用分派机制

与Oop一样,Klass也有一个继承体系,并在oopsHierarchy.hpp中进行了相关描述:

与Oop一样,Klass也有一个继承体系,并在oopsHierarchy.hpp中进行了相关描述:

其各自功能如下:

  1. instanceKlass:表示一个普通Java类,包含了该类运行时所有信息,即运行时数据结构
    1. instanceMirrorKlass:表示java.lang.Class类的运行时数据结构
    2. instanceMirrorKlass只用于描述java.lang.Class这个特定的类的运行时数据结构
    3. instanceClassLoaderKlass:表示类加载器ClassLoader体系下所有类的运行时数据结构
  1. instanceRefKlass:表示java.lang.ref.Reference类及其子类的运行时数据结构
    1. arrayKlass:表示数组类的运行时数据结构
    2. objArrayKlass:表示对象数组的运行时数据结构
    3. typeArrayKlass:表示基础数据类型数组的运行时数据结构

Klass类主要数据结构:

如上述代码片段所示,Klass继承了Metadata,而Metadata继承了MetaspaceObj,MetaspaceObj就是JDK 1.8中“元空间”的实现,这就意味着,Java对象的类型信息存储在元空间,而不是在堆中。另外,Klass中还存储了当前类所属的java.lang.Class对象对应的oop,即java mirror,以及其父类、子类的Klass指针。

InstanceKlass继承了Klass,其主要的数据结构:

其中,_methods数组对象代表该类的所有方法;_fields成员变量代表当前类所有字段信息,包括成员变量和静态变量,但是不是保存的变量的真实内容。_fields中的每个元素代表了当前field的偏移量信息,这些偏移量用于在oop中找到对应field的地址。成员变量的field偏移量用于在Java Object对应的oop中找到对应的field地址;静态变量的field偏移量用于在java mirror对应的oop中找到对应的field地址。

InstanceKlass中有两个字段:_vtable_len与_itable_len,其分别代表了该类的虚函数表的长度及接口函数表的长度,与之相关的虚函数表vatble、接口函数表itable是Java实现多态的基础,有兴趣的同学可以了解一下。

至此,我们了解了Oop与Klass的相关概念,以及其各自所处的内存区域,那么我们可以通过一段具体的代码示例,来描述Java对象模型了。代码示例:

Klass类主要数据结构:

从上图的继承关系可知,元信息是存储在元空间的,也就是JDK8中方法区的实现。

MetaspaceObj

对应的c++代码在src/share/vm/memory/allocation.hpp中:

 

kotlin

复制代码

// Base class for objects stored in Metaspace. // Calling delete will result in fatal error. // // Do not inherit from something with a vptr because this class does // not introduce one. This class is used to allocate both shared read-only // and shared read-write classes. // class MetaspaceObj {...}

注释已经明确说明了它是存储在元空间中的对象的基类

Metadata

对应的c++代码在src/share/vm/oops/metadata.hpp中:

 

kotlin

复制代码

// This is the base class for an internal Class related metadata class Metadata : public MetaspaceObj {...}

注释明确说明了它是内部class相关元数据的基类

Klass

对应的c++代码在src/share/vm/oops/klass.hpp中:

 

kotlin

复制代码

// A Klass provides: // 1: language level class object (method dictionary etc.) // 2: provide vm dispatch behavior for the object // Both functions are combined into one C++ class. class Klass : public Metadata {...}

从注释中可知,这个类提供语言级别的类对象,并且为该对象提供虚拟机的调度行为,它对应的是c++中的类对象。

这里要注意klass内存布局中的虚表指针

 

less

复制代码

// One reason for the oop/klass dichotomy in the implementation is // that we don't want a C++ vtbl pointer in every object. Thus, // normal oops don't have any virtual functions. Instead, they // forward all "virtual" functions to their klass, which does have // a vtbl and does the C++ dispatch depending on the object's // actual type. (See oop.inline.hpp for some of the forwarding code.) // ALL FUNCTIONS IMPLEMENTING THIS DISPATCH ARE PREFIXED WITH "oop_"! // Klass layout: // [C++ vtbl ptr ] (contained in Metadata) // ...

通过注释可知,为什么jvm将klass和oop(java中的对象在jvm中对应的c++对象)分开,这样就不用为每个对象都维护一个VPTR,所以普通的oops对象没有任何的虚函数,它们将所有的虚函数转发给对应的Klass对象,而Klass对象中有一个VTPR,它根据对象的实际类型进行C++调度。虚表转发是多态的底层实现。

instanceKlass

java类在jvm中的表示,它包含在执行运行时类所需的所有信息

InstanceKlass继承了Klass,其主要的数据结构:

_methods数组对象代表该类的所有方法;_fields成员变量代表当前类所有字段信息,包括成员变量和静态变量,但是不是保存的变量的真实内容。_fields中的每个元素代表了当前field的偏移量信息,这些偏移量用于在oop中找到对应field的地址。成员变量的field偏移量用于在Java Object对应的oop中找到对应的field地址;静态变量的field偏移量用于在java mirror对应的oop中找到对应的field地址。

InstanceKlass中有两个字段:_vtable_len与_itable_len,其分别代表了该类的虚函数表的长度及接口函数表的长度,与之相关的虚函数表vatble、接口函数表itable是Java实现多态的基础。

instanceKlass它有三个子类:

  • InstanceMirroKlass:每个java类关联的java.lang.Class实例,java代码中获取到的class对象(反射的原理),实际上就是这个C++的实例,它存值在堆区,学名镜像类

源码中是这样解释这个类的:

InstanceMirrorKlass是一个专门的InstanceKlass,用于java.lang.Class实例的专门实例类。 这些实例是特殊的,因为它们除了包含类的静态字段外,还包括类的正常字段。 这意味着它们是尺寸可变的实例,需要特殊的逻辑来计算它们的大小和迭代它们的OOPS。

  • instanceRefKlass:用于表示java/lang/ref/Reference类的子类(Reference是抽象类)

源码中对该类的解释:

InstanceRefKlass是一个专门用于Java类的InstanceKlass,是java/lang/ref/Reference的子类。

这些类被用来实现soft/weak/final/phantom引用和终结,并且需要垃圾收集器的特殊处理。

在GC过程中,被发现的引用对象被添加(链接)到下面四个列表中的一个,这取决于引用的类型。链接是通过java/lang/ref/Reference类中的下一个字段发生的。

之后,被发现的引用按照可达性的递减顺序进行处理。符合通知条件的引用对象被链接到类java/lang/ref/Reference中的静态pending_list,同一类中的pending list锁对象被通知。

  • instanceClassLoaderKlass:用于遍历某个加载器加载的类
为什么有了instanceKlass后,还需要instanceMirroKlass呢?

类的元信息我们是可以通过反射来获取的,实际上我们更需要的是这个class对象,分开的目的就是防止绕过一些安全检查。

ArrayKlass

所有数组类的抽象基类。

java中的数组不是静态数据类型,它不像String,有java.lang.String对应。它是动态的数据类型,也即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:

  • TypeArrayKlass:用于表示基本类型(8种)的数组
  • ObjArrayKlass:用于表示引用类型的数组
HSDB工具

我们使用hsdb工具来查看一个JAVA类对应的C++类,来验证上面的内容

 

arduino

复制代码

public class Hello { public static final int a=3; public static int b=5; public static String c="sdf"; public static int d; public final int e=0; public static void main(String[] args) { int[] intArr=new int[1]; Hello[] hellos=new Hello[1]; Hello hello=new Hello(); Class<Hello> helloClass = Hello.class; System.out.println("hello"); while (true); } }

运行上述代码,然后进入jdk的lib目录下,执行

 

bash

复制代码

java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

然后使用jps -l命令查看运行的java程序的pid,然后在HSDB中使用attach连接该id即可。

1、查看非数组类。两种方式

  • 类向导:ClassBrowser

通过类向导找到该类对应的地址,然后点击inspector,输入

  • 对象

点击main线程,点击堆栈信息,将对象的地址输入到inspector中

可以看到,上面的结果是Oop,也就是对象模型,每一个类所对应的对象,这个后面再说。其中_metadata(是一个联合体)中的_compressed_klass就是类型指针,通过这个类型指针就可以知道该对象属于哪个类。这里涉及到了指针压缩。其中_mark是MarkOopDesc,为1表示当前对象处于无锁状态(001,0代表未偏向)。

另外,在上图中也可以看到栈的结构,最下面对应的就是方法参数,然后往上依次是我们在代码中定义的int数组、对象数组、对象、class对象...

我们可以查看该对象所对应类的Class对象,也就是instanceMirrorKlass:

这里会发现上面没有e字段,因为e没有被static修饰,所以在类对象中看不到,但在上面对象所对应的oop模型中可以看到

2、查看数组。

在上面说过,数组是一种动态类型,所以通过类向导是找不到的,只有通过堆栈这种方式。

可以看到,基本数据类型对应的typeArrayKlass,其中I代表的是描述符,这里是Int类型,所以对应的是I。而引用类型对应的是ObjArrayKlass。

总结

Klass代表了Java类在JVM中的内存映像。Klass 模型是 JVM 实现 Java 类加载、存储和执行的核心部分,用于管理类的元数据和实例,包括类的定义、字段、方法、常量池等信息。Klass 模型的作用是统一管理 Java 类的元数据,使得 JVM 能够快速访问这些元数据,从而实现 Java 类的加载、链接和初始化。此外,Klass 模型还提供了 JVM 实现 Java 虚拟机的一些高级特性,例如内存管理、垃圾回收、类型安全等。

多态

什么是多态

多态是OOP中的核心思想,当我们使用基类的引用调用基类中定义的一个函数时,并不知道该函数真正作用的对象是什么类型。它可能是基类的对象也可能是派生类的对象,这在编译时期是判断不了的,只有在运行时期,才能根据引用或指针所绑定的对象的真实类型来决定。

C++中的多态实现

在c中每个类都会有一个虚函数表(每个类仅有一个),所有的对象共用这张表。c中的函数多态就是通过虚函数来实现的。

要理解具体的实现,首先得要理解c++中的一些概念:

虚函数&非虚函数

在面向对象编程中,虚函数(Virtual Function)和非虚函数(Non-Virtual Function)是用来描述类成员函数的两个术语,它们主要区别在于多态性的支持方式上。

  1. 虚函数(Virtual Function):
    • 虚函数是为了实现动态多态性而设计的。当一个基类的成员函数被声明为虚函数时,其在派生类中可以被重写(Override)。
    • 通过基类指针或引用来调用一个虚函数时,实际执行的是指针或引用所指向的具体对象(即派生类对象)的该函数版本。这意味着在运行时能够根据对象的实际类型来决定调用哪个函数,这是多态性的体现。
    • 在函数前加上virtual,如果一个函数 在基类中被声明为virtual,那么在它的所有派生类中它都是virtual的,而在派生类中对virtual函数的重定义称为重写。也就是说,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数。注意,子类可以不用重写父类中的虚函数。
  1. 非虚函数(Non-Virtual Function):
    • 非虚函数是默认的函数行为,即没有被声明为虚函数的成员函数。
    • 对于非虚函数,编译器在编译时期就确定了函数调用的绑定,这称为静态绑定或早期绑定。这意味着,如果通过基类指针或引用来调用一个非虚函数,无论该指针或引用实际指向的是基类对象还是派生类对象,调用的总是基类中的那个函数实现。
    • 非虚函数通常用于那些不需要在派生类中重写的行为,或者在性能敏感场景下避免虚函数表带来的开销。

虚函数支持运行时的动态多态性,允许派生类重写基类的行为;而非虚函数则不具备这一特性,调用时基于对象的静态类型。选择使用虚函数还是非虚函数,需根据设计需求和性能考量来决定。

纯虚函数

不能被用来声明对象,是抽象类。带有纯虚函数的类就是抽象类。子类必须重写父类中的虚函数,除非子类也是抽象类。

绑定

把函数体和函数调用相联系称为绑定。又分为早绑定和晚绑定

  • 早绑定:绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定
  • 晚绑定(动态绑定、运行时绑定):早绑定不能实现多态,这时候需要晚绑定,根据对象的类型,发生在运行时。当使用基类的引用调用一个虚函数时将发生动态绑定,所以晚绑定只对虚函数起作用。

如何实现晚绑定

虚表

关键字virtual告诉编译器它不应当执行早绑定,相反,它应当自动安装对于实现晚绑定必须的所有机制。为了达到这个目的,编译器对每个包含虚函数/纯虚函数的类创建一个虚表(VTABLE) ,在虚表中,编译器会放置特定类的虚函数地址。每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个唯一的虚表VTABLE,这个表中放置了在这个类中或它的基类中所有已声明为virtual函数的地址

虚表指针

那么如何找到这个表呢?答案是通过虚表指针

在每个带有虚函数的类中,编译器会放置一个VPTR,指向这个对象的VTABLE。当通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入能取得这个VPTR并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数并引起晚捆绑的发生

如何取出对象的VPTR?

对于所有的基类对象或右基类派生的对象,它们的VPTR都在对象的相同位置(常常在对象的开头),所以编译器能够取出这个对象的VPTR。VPTR指向VTABLE的起始地址。所有的VTABLE都具有相同的顺序(按照声明的顺序),即虚方法在类中的顺序是确定的,这样就可以通过VPTR和虚方法在虚表中的地址偏移找到它

要注意:

  1. 如果子类继承了父类,但是并没有重写父类中的虚函数,那么在虚表中父类的虚函数在子类的虚函数前面
  2. 如果子类继承父类并且重写父类中的虚函数,那么子类中重写父类的函数会放到虚表中原来父类虚函数的位置,没有被重新的函数依旧。
  3. 如果是多继承,那么每个父类都有自己的虚表,子类的成员函数被放到第一个父类(按照声明顺序)的表中。

假设现在子类调用了父类的虚函数N并且没有重写,那么就能根据虚表指针和该虚函数在虚表中的地址来确定到底调用的是哪个函数。

如果子类重写了该方法:

虚表分发

就是上面这两张图的过程,通过虚表内存地址拿到虚表记录,然后通过函数名+参数信息到虚表中去找。因为是从前往后找,所以如果子类重写了父类方法,那么就会调用子类的方法。并且从上面的图可以看出,虚表其实就是数组实现的,前面的数字就是索引

Java中的多态实现

在Java编程语言中,并没有“虚函数”的概念,具体来说就是你不能在Java 源代码中使用“virtual”这个关键字去修饰一个Java方法。而有过C++编程经验的小伙伴都知道,C++实现面向对象多态性的关键字主要就是virtual。这本来与Java毫无关系,毕竟是两种不同的语言,谁也无权强制要求别人为了支持多态就一定要通过“virtual”这个关键字。

但是不巧的是,JVM在内部使用 C++类所定义的一套对象机制去表达 Java 类的面向对象机制,这一表达顺手就把Java类的多态机制也包含在内了,毕竟Java号称是比C++更加纯粹的面向对象的语言,结果总不能连个多态都不支持吧。但是爷非但不走寻常路,而且还把正常的路径给堵住了不让走,就是不让Java 支持 virtual 的概念。但是这样就会带来一个问题,Java类最终被表达成了JVM内部的C++类,并且Java类方法的调用最终要通过对应的C++类,但是Java语言是面向对象的,多态性是其基本特性,这意味着JVM内部的C++类要能够支持Java语言的多态性,可是Java 方法并不支持使用virtual 这个关键字来修饰,这样问题就来了,C++层面怎样才能知道Java类中的哪个方法是虚函数,哪个方法不是虚函数呢?换言之,当面对一个多重继承的Java类体系时,JVM内部的C++类怎么才能将这种多态性表达出来呢?设计者的做法很简单粗暴,那就是将 Java 类的所有函数都视为是 virtual 的,这样 Java 类中的每个方法都可以直接被其子类、子子类覆盖而不需要增加任何关键字作修饰符。正因为如此,Java 类中的每个方法都可以晚绑定,只不过对于一些确定的调用,在练译期便能实现早绑定

jvm是用c写的,所以java中多态实现跟c的多态实现是相似的,比如虚表也是数组实现的,但是还是有不一样的地方。

虚表在哪

因为java中的类,在jvm中对应的c对象是klass模型,java中的对象,在jvm中对应的c对象是oop模型。前面我们知道,c++中的虚表是由VPTR指向的,而这个VPTR在类对象的头部。所以jvm中的虚表在klass模型的头部,即java类对象的头部

我们看下klass.hpp源码:

 

csharp

复制代码

Klass layout: [C++ vtbl ptr ] (contained in Metadata) [layout_helper ] [super_check_offset ] for fast subtype checks [name ] [secondary_super_cache] for fast subtype checks [secondary_supers ] array of 2ndary supertypes [primary_supers 0] [primary_supers 1] [primary_supers 2] ...

可以看到,klass内存布局的第一个就是虚表指针。

jvm中的虚表存的是什么

我们知道java中是没有虚函数这个概念的,c++中的虚函数其实对应的就是java中的普通函数,而纯虚函数对应的就是java中的抽象函数。所以在java中,随便定义一个类,它是有虚表的,关键是这个虚表中存放的是哪些方法地址。而只有被public、protect类型的,且不被static、final修饰的方法才能被多态调用,所以才会进入虚表。

比如Object类,它里面满足这些条件的方法都会进入虚表,我们可以通过HSDB工具查看:

jvm如何实现虚表分发

在多态中,父类引用指向子类对象,然后进行方法调用,所以有必要了解jvm中的方法调用。在c++中有直接调用和间接调用,jvm则抽象成了4个指令来完成:

  1. invokevirtual:这个指令用于调用public、protected修饰,且不被static、final修饰的方法。跟多态机制有关。
  2. invokeinterface:跟invokevirtual差不多。区别是多态调用时,如果父类引用是对象,就用invokevirtual。如果父类引用是接口,就用这个。
  3. invokespecial:只用于调用私有方法,构造方法。跟多态机制无关。
  4. invokestatic:只用于调用静态方法。与多态机制无关。

ps:现在又新增了一条指令invokedynamic,该指令用于调用动态方法

这里主要说下第2个指令,因为它和其他指令不一样,其他指令的后面就是2个操作数(常量池索引),拿着操作数去常量池中就可以找到类信息、方法信息,但是invokeinterface后面的操作数占了4个字节。第一个操作数和第二个操作数加起来就是常量池的索引,对应常量池项CONSTANT_InterfaceMethodref,它包含了接口方法的名称和描述符,以及对该方法所在接口的符号引用。第3个操作数是方法的参数个数,需要注意非静态方法就算没有参数,也会默认有一个,就是this指针,long和double类型的参数占用2个数量单位,其实这些信息完全可以从方法的描述符中获取到,之所以有这个参数完全是历史原因。第4个操作数用于维持向后兼容性

所以在调用的时候,会通过这个this指针从操作数栈拿到真正的对象,然后通过对象头中的类型指针拿到该类对应的C类对象,即klass模型,然后通过函数名+参数信息及返回值信息根据VPTR去虚表中找,如果子类重写了父类方法,那么就会调用子类方法,和上面c中的虚表分发是一样的逻辑,这就是jvm虚表分发的底层实现。

多态的机制

本质上多态分两种:

  1. 编译时多态(又称静态多态)
  2. 运行时多态(又称动态多态)

重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行时运行的时候调用的是确定的方法。

我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。

多态的用途

多态最大的用途我认为在于对设计和架构的复用,更进一步来说,《设计模式》中提倡的针对接口编程而不是针对实现编程就是充分利用多态的典型例子。

定义功能和组件时定义接口,实现可以留到之后的流程中。同时一个接口可以有多个实现,甚至于完全可以在一个设计中同时使用一个接口的多种实现(例如针对ArrayList和LinkedList不同的特性决定究竟采用哪种实现)。

总结

c++的多态是通过对存在虚函数的类创建一个虚表,然后通过虚表分发实现。

而jvm中,因为java没有虚函数的概念,所以jvm中的虚表中存的方法也有区别,并且jvm将c++中的方法调用抽象成了4个调用指令,而与多态相关的就只有invokevirtualinvokeinterface指令,执行这些指令,会将this传递过来,通过它就可以往后找到虚表中的方法地址,从而完成运行时的方法调用。

DEMO

至此,我们了解了Oop与Klass的相关概念,以及其各自所处的内存区域,那么我们可以通过一段具体的代码示例,来描述Java对象模型了。

DEMO1

代码示例:

 

typescript

复制代码

class Animal{ public void say(){ System.out.println("this is animal"); } } class test { public static void main(String[] args){ Animal animalA = new Animal(); Animal animalB = new Animal(); } }

这两行代码后,通过前面几节的分析,内存中的对象布局为:

  • 在创建了两个Animal类型的对象animalA和animalB后,在堆内存中,会有两个instanceOop类型的java对象instanceOop:animalA和instanceOop:animalB,它们中的_klass指针都指向了元空间中描述Animal类运行时数据结构的instanceKlass对象instanceKlass:Animal(一个)。
  • instanceKlass:Animal中的methods数组中包含一个指向描述了say方法的Method对象的Method指针,此外instanceKlass:Animal中的_java_mirror指向了堆内存中的Animal类对应java.lang.Class对象instanceOop:java mirror-Animal(一个)。
  • instanceOop:java mirror-Animal中的_klass指针又指向了元空间中的java.lang.Class类的运行时数据结构instanceMirrorKlass对象instanceMirrorKlass:java.lang.Class(一个)。

DEMO2

 

arduino

复制代码

public class Book { private String name ; public Book(){ } public Book(String bookName){ name = bookName; } public String getName(){ return name; } public void print(){ System.out.println("Common Book"); } public static void main(String[] args) { int bookCnt = 0; Book book1 = new Book("Java Programming Book "); } }

我们先来看一下上面这段代码 ,通过代码的内容我们能够知道Book对象的运行时数据是如何存储的

具体如下图所示:

对应到上面Book的例子。

OOP-Klass和Java对象模型是很多实现的基础,其中一个很重要的应用就是多态。我们知道面向对象编程有三大特性,分别是封装、继承和多态。而OOP-Klass就是多态的底层实现原理。

下面我就通过重写来介绍下JVM背后是如何通过OOP-Klass和Java对象模型来实现的。还是以上面的Book为例,我们来实现一个Book的子类。

 

java

复制代码

public class ColorBook extends Book { public void print(){ System.out.println("Color Book"); } public static void main(String[] args) { Book book = new Book(); Book colorBook = new ColorBook(); book.print(); colorBook.print(); } }

 

css

复制代码

Common Book Color Book

我们发现这个地方打印出来的分别是Common Book和Color Book。也就是说,我们虽然定义colorBook的类型是Book,但是它还是准确定位到了ColorBook这个类,那它是如何做到的呢?这就涉及到了Klass模型里的函数表功能。

函数表(Function Table)也叫做方法表(Method Table),是Klass的一部分。它是一个数组,存储了类的方法的地址指针。每个方法在函数表里都有一个条目,用来表示该方法的地址。通过函数表,Java虚拟机能够根据方法的索引或名称来查找并调用相应的方法。Klass里的函数表和Java类里的方法是一一对应的关系。函数表里的每个条目都对应着Java类里的一个方法,它存储了方法的地址,以便在运行时进行动态绑定和方法调用。

通过Klass里的函数表,Java虚拟机可以实现多态性。当通过父类的引用调用方法的时候,Java虚拟机会根据实际对象的类型,在函数表里查找对应的方法地址,然后进行方法调用。Java 采用的这种动态绑定机制,是实现多态特性的重要手段之一。

你可以结合图示来理解,ColorBook拷贝了一份Book函数表,使它的函数表指针指向新的函数表,因为ColorBook覆写了Book的函数print(),所以把函数表里覆写函数的函数指针替换成了ColorBook覆写的函数指针,而被调用函数在函数表里偏移量是固定的,这就是多态功能的原理。

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值