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

c++,指针,地址,偏移,hotspot,vm,应用 · 浏览次数 : 89

小编点评

**new运算符**是一种在内存中分配内存空间并创建对象的特殊运算符。该运算符允许用户通过指定的大小分配内存,并根据分配的大小创建不同的类型对象。 **重载new运算符**允许用户为类定义自己的内存分配函数。当用户使用该函数分配内存时,该函数会根据类信息分配所需的内存大小。 **内存分配操作** * **`operator new(size_t requested_size)`** 函数会分配 `requested_size` 字节的大小。 * **`operator new(size_t requested_size, size_t length)`** 函数会分配 `requested_size` 字节的大小并分配 `length` 字节的额外空间。 **偏移量操作** 通过 **`offset_of()`** 宏可以计算变量的偏移量。该宏使用偏移量和类信息来计算变量的实际地址。 **示例** ```cpp class Test { private: int a = 1; public: // 重载new运算符 void *operator new(size_t requested_size) throw() { return ::malloc(requested_size); } // 重载new运算符 void *operator new(size_t requested_size, size_t length) throw() { return ::malloc(requested_size + length); } // 其他成员函数... }; ``` **使用** ```cpp Test *t1 = new Test(); // 分配 8 字节内存 Test *t2 = new (8) Test(); // 分配 8 字节内存,并分配 16 字节的额外空间 std::cout << sizeof(Test) << std::endl; // 打印输出:8 // 设置 b 值 t2->set_b_value(10); // 获取 b 值 std::cout << t2->get_b_value() << std::endl; // 打印输出:10 ```

正文

 在前面我们介绍过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++指针和地址偏移在HotSpot VM中的应用相似的内容:

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

在前面我们介绍过new运算符,这个操作实际上上包含了如下3个步骤: 调用operator new的标准库函数。此函数会分配一块内存空间以便函存储相应类型的实例; 调用相应类的构造函数; 返回一个指向该对象的指针。 在第一步中,其实我们可以自己写个operator new函数对标准库函数进行重载,通常

C语言指针易混淆知识点总结

指针 定义 指针是一个变量,存储另一个变量的内存地址,它允许直接访问和操作内存中的数据,使得程序能够以更灵活和高效的方式处理数据和内存。 获取变量地址:使用取地址符 &。 访问地址上的数据:使用解引用符 *。 例子1 指针是存储另一个变量地址的变量。通过使用取地址符 & 和解引用符 *,我们可以灵活

C#中的浅拷贝与深拷贝

## 前言 众所周知,C#中有两种类型变量:那就是**值类型**和**引用类型**。对于值类型而言,copy就相当于是全盘复制了,真正的实现了复制,属于**深拷贝**;而对于引用类型而言,一般的copy只是**浅拷贝**,只是copy到了引用对象的地址,相当于值传递了一个引用指针,==新的对象通过地

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

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

10.1 C++ STL 模板适配与迭代器

STL(Standard Template Library)标准模板库提供了模板适配器和迭代器等重要概念,为开发者提供了高效、灵活和方便的编程工具。模板适配器是指一组模板类或函数,它们提供一种适配机制,使得现有的模板能够适应新的需求。而迭代器则是STL中的令一种重要的概念,它是一个抽象化的数据访问机制,通过迭代器可以遍历STL容器中的元素。适配器与迭代器两者的紧密配合,使得开发者能够高效地处理容器

7.1 C++ STL 非变易查找算法

C++ STL 中的非变易算法(Non-modifying Algorithms)是指那些不会修改容器内容的算法,是C++提供的一组模板函数,该系列函数不会修改原序列中的数据,而是对数据进行处理、查找、计算等操作,并通过迭代器实现了对序列元素的遍历与访问。由于迭代器与算法是解耦的,因此非变易算法可以广泛地应用于各种容器上,提供了极高的通用性和灵活性。

8.1 C++ STL 变易拷贝算法

C++ STL中的变易算法(Modifying Algorithms)是指那些能够修改容器内容的算法,主要用于修改容器中的数据,例如插入、删除、替换等操作。这些算法同样定义在头文件 algorithm中,它们允许在容器之间进行元素的复制、拷贝、移动等操作,从而可以方便地对容器进行修改和重组。

[转帖]awk 文本处理

https://juejin.cn/post/6844903860629143559 awk 文本处理 awk 是一种样式扫描和处理语言,使用 Linux 的 awk 命令可以高效快捷地进行文本处理。awk 扫描文本的每一行并执行指定的命令。 awk 诞生于 1977 年,借鉴了 C 语言等编程语言

分享一个关于Avl树的迭代器算法

1 研究过程 前段时间在研究avl树的迭代实现,在节点不使用parent指针的情况下,如何使用堆栈来实现双向地迭代。我参考了网络上的大部分迭代器实现,要么是使用了parent指针(就像c++的map容器中的迭代算法),要么就是前中后序遍历,没找到一种真正意义上可以双向迭代的算法,于是乎在我的不屑努力

C++ STL 容器简单讲解

STL 简单讲解 网上有很多很好的资料可以参考 而直接看标准是最准确清晰的 vector stack queue / priority_queue deque array map / multimap set / multiset unordered_map unordered_set 关于指针和迭