C++的动态分派在HotSpot VM中的重要应用

c++,动态,分派,hotspot,vm,重要,应用 · 浏览次数 : 128

小编点评

**C++ 的动态分派** * 使用 `virtual`关键字声明虚函数,并在派生类中重写虚函数时,根据接收者类型动态选择方法。 * 虚函数通过 `vfptr` 指针指向虚函数表,虚函数表包含指向实际方法的指针。 **Java 的动态分派** * 方法默认是动态分派的,当子类重写父类的方法时,方法会根据接收者类型自动调用相应的虚方法。 * 虚函数表是一个类属性,包含指向虚方法的指针。 **区别** | 特性 | C++ | Java | |---|---|---| |虚函数声明 | `virtual` | `public` | | 方法重写 | 重载父类方法 | 重写父类方法 | | 方法类型 | 虚函数 | 方法 | | 方法表 | 指向虚函数表 | 指向方法表 | | 静态绑定 | 否 | 是 | | 动态绑定 | 是 | 否 |

正文

众所周知,多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。C++ 和 Java 作为当前最为流行的两种面向对象编程语言,其内部对于多态的支持对于单继承的实现非常类似。

首先来体现一下C++的动态分派,如下:

class Base1{
  
public: 
 int base1_var1; 
 int base1_var2; 
  
 void func(){}  
};

C++中有函数的动态分派,就类似于Java中方法的多态。而C++实现动态分派主要就是通过虚函数来完成的,非虚函数在编译时就已经确定调用目标。C++中的虚函数通过关键字virtual来声明,如上函数func()没有virtual关键字,所以是非虚函数。
查看内存布局,如下:

1>  class Base1  size(8):
1>   +---
1>   0   | base1_var1
1>   4   | base1_var2
1>   +---

非虚函数不会影响内存布局。 

class Base1{
  
public: 
 int base1_var1; 
 int base1_var2; 
  
 virtual void base1_fun1() {}
};

内存布局如下:

1>  class Base1  size(16):
1>   +---
1>   0   | {vfptr}
1>   8   | base1_var1
1>  12   | base1_var2
1>   +---

在64位环境下,指针占用8字节,而vfptr就是指向虚函数表(vtable)的指针,其类型为void**, 这说明它是一个void*指针。类似于在类Base1中定义了如下类似的伪代码:

void* vtable[1] = {  &Base1::base1_fun1  };
 
const void**  vfptr = &vtable[0];

这个非常类似于Java虚拟机中对Java方法动态分派时的虚函数表(可参看深入剖析Java虚拟机:源码剖析与实例详解》一书第6.3节)。

虚函数表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚函数表即可。同一个类的所有对象都使用同一个虚函数表。为了指定对象的虚函数表,对象内部包含一个虚函数表的指针,来指向自己所使用的虚函数表。​为了让每个包含虚函数表的类的对象都拥有一个虚函数表指针,编译器在类中添加了一个指针,用来指向虚函数表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚函数表。

从如上的例子我们应该能够得到如下一些结论:

  1. C++的动态分派需要明确用virtual关键字指明,而Java中的方法默认就是动态分派,这也体现出两种语言设计理念的不同,C++不想让用户为用不到的功能付出代价,Java更看重开发效率,将一些复杂的底层控制全权托管给虚拟机;
  2. C++中只要有虚函数,就会在类中有一个虚函数表,而且类的每个实例都会多出一个指向虚函数表的指针。

对于第2点来说,HotSpot VM的设计者充分考虑了这个情况,所以在一些类的设计上能不用虚函数就绝对不用,例如oop继承体系下的所有类都不会用虚函数,因为有一个Java实例就会有一个oop,现在的应用程序一般都有过千万的实例,那我们可以算一下,每个实例要多出8个字节存储指向虚表的指针,那要消耗掉多少内存呢?!

下面我们来谈谈第1点提到的动态分派。先来看一下单继承情况下C++的动态分派。

class Person{ 
     . . . 
 public : 
    void init(){} // 非virtual方法
    virtual void sing (){}; 
    virtual void dance (){}; 
 }; 
  
class Girl : public Person{ 
     . . . 
public : 
   virtual void sing(){}; 
   virtual void speak(){}; 
};

动态分派的过程大概如下图所示。


只有加virtual关键字的虚函数才会存在于虚函数表中,也就是这些虚函数需要动态分派。当子类重写了父类的方法时,动态分派能准确根据接收者类型找到实际需要调用的函数。

在HotSpot VM中有非常多使用动态分派的例子,如Klass继承体系下的类有个oop_oop_iterate_v_m()函数,在发生YGC时,由于老年代也会有引用指向年轻代对象,所以必须通过卡表找到这些可能的对象,在找到这些对象后,这些对象的哪些区域是引用还要进一步借助Klass来完成,如一般的对象会调用InstanceKlass中的oop_oop_iterate_v_m()函数,而java.lang.Class对象会调用InstanceMirrorKlass类中的oop_oop_iterate_v_m()函数。

下面看一下Java的动态分派。举个例子如下:

class Person{ 
  	void sing (){}
	void dance (){}
}

class Girl extends Person{
	void sing(){}
	void speak(){}
}

了解过Java虚函数表的人可能知道,类中许多的方法都会放到虚函数表中,这就是我们前面说的,Java中的方法默认都是动态分派的。
动态分派的过程大概如下图所示。

从Object继承下来5个方法,final、静态方法和构造方法不会进入虚函数表中。final关键字与C++的virtual有着相反的作用。
JVM的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的,invokevirtual表示调用虚方法,也就是会查虚函数表进行调用,而invokeinterface表示调用接口方法,会有个接口函数表,这里暂不介绍。

本人最近准备出一个手写Hotspot VM的课程,超级硬核,从0开始写HotSpot VM,将HotSpot VM所有核心的实现全部走一遍,如感兴趣,速速入群。

群里可讨论虚拟机和Java性能剖析与故障诊断等话题,欢迎加入。

与C++的动态分派在HotSpot VM中的重要应用相似的内容:

C++的动态分派在HotSpot VM中的重要应用

众所周知,多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。C++ 和 Java 作为当前最为流行的两种面向对象编程语言,其内部对于多态的支持对于单继承的实现非常类似。 首先来体现一下C++的动态分派,如下: class Base1{ pub

7.1 C/C++ 实现动态数组

动态数组相比于静态数组具有更大的灵活性,因为其大小可以在运行时根据程序的需要动态地进行分配和调整,而不需要在编译时就确定数组的大小。这使得动态数组非常适合于需要动态添加或删除元素的情况,因为它们可以在不浪费空间的情况下根据需要动态增加或减少存储空间。动态数组的内存空间是从堆(heap)上分配的,动态数组需要程序员手动管理内存,因为它们的内存空间是在程序运行时动态分配的。程序员需要在使用完动态数组后

6.0 Python 使用函数装饰器

装饰器可以使函数执行前和执行后分别执行其他的附加功能,这种在代码运行期间动态增加功能的方式,称之为`"装饰器"(Decorator)`,装饰器的功能非常强大,装饰器一般接受一个函数对象作为参数,以对其进行增强,相当于C++中的构造函数,与析构函数。装饰器本质上是一个python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象.它经常用于有迫切需求的场

Java JVM——12. 垃圾回收理论概述

1.前言 1.1 什么是垃圾? 在提到什么是垃圾之前,我们先看下面一张图: 从上图我们可以很明确的知道,Java 和 C++ 语言的区别,就在于垃圾收集技术和内存动态分配上,C++ 语言没有垃圾收集技术,需要我们手动的收集。 垃圾收集,不是 Java 语言的伴生产物,早在1960年,第一门开始使用内

关于C++拷贝控制

通常来说,对于类内动态分配资源的类需要进行拷贝控制:要在拷贝构造函数、拷贝赋值运算符、析构函数中实现安全高效的操作来管理内存。但是资源管理并不是一个类需要定义自己的拷贝控制成员的唯一原因。C++ Primer 第5版 中给出了一个Message类与Folder类的例子,分别表示电子邮件消息和消息目录

7.4 C/C++ 实现链表栈

相对于顺序栈,链表栈的内存使用更加灵活,因为链表栈的内存空间是通过动态分配获得的,它不需要在创建时确定其大小,而是根据需要逐个分配节点。当需要压入一个新的元素时,只需要分配一个新的节点,并将其插入到链表的头部;当需要弹出栈顶元素时,只需要删除链表头部的节点,并释放其所占用的内存空间即可。由于链表栈的空间利用率更高,因此在实际应用中,链表栈通常比顺序栈更受欢迎。在实现上,链表栈通过使用`malloc

[转帖]【JVM】Java内存区域与OOM

引入 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 Java虚拟机运行时数据区 如图所示 1.程序计数器(线程私有) 作用 记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节

[转帖]【JVM】Java内存区域与OOM

引入 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 Java虚拟机运行时数据区 如图所示 1.程序计数器(线程私有) 作用 记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节

7.2 C/C++ 实现动态链表

动态链表是一种常用的动态数据结构,可以在运行时动态地申请内存空间来存储数据,相比于静态数组和静态链表,更加灵活和高效。在动态链表中,数据元素被组织成一条链表,每个元素包含了指向下一个元素的指针,这样就可以通过指针将所有元素串联起来。使用动态链表存储数据时,不需要预先申请内存空间,而是在需要的时候才向内存申请。当需要添加新的元素时,可以使用`malloc`函数动态地申请内存空间,然后将新的元素插入到

2.1 C++ STL 数组向量容器

Vector容器是C++ STL中的一个动态数组容器,可以在运行时动态地增加或减少其大小,存储相同数据类型的元素,提供了快速的随机访问和在末尾插入或删除元素的功能。该容器可以方便、灵活地代替数组,容器可以实现动态对数组扩容删除等各种复杂操作,其时间复杂度`O(l)常数阶`,其他元素的插入和删除为`O(n)线性阶`,其中n为容器的元素个数,vector具有自动的内存管理机制,对于元素的插入和删除可动