查看 JVM 规范结构,它基本上说堆栈包含帧,并且通过正确分配变量和函数,这些帧包含类内部的任何内容。也许我在这里遗漏了一些东西,但我不明白这与 C++ 所做的有什么不同。我问是因为第一个链接说 Java 的堆栈内容规范避免了编译器不兼容。
在实践中,C++ 编译器遵循相同的基本策略。但是,标准委员会不认为这是语言问题。相反,C++ 编译器遵循这个系统,因为这是大多数 CPU 和操作系统的设计方式。不同的平台在数据是通过堆栈还是通过寄存器(RISC机器)传递给函数,堆栈是向上还是向下,是否有不同的调用约定允许“正常”调用使用堆栈而其他调用使用某些方面存在分歧else(如__fastcall和naked),是否有nested functions、tail call support等。
事实上,符合标准的 C++ 编译器可以编译成类似于 Scheme VM 的东西,其中“堆栈”有很大不同,因为 Scheme 需要实现来支持尾调用和延续。我从未见过这样的事情,但这是合法的。
The "compiler incompatibilities" are most obvious if you try to write a garbage collector:
当前函数及其所有调用者的所有局部变量都在 ["the" 堆栈中,但请考虑 ucontext.h 和 Windows Fibers]。对于每个平台(即 OS + CPU + 编译器),都有一种方法可以找出 ["the" stack] 的位置。 Tamarin 会这样做,然后它会在 GC 期间扫描所有内存以查看本地人指向的位置。 ...
这个魔法存在于一个宏 MMGC_GET_STACK_EXTENTS 中,该宏定义在 MMgc/GC.h 头文件中。 ... [T]每个平台都有一个单独的实现。
在任何给定时刻,一些局部变量可能在 CPU 寄存器中,而不是在堆栈中。为了解决这个问题,宏使用几行汇编代码将所有寄存器的内容转储到堆栈中。这样,MMgc 就可以扫描堆栈,它会看到所有的局部变量。
此外,Java 中的 对象 通常不会在堆栈上分配。相反,对它们的引用是。整数、双精度、布尔值和其他原始类型确实在堆栈上分配。在 C++ 中,任何东西都可以在堆栈上分配,它有自己的优缺点列表。
我不明白的另一件事是运行时常量池。它应该是“类文件中的 constant_pool 表的每个类或每个接口的运行时表示”,但我想我不明白它的作用。
考虑:
String s = "Hello World";
int i = "Hello World".length();
int j = 5;
s、i 和 j 都是变量,并且可以在程序的某个稍后时间点进行更改。但是,“Hello World”是一个不能更改的 String 类型的对象,5 是一个不能更改的 int,并且“Hello World”.length() 可以在编译时确定始终返回 11。这些常量是有效的对象和方法可以在它们上调用(嗯,至少在字符串上),所以它们需要被分配到某个地方。但它们永远无法改变。如果这些常量属于一个类,那么它们将被分配到每个类的常量池中。不属于类的其他常量数据(如 main() 线程的 ID)分配在每个运行时常量池中(“运行时”在本例中表示“JVM 的实例”)。
C++ 标准有一些关于类似技术的语言,但实现取决于二进制格式(ELF、a.out、COFF、PE 等)。该标准期望整数数据类型(bool、int、long 等)或 c 样式字符串的常量实际保存在二进制的常量部分中,而其他常量数据(双精度、浮点数、类)可能被存储作为一个变量以及一个表示“变量”不可修改的标志(也可以将它们与整数和 c 样式的字符串常量一起存储,但许多二进制格式不使此选项成为一种选择)。
一般来说,当一次打开多个程序副本时,可以共享二进制文件的“常量数据部分”(因为程序的每个副本中的常量数据都是相同的)。 On ELF this section is called the .rodata section.