Java“ Foo f = new Foo()”中的对象初始化与使用malloc作为C语言中的指针是否基本相同?


9

我试图了解Java中对象创建背后的实际过程-并且我想使用其他编程语言。

假定Java中的对象初始化与对C中的结构使用malloc相同是错误的吗?

例:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

这就是为什么说对象位于堆而不是堆栈上的原因吗?因为它们本质上只是数据指针?


在堆上为托管语言(如c#/ java)创建对象。在CPP你可以在栈上创建对象一样好
巴斯

为什么Java / C#的创建者决定只在堆上存储对象?
Jules

认为是为了简单起见。将对象存储在堆栈上并传递给它们更深的层次涉及将对象复制到堆栈上,这涉及复制构造函数。我没有谷歌的一个正确的答案,但我敢肯定,你可以找到自己更满意的答案(或别人将详细阐述这个问题侧)
BAS

Java中的@Jules对象仍然可以在运行时“ scalar-replacement分解”(称为)到仅存在于堆栈中的纯字段中;但这是可以JIT做到的,而不是javac
尤金(Eugene)

“堆”只是与分配的对象/内存关联的一组属性的名称。在C / C ++中,您可以从两组不同的属性中进行选择,即“堆栈”和“堆”,在C#和Java中,所有对象分配都具有相同的指定行为,并以“堆”为名,而没有暗示这些属性与C / C ++“堆”的属性相同,但实际上并非如此。这并不意味着实现不能具有用于管理对象的不同策略,这意味着这些策略与应用程序逻辑无关。
Holger

Answers:


5

在C语言中,malloc()在堆中分配一个内存区域并返回一个指向它的指针。那就是你所得到的。内存是未初始化的,您不能保证它全为零或其他任何值。

在Java中,调用new就像一样进行基于堆的分配malloc(),但是您还会获得很多额外的便利(如果愿意,也可以增加开销)。例如,您不必显式指定要分配的字节数。编译器会根据您要分配的对象类型为您解决问题。此外,还调用了对象构造函数(如果您想控制初始化的发生方式,可以将其传递给参数)。当new返回时,你保证有一个已初始化的对象。

但是,是的,在调用结束时,malloc()和的结果new都只是指向某些基于堆的数据块的指针。

问题的第二部分询问堆栈和堆之间的区别。通过学习(或阅读有关)编译器设计课程,可以找到更全面的答案。操作系统课程也将有所帮助。关于堆栈和堆,SO上也有许多问题和答案。

话虽如此,我将给出一个总体概述,我希望它不要太冗长,并且旨在从较高的角度解释这些差异。

从根本上讲,拥有两个内存管理系统(即堆和堆栈)的主要原因是为了提高效率。第二个原因是,在某些类型的问题上,每种方法都比另一种方法更好。

作为一个概念,堆栈对于我来说比较容易理解,因此我从堆栈开始。让我们考虑一下C中的此功能...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

以上似乎很简单。我们定义一个名为的函数,add()并传入左右加数。该函数将它们相加并返回结果。请忽略所有可能发生的情况(例如溢出),这与讨论无关。

add()函数的目的似乎很简单,但是我们可以说出它的生命周期吗?特别是它的内存利用率需要吗?

最重要的是,编译器先验地(即在编译时)知道数据类型有多大以及将使用多少种。的lhsrhs参数是sizeof(int),4个字节的每个。变量result也是sizeof(int)。编译器可以告诉该add()函数使用4 bytes * 3 ints或总共12个字节的内存。

add()调用该函数时,称为堆栈指针的硬件寄存器将具有指向堆栈顶部的地址。为了分配add()函数需要运行的内存,所有函数输入代码需要执行的是发出一条汇编语言指令,以将堆栈指针寄存器的值减12。这样做,它将在堆栈上创建三个存储空间。ints,每一个用于lhsrhs,和result。通过执行一条指令来获取所需的存储空间在速度方面是一个巨大的胜利,因为一条指令往往在一个时钟周期内执行(1 GHz CPU每秒执行十亿分之一秒)。

同样,从编译器的角度来看,它可以创建一个映射到看起来像索引数组的变量的映射:

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

同样,所有这些都非常快。

add()函数退出时必须进行清理。它通过从堆栈指针寄存器中减去12个字节来实现。这类似于对的调用,free()但是它仅使用一条CPU指令,并且只需要花费一勾。非常非常快。


现在考虑基于堆的分配。当我们不知道先验需要多少内存时(即我们只会在运行时了解它),这就会起作用。

考虑以下功能:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

请注意,该addRandom()函数在编译时不知道count参数的值。因此,array像我们将其放入堆栈那样尝试定义是没有意义的,如下所示:

int array[count];

如果count太大,可能会导致我们的堆栈变得太大,并覆盖其他程序段。当此堆栈溢出发生时,您的程序将崩溃(或更糟)。

因此,在直到运行时都不知道需要多少内存的情况下,我们使用malloc()。然后,我们可以只在需要时问我们需要的字节数,malloc()然后检查它是否可以出售那么多字节。如果可以的话,很好,我们将其取回,否则,我们将得到一个NULL指针,该指针告诉我们对malloc()失败的调用。值得注意的是,该程序不会崩溃!当然,作为资源程序员,您可以决定如果资源分配失败,则不允许您的程序运行,但是由程序员启动的终止不同于虚假的崩溃。

所以现在我们必须回头看看效率。堆栈分配器非常快-一条分配指令,一条释放指令,这是由编译器完成的,但是请记住,堆栈是用于已知大小的局部变量之类的,因此它往往很小。

另一方面,堆分配器要慢几个数量级。它必须在表中进行查找,以查看其是否有足够的可用内存来出售用户所需的内存量。它有它出售该内存,以使后更新这些表肯定没有其他人可以使用块(这簿记可能需要分配到备用内存本身除了它计划鬻)。分配器必须采用锁定策略,以确保它以线程安全的方式出售内存。当记忆终于来了free()d,它发生在不同的时间,并且通常没有可预见的顺序,分配器必须找到连续的块并将它们缝合在一起以修复堆碎片。如果这听起来需要完成一条以上的CPU指令,那是对的!这非常复杂,需要一段时间。

但是堆很大。比堆栈大得多。我们可以从它们那里获得很多内存,当我们在编译时不知道需要多少内存时,它们就很棒。因此,我们要权衡一个托管内存系统的速度,该内存系统会礼让我们,而不是在尝试分配太大的内存时崩溃。

我希望这有助于回答您的一些问题。如果您想对上述任何一项进行澄清,请告诉我。


int在64位平台上不是8个字节。现在仍然是4。此外,编译器很可能会将int堆栈中的第三个优化为返回寄存器。实际上,这两个参数也可能在任何64位平台上的寄存器中。
SS安妮

我已经编辑了答案,删除了int在64位平台上有关8字节的语句。您是正确的,int在Java 中仍然保留4个字节。但是,我留下了剩下的答案,因为我相信开始进行编译器优化会使工作陷入困境。是的,您在这些方面也很正确,但是这个问题要求您对堆栈还是堆进行澄清。RVO,通过寄存器传递的参数,代码省略等负担了基本概念的负担,并妨碍了人们理解基本原理。
票面
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.