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

发布时间 2023-09-25 08:02:25作者: 鸠摩(马智)

众所周知,多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。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节)。

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

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

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

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

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

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

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

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

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

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