Java中的堆栈和堆内存


99

据我了解,在Java中,堆栈内存保存了原语和方法调用,而堆内存则用于存储对象。

假设我有一堂课

class A {
       int a ;
       String b;
       //getters and setters
}
  1. a类中的基元A将存储在哪里?

  2. 为什么堆内存根本存在?为什么我们不能将所有内容都存储在堆栈中?

  3. 当对象被垃圾回收时,与对象相关联的堆栈是否被破坏?



@ S.Lott,只不过这是关于Java而不是C。
PéterTörök

@PéterTörök:同意。虽然代码示例是Java,但是没有标签表明它只是Java。而且一般的原则也应该像C一样适用于Java。此外,有关堆栈溢出的问题有很多答案。
S.Lott

9
@SteveHaigh:在这个网站上,每个人都太在意某个东西是否属于这里...我想知道这个网站真正引起了人们的广泛关注,是关于问题是否属于这里的种种挑剔。
山姆·戈德堡2014年

Answers:


106

堆栈和堆之间的基本区别是值的生命周期。

堆栈值仅存在于创建它们的函数范围内。一旦返回,它们将被丢弃。
但是堆值存在于堆中。它们是在某个时间点创建的,而在另一个时间点被破坏(通过GC或手动,具体取决于语言/运行时)。

现在,Java仅将原语存储在堆栈中。这样可使堆栈变小,并有助于使各个堆栈帧变小,从而允许进行更多的嵌套调用。
对象是在堆上创建的,并且只有引用(即原语)在堆栈上传递。

因此,如果您创建一个对象,它将与所有属于它的变量一起放入堆中,这样它就可以在函数调用返回后继续存在。


2
“而且只有引用(反过来又是基元)”为什么说引用是基元?你能澄清一下吗?
极客

5
@Geek:因为原始数据类型的通用定义适用于:“一种由编程语言提供的数据类型作为基本构建块”。您可能还会注意到,在文章的后面的一些规范示例中列出了引用。
back2dos

4
@Geek:就数据而言,您可以将任何原始数据类型(包括引用)视为数字。偶数char是数字,因此可以互换使用。引用也只是指向内存地址的数字,它们可以是32位或64位长(尽管不能这样使用-除非您弄乱了sun.misc.Unsafe)。
桑·拉斯穆森

3
这个答案的术语是错误的。根据Java语言规范,引用不是原语。答案的要旨是正确的。(虽然你可以使一个论点,即引用“在一定意义上的”原始是靠了靠的JLS定义了Java中的术语,它说,原始的类型有booleanbyteshortcharintlongfloatdouble。)
斯蒂芬ç

4
正如我在本文中发现的那样,Java 可能会将对象存储在堆栈上(甚至存储在用于短期对象的寄存器中)。JVM可以做很多事情。说“现在的Java仅将原语存储在堆栈中”并不完全正确。

49

基本字段存储在哪里?

基本字段存储为在某个地方实例化的对象的一部分。想想这是哪里的最简单方法是堆。 但是,并非总是如此。如Java理论和实践中所述:回顾城市性能传奇

JVM可以使用一种称为转义分析的技术,通过这种技术,他们可以告诉我们某些对象在其整个生命周期内都被限制在单个线程中,并且生命周期受给定堆栈帧的生命周期的限制。这样的对象可以安全地分配在堆栈上而不是堆上。更好的是,对于小型对象,JVM可以完全优化分配,只需将对象的字段提升到寄存器中即可。

因此,除了说“对象已创建并且字段也在那里”之外,我们无法说出是堆还是栈中的东西。请注意,对于小型,寿命短的对象,“对象”可能不存在于内存中,而是直接将其字段放置在寄存器中。

本文的结论是:

JVM出奇的擅长弄清我们以前假定只有开发人员才能知道的事情。通过让JVM根据具体情况在堆栈分配和堆分配之间进行选择,我们可以获得堆栈分配的性能优势,而无需让程序员为在堆栈上分配还是在堆上分配分配而烦恼。

因此,如果您的代码如下所示:

void foo(int arg) {
    Bar qux = new Bar(arg);
    ...
}

...不允许qux离开该范围,qux 可以在堆栈上,而不是进行分配。对于VM来说,这实际上是一个胜利,因为这意味着它永远都不需要进行垃圾回收-当它离开作用域时,它将消失。

有关Wikipedia 逃逸分析的更多信息。对于那些愿意研究论文的人,IBM的Escape Analysis for Java。对于那些来自C#领域的人,您可能会读到Eric Lippert的《堆栈是实现细节》《关于值类型的真相》(它们对于Java类型很有用,因为许多概念和方面相同或相似) 。为什么.Net书籍为什么谈论堆栈与堆内存分配?也涉及到这一点。

关于堆栈和堆的原因

在堆上

那么,为什么要堆栈或堆呢?对于超出范围的事物,堆栈可能会很昂贵。考虑代码:

void foo(String arg) {
    bar(arg);
    ...
}

void bar(String arg) {
    qux(arg);
    ...
}

void qux(String arg) {
    ...
}

参数也是堆栈的一部分。在没有堆的情况下,您将在堆栈上传递完整的值集。对于"foo"小的字符串来说,这是很好的方法……但是,如果有人在该字符串中放入了巨大的XML文件,将会发生什么。每次调用会将整个巨大的字符串复制到堆栈中- 将非常浪费。

相反,最好将寿命超出直接作用域(传递给另一个作用域,卡在其他人维护的结构中等等)的对象放到另一个称为堆的区域中。

在堆栈上

不需要堆栈。假设可以编写一种不使用堆栈(任意深度)的语言。我小时候就学过的一种古老的BASIC可以做到,一个人只能进行8个级别的gosub调用,并且所有变量都是全局变量-没有堆栈。

堆栈的优点是,当您有一个与范围一起存在的变量时,当您离开该范围时,将弹出该堆栈框架。它确实简化了存在和不存在的内容。程序移至另一个过程,即新的堆栈框架。程序返回到该过程,并且您已回到查看当前范围的那一步;程序将离开该过程,并释放堆栈中的所有项目。

对于编写运行时代码的人,使用堆栈和堆确实很容易。他们简单地使用了许多代码的概念和方法,从而使使用该语言编写代码的人可以摆脱对它们的明确思考。

堆栈的性质也意味着它不会变得零散。内存碎片是堆的真正问题。您分配了一些对象,然后垃圾回收了一个中间的对象,然后尝试为下一个要分配的较大对象寻找空间。一团糟。能够将内容放到堆栈上意味着您不必处理这些。

垃圾收集时

当一些东西被垃圾收集时,它就消失了。但这只是垃圾回收,因为已经将其遗忘了-程序中没有更多可以从程序当前状态访问的对象引用。

我将指出,这是垃圾收集的极大简化。有很多垃圾收集器(即使在Java中-您也可以通过使用各种标志(docs)来调整垃圾收集器。这些标志的行为各不相同,每个人如何做事情的细微差别对此答案来说都太深了。您不妨阅读一下Java垃圾回收基础知识,以更好地了解其中的一些原理。

也就是说,如果在堆栈上分配了某些内容,则不会将其作为一部分进行垃圾回收System.gc()-当堆栈帧弹出时,将其释放。如果有东西在堆上,并且是从栈中的东西引用的,那么此时将不会进行垃圾回收。

为什么这么重要?

在很大程度上,它是传统。编写的教科书和编译器类以及各种位文档对堆和堆栈有很大的影响。

但是,当今的虚拟机(JVM和类似的虚拟机)已经竭尽全力以防止对程序员隐藏。除非您用尽了另一个并且需要知道原因(而不仅仅是适当地增加存储空间),否则不要紧。

该对象位于某个地方,并且在适当的时间范围内可以正确,快速地对其进行访问。如果它在堆栈或堆上-并不重要。


7
  1. 在堆中,作为对象的一部分,由堆栈中的指针引用。即。a和b将彼此相邻存储。
  2. 因为如果所有内存都是堆栈内存,它将不再有效。最好在我们开始时有一个小的快速访问区域,并将这些参考项目放在更大的内存区域中。但是,当一个对象只是一个简单的原语时,这是过大的杀伤力,该原语将占用与指向它的指针相同的堆栈空间。
  3. 是。

1
我想在第二点说,如果您将对象存储在堆栈上(想象一个有成千上万个条目的字典对象),那么为了将其传递给函数或从函数返回它,您需要复制每个对象时间。通过使用指向堆中对象的指针或引用,我们仅传递(小)引用。
Scott Whitlock

1
我以为3是“否”,因为如果该对象被垃圾回收,则堆栈中没有指向它的引用。
卢西亚诺

@Luciano-我明白你的意思。我对问题3的理解有所不同。“同时”或“到那个时候”是隐式的。::耸耸肩::
pdr

3
  1. 在堆上,除非通过转义分析证明这不会影响语义,否则Java将堆栈上的类实例分配为优化对象。但是,这是一个实现细节,因此对于所有实际目的,除了微优化之外,答案都是“在堆上”。

  2. 堆栈内存必须按照先进先出的顺序分配和释放。堆内存可以按任何顺序分配和释放。

  3. 垃圾回收对象时,堆栈中不再有指向它的引用。如果有的话,它们会使对象保持活动状态。堆栈基元根本不会被垃圾收集,因为当函数返回时,它们会被自动销毁。


3

堆栈存储器用于存储局部变量和函数调用。

而堆内存用于在Java中存储对象。没关系,对象在代码中创建的位置。

基元将存储a在哪里class A

在这种情况下,原语a与A类对象相关联。因此,它在堆内存中创建。

为什么堆内存根本存在?为什么我们不能将所有内容都存储在堆栈中?

  • 在堆栈上创建的变量将超出范围并自动销毁。
  • 与堆上的变量相比,分配栈要快得多。
  • 堆上的变量必须由垃圾收集器销毁。
  • 与堆栈上的变量相比,分配速度较慢。
  • 如果您确切地知道在编译之前需要分配多少数据并且它不会太大,则可以使用堆栈。(原始局部变量存储在堆栈中)
  • 如果您不确切知道运行时需要多少数据或需要分配大量数据,则可以使用堆。

当对象被垃圾回收时,与对象相关联的堆栈是否被破坏?

垃圾收集器在堆内存的范围内工作,因此它会破坏从根到根没有引用链的对象。


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.