• C++指针和地址偏移在HotSpot VM中的应用


     在前面我们介绍过new运算符,这个操作实际上上包含了如下3个步骤:

    1. 调用operator new的标准库函数。此函数会分配一块内存空间以便函存储相应类型的实例;
    2. 调用相应类的构造函数;
    3. 返回一个指向该对象的指针。

    在第一步中,其实我们可以自己写个operator new函数对标准库函数进行重载,通常会根据类信息分配出需要的内存大小,但是分配内存的逻辑现在由我们自己控制,那我们就可以多分配一些内存,然后在多分配出来的内存上存储一些额外定义的信息。例如:

    class Test {
    private:
        int a = 1;
    public:
        void *operator new(size_t requested_size) throw() {
            return ::malloc(requested_size);
        }
     
        void *operator new(size_t requested_size, size_t length) throw() {
            return ::malloc(requested_size + length);
        }
     
        long access_a_offset() {
            return (size_t) ((intptr_t) &(((Test *) 16)->a) - 16);
        }
     
        u_char *start_b_address() {
            return (u_char *) this + 4;
        }
     
        void set_b_value(long val) {
            *start_b_address() = val;
        }
     
        long get_b_value() {
            return (long) (*start_b_address());
        }
    };

    我们重载了new运算符,第一个重载函数会分配类实例本来需要的内存大小,而第二个重载函数多分配了length个字节的大小。举个具体使用的例子,如下: 

    std::cout << sizeof(Test) << std::endl;
    // 调用第一个operator new函数,分配的内存大小为8,用来存储变量
    Test *t1 = new Test();
    // 调用第二个operator new函数,分配的内存大小为16,用来存储变量外,还有空闲的8字节
    Test *t2 = new (8) Test();
     
    std::cout << t2->access_a_offset() << std::endl;
    t2->set_b_value(10);
    std::cout << t2->get_b_value() << std::endl;

    最终打印的值为0 10

    如上的例子在内存末尾多开辟了8字节用来存储long类型的数据,因为这个数据没有对应的属性用来直接存取,所以只能通过偏移来操作。

    注意:我们通过this获取到当前实例内存的首地址时,必须要强制转换为u_char*类型,这样加4后才会移动4个字节,因为u_char占用一个字节,此时的指针指向u_char数据类型。其实还可以这样获取:

    (u_char *) (this + 1)

    this指向的是Test类型,加1后指针本身占用的内存大小的末尾,将其强制转换为指向u_char类型的指针即可。

    在HotSpot VM中也有这样的操作,例如Method,根据需要有两个可选择性的字段,如下:

    根据Method决定是否要多开辟内存来存储native_function和signature_handler,源代码如下:

    // 源代码位置:openjdk/hotspot/src/share/vm/oops/method.cpp
     
    int size = Method::size(access_flags.is_native());
     
    return new (loader_data, size, false, MetaspaceObj::MethodType, THREAD) Method(cm, access_flags, size);

    当为本地方法时,会为Method多开辟2 个指针大小的存储空间,然后使用new关键字创建对象。这里也重载了new运算符从指定的元数据区分配内存。  

    下面来看对这两个伪字段(不能通过类中的实例字段进行存取操作,但是又确实存在)的存取操作。

    typedef   u_char*       address;
     
    // 源代码位置:openjdk/hotspot/src/share/vm/oops/method.hpp
    address* native_function_addr() const {
          assert(is_native(), "must be native");
          return (address*) (this+1);
    }
    address* signature_handler_addr() const {
          return native_function_addr() + 1;
    }

    获取两个伪字段的地址。

    注意这里的this+1,因为this的类型是Method实例,所以加1并不是加一个字节而是增加一个Method对应的字节数,即获取Method对应内存区域的下一个字节的地址;第二个native_function_addr() + 1,因为native_function_addr()返回的就是一个指针类型的数据,所以这里的加1是增加指针对应的字节数,64位下是8字节。

    返回的类型为u_char**,也就是返回一个指向指针的指针。当我们要存储本地函数地址时,可如下操作:

    // 读取操作
    address current = *native_function;
     
    // 存储操作
    *native_function = function

    其中的function的类型为address。

    下面继续看偏移量的操作,HotSpot VM中经常做的操作就是计算某个变量的偏移量。例如定义的用来表示Java类的C++类Klass中有如下2个函数: 

    static ByteSize access_flags_offset(){
      return in_ByteSize(offset_of(Klass, _access_flags));
    }

    其中的_access_flags属性就是定义在Klass中的,通过调用access_flags_offset()函数来计算这个属性在类中的偏移量。offset_of是一个宏,如下:

    #define offset_of(klass,field) (size_t)((intx)&(((klass*)16)->field) - 16)

    经过宏替换和格式调整后的方法如下:

    static ByteSize access_flags_offset(){
      return in_ByteSize((size_t)(
         (intx)&(  ((Klass*)16)->_access_flags) - 16
      ));
    }

    通过 (intx)&(((Klass*)16)->_access_flags) - 16 方式来计算出具体的偏移量。解释一下这种写法。

    假如定义个变量Klass a; 我们都知道&a表示变量a的首地址,&(a._access_flags)表示变量_access_flags的地址,那么&(a._access_flags)减去&a就得到_access_flags的偏移量。

    ((Klass*)16)的地址为16,所以偏移量最终等于&( ((Klass*)16)->_access_flags)减去16。

    当HotSpot VM要用一个成员变量的时候,它会根据对象的首地址加上成员的偏移量得到成员变量的地址。当对象的首地址为0时,得到的成员变量地址就是它的偏移量。

     

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

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

  • 相关阅读:
    【C++】STL容器——list类的使用指南(含代码演示)(13)
    MySQL支持哪些存储引擎
    web请求cookie中expires总结
    backbone核心详解系列——RepVGG
    Python中的函数、闭包、装饰器、lambda表达式、生成器和递归
    SQL基础培训-SQL开发思路畅谈与开发规范
    JVM面试高频问题
    【MySQL】【牛客-SQL进阶挑战】02 表与索引操作
    中国日用化工市场竞争格局展望及未来战略分析报告2022年版
    【pen200-lab】10.11.1.39
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/17722818.html