【问题标题】:About Memory Management in Java and C++关于 Java 和 C++ 中的内存管理
【发布时间】:2009-01-14 00:05:06
【问题描述】:

好吧,我接到了一项任务,主要是弄清楚我将使用的任何语言的内存分配是如何工作的。经过一番研究,我有一些问题和疑问,我想了解一下。例如:

我读到here,Java 明确指定了堆栈内容的组织方式。查看JVM spec structure,它基本上说堆栈包含帧,并且通过正确分配变量和函数,这些帧包含类内部的任何内容。也许我在这里遗漏了一些东西,但我不明白这与 C++ 所做的有什么不同。我问是因为第一个链接说 Java 的堆栈内容规范避免了编译器不兼容。

此外,我还没有找到内存段是如何准确地相互叠加的。例如,我知道内存分为全局变量、调用堆栈、堆和 C++ 代码,但我不知道堆的地址是否高于堆栈的地址,或者是否取决于实现。我还想知道 Java 程序是否具有更多功能,以及如何布局。我想有一个标准,因为 JVM 必须知道在哪里使用它,尽管我想它可能只有指针,其余的留给操作系统。我也想,至少要有一个事实上的标准。

我不明白的另一件事是运行时常量池。它应该是“类文件中常量池表的每个类或每个接口的运行时表示”,但我认为我不明白它的作用。它似乎有一个标签来指示所讨论的结构是什么类型?然后是结构的名称(由程序员给出还是由底层系统分配?)然后它的其余部分似乎随标签描述的任何内容(线程、数组等)而变化。

如果我对运行时常量池的解释是正确的,那么为什么它们和堆栈帧一样需要呢?是不是因为栈帧只处理栈段,而运行时常量池也必须有堆分配内存的指针?

【问题讨论】:

    标签: java c++ memory-management


    【解决方案1】:

    查看 JVM 规范结构,它基本上说堆栈包含帧,并且通过正确分配变量和函数,这些帧包含类内部的任何内容。也许我在这里遗漏了一些东西,但我不明白这与 C++ 所做的有什么不同。我问是因为第一个链接说 Java 的堆栈内容规范避免了编译器不兼容。

    在实践中,C++ 编译器遵循相同的基本策略。但是,标准委员会不认为这是语言问题。相反,C++ 编译器遵循这个系统,因为这是大多数 CPU 和操作系统的设计方式。不同的平台在数据是通过堆栈还是通过寄存器(RISC机器)传递给函数,堆栈是向上还是向下,是否有不同的调用约定允许“正常”调用使用堆栈而其他调用使用某些方面存在分歧else(如__fastcallnaked),是否有nested functionstail call support等。

    事实上,符合标准的 C++ 编译器可以编译成类似于 Scheme VM 的东西,其中“堆栈”有很大不同,因为 Scheme 需要实现来支持尾调用和延续。我从未见过这样的事情,但这是合法的。

    The "compiler incompatibilities" are most obvious if you try to write a garbage collector:

    当前函数及其所有调用者的所有局部变量都在 ["the" 堆栈中,但请考虑 ucontext.hWindows 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.

    【讨论】:

    • 不错的文章。小修正:虽然通常对象不会在堆栈上分配,但由于 Java SE 6 javac 可以进行逃逸分析,因此它甚至可以在堆栈上分配对象。见ibm.com/developerworks/java/library/j-jtp09275.html。但这对开发者来说应该是透明的:-)。
    【解决方案2】:

    给你的任务到底是什么?

    Java 和 C++ 的主要区别在于 Java 是由 VM 收集垃圾,而在 C++ 中,程序直接在机器上执行,内存通过 OS 服务进行管理。

    关于堆栈,框架只是 C++ 编译器所做的更“官方”和标准的形式。当您从一个调用移动到另一个调用时,C++ 编译器只是将堆栈中的东西放在一起。在 Java 中,这个术语是框架,因为编译后的 Java 代码应该在任何平台上运行,所以有非常明确的标准来说明它是如何发生的。在 C++ 中,每个编译器都可以不同地处理堆栈(例如,甚至根据字长的性质)。

    在 Java 中,一切都在管理一切的 VM 中运行,尽管它会将一些东西委托给环境。换句话说,您无法访问 JVM 将您的数据和代码放在哪里,您的代码甚至可能永远不会成为真正的“代码段”。换句话说,这真的无法回答。在 C++ 中,一切都在硬件上运行,因此您将拥有堆栈段、数据段等。请查看有关 C++ 的信息。

    在 C++ 中,类在运行时在内存中没有表示;实际上,您可以将 C++ 编译成 C,然后将结果编译成程序集。在 Java 中,一切都在运行时表示,因此您可以询问对象它属于什么类以及支持什么方法。因此,每个类文件都有一个“常量池”,其中出现了表示方法名称、字段名称等内容的字符串。实际的类定义是指池。所以换句话说,这与堆栈帧几乎没有关系。堆栈帧是存储方法参数、局部变量和返回值的地方。

    【讨论】:

    • 关于 C++ 类在内存中没有表示的最后一部分并不完全正确。 C++ 确实支持 RTTI,这对于某些事情是必要的(例如 dynamic_cast、typeid 和异常处理)。
    • 我同意。但是,这在我关于常量池的回答之内。 AFAIK,C++ 中的 RTTI 不保留源材料的命名。
    • 不完全正确,考虑 typeid(x).name(),即使名称被破坏(这是可逆的)它仍然是源材料。
    猜你喜欢
    • 2012-01-24
    • 1970-01-01
    • 1970-01-01
    • 2012-08-08
    • 2016-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-28
    相关资源
    最近更新 更多