【问题标题】:Array memory allocation - paging数组内存分配——分页
【发布时间】:2026-02-11 07:05:03
【问题描述】:

不确定对于 Java、C# 和 C++ 的答案是否相同,所以我对它们进行了分类。所有语言的答案都会很好。

我整天都在想,如果我分配数组,所有单元格都会在一个连续的空间中。因此,如果系统中的一块内存不足,则会引发内存不足的异常。

没关系,我说的是什么?或者有没有可能,分配的数组会被分页?

【问题讨论】:

    标签: c# java c++ memory-management


    【解决方案1】:

    C++ 数组是连续的,这意味着内存具有连续的地址,即它在虚拟地址空间中是连续的。它在物理地址空间中不需要是连续的,因为现代处理器(或其内存子系统)具有将虚拟页面与物理页面相关联的大映射。在用户模式下运行的进程永远不会看到其阵列的物理地址。

    我认为实际上大多数或所有 Java 实现都是相同的。但是程序员永远不会看到数组元素的实际地址,只是对数组的引用和索引它的方法。因此,理论上,Java 实现可以破坏数组并将该事实隐藏在 [] 运算符中,尽管 JNI 代码仍然可以以 C++ 样式查看数组,此时需要一个连续的块。这是假设 JVM 规范中没有关于数组布局的任何内容,而 jarnbjo 告诉我没有。

    我不了解 C#,但我预计情况与 Java 非常相似 - 您可以想象一个实现可能会使用 [] 运算符来隐藏数组在虚拟地址空间中不连续的事实。一旦有人获得指向它的指针,这种伪装就会失败。 [编辑:多项式表示 C# 中的数组在有人固定它们之前可能是不连续的,这是有道理的,因为您知道必须先固定对象,然后才能将它们传递给使用地址的低级代码。]

    请注意,如果您分配某个大型对象类型的数组,那么在 C++ 中,该数组实际上是许多端到端放置的大型结构,因此所需的连续分配大小取决于对象的大小。在 Java 中,对象数组“实际上”是引用数组。所以这是一个比 C++ 数组更小的连续块。对于原生类型,它们是相同的。

    【讨论】:

    • 在语言或 VM 规范中没有要求 Java 数组在内存中必须是连续的。这甚至适用于 JNI,因为您无法从本机代码直接访问 Java 数组,但必须调用 GetArrayElements 之一来获取 Java 数组的本机“视图”。然而,AFAIK 所有的 JVM 实现都使用连续内存进行数组存储,如果没有足够的连续空间可用于分配请求的数组,则较新的虚拟机使用堆碎片整理技术。
    • @jarnbjo:很公平,所以只有在调用GetWhateverArrayElements 时,实现才需要为数组找到一个连续的地址范围。答案已更新,谢谢。
    【解决方案2】:

    在 C# 中,您不能保证内存块是连续的。 CLR 试图在一个连续的块中分配内存,但它可能会在几个块中分配它。关于 CLR 应如何管理 C# 内存的定义行为很少,因为它被设计为由托管构造抽象出来。

    在 C# 中真正重要的唯一情况是,如果您通过 P/Invoke 将数组作为指针传递给一些非托管代码,在这种情况下,您应该使用 GC.Pin 锁定对象在内存中的位置。也许其他人将能够解释 CLR 和 GC 在这种情况下如何处理对连续内存的需求。

    【讨论】:

    • 澄清一下,分片对象存在明显的性能问题,但 CLR 和 GC 将始终尝试以最佳方式分配和收集。
    • “也许其他人能够解释 CLR 和 GC 在这种情况下如何处理对连续内存的需求”——它“处理”它是因为您声称它保证单个分配的连续内存是错误的。如果没有将数组的内存分配为单个连续块(例如Buffer.BlockCopy()),那么 .NET 中有太多内容会简单地中断。此外,如果 .NET 可以将大型数组分配为一些较小分配的集合,则不需要大型对象堆。
    【解决方案3】:

    没关系,我说的是什么?

    没错,在 Java 和 C# 中,但 C++ 只有在达到进程或系统限制时才会出错。不同之处在于,在 Java 和 C# 中,应用程序对其自身施加了限制。在 C++ 中,限制是由操作系统强加的。

    或者有没有可能,分配的数组会被分页?

    这也是可能的。但是在 Java 中,堆分页对性能非常不利。当 GC 运行时,所有检查的对象都必须在内存中。在 C++ 中它不是很好,但影响较小。

    如果您想要可以在 Java 中分页的大型结构,您可以使用 ByteBuffer.allocateDirect() 或内存映射文件。这通过使用堆外内存来工作(基本上是 C++ 使用的)

    【讨论】:

      【解决方案4】:

      在 C(++) 程序中,通常(也就是说,除非我们谈论的是解释代码而不是直接编译 + 执行它)数组在虚拟地址空间中是连续的(当然,如果存在这样的有问题的平台上的东西)。

      在那里,如果一个大数组不能连续分配,即使有足够的空闲内存,你也会得到 std::bad_alloc 异常(在 C++ 中)或 NULL(来自 C/中的 malloc()-like 函数C++ 或 C++ 中的非抛出运算符 new)。

      虚拟内存(和磁盘分页)通常不能解决虚拟地址空间碎片问题,或者至少不能直接解决,它的用途不同。它通常用于让程序认为有足够的内存,而实际上没有。可用磁盘空间有效地扩展了 RAM,但以降低性能为代价,因为当存在内存压力时,操作系统必须在 RAM 和磁盘之间交换数据。

      操作系统可以将您的阵列(部分或全部)卸载到磁盘上。但这对您的程序来说是透明的,因为每当它需要访问数组中的某些内容时,操作系统都会将其加载回来(再次,部分或全部,因为操作系统认为有必要)。

      在没有虚拟内存的系统上,没有虚拟到物理地址的转换,您的程序将直接使用物理内存,因此,它必须处理物理内存碎片并与其他程序竞争空闲内存和地址空间,通常更容易发生分配失败(具有虚拟内存的系统通常在单独的虚拟地址空间中运行程序,应用 A 的虚拟地址空间中的碎片不会影响应用 B 的碎片)。

      【讨论】:

        【解决方案5】:

        当然是 Java 和 C#。我们可以通过在内存页面大小为 4096 字节的 Windows 机器上运行 byte[] array = new byte[4097]; 来展示这一点。因此它必须在一页以上。

        当然分页会影响性能,但这可能是使用 .NET 或 Java 等框架的 GC 可能具有优势的情况之一,因为 GC 是由知道分页发生的人编写的。结构仍然有一些优势,使得它更有可能在同一页面上具有相关元素(偏爱数组支持的集合而不是指针追踪的集合)。这在 CPU 缓存方面也有优势。 (大数组仍然是导致 GC 必须解决的堆碎片的最佳方法之一,但由于 GC 非常擅长这样做,它仍然会胜过许多其他处理相同问题的方法)。

        几乎可以肯定的是,使用 C++,因为我们通常在操作系统的内存管理级别进行编码 - 数组位于连续的虚拟空间(无论是在堆上还是在堆栈上),而不是连续的物理空间。在 C 或 C++ 中可以编写低于此级别的代码,但这通常只能由实际编写内存管理代码本身的人来完成。

        【讨论】:

          【解决方案6】:

          如果 java Array 被实现为 Object....
          并且对象仅在堆中获取 m/m...
          所以我不完全确定,但 ..heap 仅在 RAM 中制作....

          你可以检查..IBM M/m

          【讨论】: