C++中的动态分派在HotSpot VM中的应用小结
作者:鸠摩(马智)
众所周知,多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。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节)。
虚函数表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚函数表即可。同一个类的所有对象都使用同一个虚函数表。为了指定对象的虚函数表,对象内部包含一个虚函数表的指针,来指向自己所使用的虚函数表。为了让每个包含虚函数表的类的对象都拥有一个虚函数表指针,编译器在类中添加了一个指针,用来指向虚函数表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚函数表。
从如上的例子我们应该能够得到如下一些结论:
- C++的动态分派需要明确用virtual关键字指明,而Java中的方法默认就是动态分派,这也体现出两种语言设计理念的不同,C++不想让用户为用不到的功能付出代价,Java更看重开发效率,将一些复杂的底层控制全权托管给虚拟机;
- 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表示调用接口方法,会有个接口函数表,这里暂不介绍。
到此这篇关于C++中的动态分派在HotSpot VM中的应用小结的文章就介绍到这了,更多相关C++动态分派HotSpot VM应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!