• 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节)。

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

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

    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性能剖析与故障诊断等话题,欢迎加入。

  • 相关阅读:
    线性代数(四)| 解方程 齐次性 非齐次性 扩充问题
    Java设计模式-行为型模式
    谷粒商城实战(033 业务-秒杀功能4-高并发问题解决方案sentinel 2)
    768. 最多能完成排序的块 II 分治
    操作系统——网络通信——多路复用——select 、poll 、epoll的函数的相关使用
    导入(excel)+导出(excel)+国际化(i18n)+
    Day18_VueComponent
    特种期货开户交易权限开通认定标准
    js分组匹配、遍历结果
    开发 pgadmin4 遇到后后端无法切换目标数据库的问题
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/17722230.html