【问题标题】:Java int memory usageJava int 内存使用情况
【发布时间】:2018-01-16 04:47:48
【问题描述】:

当我在思考各种类型的内存使用情况时,我开始对 Java 在传递给方法时如何将内存用于整数感到有些困惑。

说,我有以下代码:

public static void main (String[] args){
     int i = 4;
     addUp(i);
}

public static int addUp(int i){
     if(i == 0) return 0;
     else return addUp(i - 1);         
}

在以下示例中,我想知道我的以下逻辑是否正确:

  • 我最初为整数 i = 4 创建了一个内存。然后我将它传递给一个方法。但是,由于Java中没有指向原语,所以在addUp(i == 4)中,我创建了另一个整数i = 4。然后,还有另一个addUp(i == 3),addUp(i == 2), addUp(i == 1), addUp(i == 0) 其中每次由于没有指向值,所以在内存中分配一个新的 i 值。
  • 那么对于单个“int i”值,我使用了 6 个整数值内存。

但是,如果我总是通过数组传递它:

public static void main (String[] args){
     int[] i = {4};
     // int tempI = i[0];
     addUp(i);
}

public static int addUp(int[] i){
     if(i[0] == 0) return 0;
     else return addUp(i[0] = i[0] - 1);         
}

- 因为我创建了一个大小为 1 的整数数组,然后将其传递给 addUp,它将再次传递给 addUp(i[0] == 3), addUp(i[0] == 2), addUp(i [0] == 1), addUp(i[0] == 0),我只需要使用 1 个整数数组内存空间,因此更具成本效益。此外,如果我要预先创建一个 int 值来存储 i[0] 的初始值,我仍然有我的“原始”值。

那么这就引出了一个问题,为什么人们在 Java 方法中传递像 int 这样的原语?仅传递这些原语的数组值不是更节省内存吗?还是第一个示例仍然只是 O(1) 内存?

在这个问题之上,我只是想知道使用 int[] 和 int 的内存差异,特别是对于 1 的大小。提前谢谢你。我只是想知道使用 Java 可以提高内存效率,这让我想到了。

感谢所有答案!我现在很快想知道我是否要“分析”每个代码的大内存,它们都被认为是 O(1) 还是假设是错误的?

【问题讨论】:

  • “为什么人们在 Java 方法中传递像 int 这样的原语?”因为 Java 是一种将可读、可维护的代码置于高效乱码之上的语言。
  • 顺便说一句,当然,在分析递归函数时,您应该计算渐近内存使用中隐式堆栈的大小。您是否承担尾随电话是一个有趣的考虑因素..
  • @Nathan 仅适用于 Integer 包装类,而不是原始 int
  • @Nathan 从值 -2^31 到 +2^31-1 的所有整数的行为方式完全相同。不超过 127 的值没有什么特别之处。您一定在考虑 Integer 而不是 int
  • 两种方法都使用 O(n) 堆栈内存,因为两种方法都使用参数(对于 Big-O 表示法,参数是需要 4 个字节的 int 还是对4 字节或 8 字节的 int 数组)

标签: java arrays memory jvm int


【解决方案1】:

这里缺少什么:示例中的 int 值位于 stack 上,而不是堆上。

与堆上的对象相比,处理堆栈上存在的固定大小的原始值的开销要少得多!

换句话说:使用“指针”意味着您必须在堆上创建一个新对象。 所有对象都在堆上;数组没有 no 堆栈!在您停止使用对象后,它们会立即成为垃圾收集的对象。另一方面,堆栈在调用方法时来来去去!

除此之外:请记住,编程语言提供给我们的抽象旨在帮助我们编写易于阅读、理解和维护的代码。您的方法基本上是进行某种微调,从而导致更复杂的代码。这不是 Java 解决此类问题的方式。

含义:对于 Java,真正的“性能魔法”发生在运行时,即即时编译器启动时!您会看到,当“经常”调用方法时,JIT 可以内联调用小方法。然后将数据“紧密”在一起变得更加更加重要。如:当数据存在于堆中时,您可能必须访问 memory 才能获取值。而存在于堆栈中的项目 - 可能仍然“关闭”(如:在处理器缓存中)。因此,您优化内存使用的小想法实际上可能会使程序执行速度减慢几个数量级。因为即使在今天,访问处理器缓存和读取主存之间也存在数量级。

长话短说:避免针对性能或内存使用进行此类“微调”:JVM 针对“正常、典型”用例进行了优化。因此,您尝试引入巧妙的解决方法很容易导致“不太好”的结果。

所以 - 当您担心性能时:做其他人都在做的事情。如果 一个 真的关心 - 然后了解 JVM 如何工作。事实证明,即使我的知识也有些过时了——因为 cmets 暗示 JIT 可以内联堆栈上的对象。从这个意义上说:专注于编写简洁、优雅的代码,以直接的方式解决问题!

最后:这可能会在某些时候发生变化。有想法将 true 值值对象引入 java。它基本上住在堆栈上,而不是堆上。但不要指望这会在 Java10 之前发生。或者 11. 或者 ...(我认为 this 在这里很重要)。

【讨论】:

  • 谢谢。顺便说一句,我知道这听起来很愚蠢而且很初级,但是如果有人要两者的大内存空间,那么在一天结束时都会被认为是 O(1) 吗?
  • 没有转储问题。只有愚蠢的人;-) ...老实说;我对这个问题一无所知 - 仅仅是因为它是太久以前了,因为我必须在区分堆栈和堆的同时计算 BigO 的内存。换句话说:我不记得确切的规则......因此我恭敬地将这方面留给其他人回答。
  • 它有点搅浑水,但是如果现代 JIT 能够证明对象的生命周期永远不会超过堆栈帧。因此,即使是问题中的示例代码,如果它被识别为热点,也可能会被 JIT 编译为机器代码,无论如何都会将值保留在堆栈中。
  • @CortAmmon:“相当聪明”的编译器在 Java 中似乎更难实现。 AFAIK,Sun 的 JVM(和 OpenJDK 的)仍然没有 TCO(显然 IBM 的 JVM 有 softwareengineering.stackexchange.com/questions/157684/…)。 Java 安全模型涉及检查给定调用者是否有权调用给定被调用者。此外,更改堆栈跟踪是 TCO 的潜在问题。
  • @Daniel Pryden:不仅仅是在堆栈上分配一个对象。一个纯粹的本地对象可能会被分解成它的变量,然后像其他本地变量一样对待这些变量,即冗余字段可能会被折叠或未使用的字段被删除,直到一个实际过时的本地对象被完全删除。
【解决方案2】:

几件事:

首先要分头,但是当您在 java 中传递一个 int 时,您将 4 个字节分配到堆栈上,而当您传递一个数组(因为它是一个引用)时,您实际上分配了 8 个字节(假设一个 x64架构)到堆栈上,加上将 int 存储到堆中的额外 4 个字节。

更重要的是,数组中的数据被分配到堆中,而对数组本身的引用被分配到堆栈中,当传递整数时不需要堆分配,原语只分配到堆栈中.随着时间的推移,减少堆分配将意味着 垃圾收集器 需要清理的东西更少。而堆栈帧的清理是微不足道的,不需要额外的处理。

但是,这一切都没有实际意义(恕我直言),因为在实践中,当您拥有复杂的变量和对象集合时,您最终可能会将它们组合成一个类。通常,您应该编写以提高可读性和可维护性,而不是试图从 JVM 中挤出最后一滴性能。 JVM 非常快,并且总是有 Moore's Law 作为后盾。

很难分析每个 Big-O,因为为了获得真实情况,您必须考虑垃圾收集器的行为,并且该行为高度依赖于 JVM 本身和任何运行时(JIT) JVM 对您的代码所做的优化。

请记住Donald Knuth的名言“过早优化是万恶之源”

编写避免微调的代码,提高可读性和可维护性的代码从长远来看会更好。

【讨论】:

  • 在你的头发分割上的几个头发分割点: 1. 使用压缩的 OOP(现在是默认的 AFAIK),指向数组的指针可能只占用 4 个字节,即使在 64-位系统; 2. 年轻代分配(没有终结器的对象)被大量收集,实际上可以与堆栈分配一样便宜。 GC 的开销实际上只有在需要复制对象时才会发挥作用(尤其是当它被复制足够多的时间以移出清道夫空间并进入终身代时)。
  • 同意,但所有这些都是为了强调我们不应该担心它,应该让 JVM/JIT 解决它(因为所有这些点都是有效的,但是严重依赖 JVM 实现)而应该只专注于编写清晰和可维护的代码,让 JVM 做它最擅长的事情。
【解决方案3】:

如果您的假设是传递给函数的参数必然会消耗内存(顺便说一句,这是错误的),那么在您传递数组的第二个示例中,会生成对数组的引用的副本。该引用实际上可能比 int 大,它不可能更小。

【讨论】:

  • 传递给函数的参数必然会消耗内存。这就是处理器的工作方式。我错过了什么吗?
  • @Jack 参数可以在寄存器中传递,这不是 x86 代码的规范,但它适用于 x64 和 ARM。当然只能达到一些限制。
  • @Jack - 此外,任何重要的 Java 代码都将被编译,可能会编译多次,最终结果将被高度内联,并且可能存在“零”内存用于许多参数,因为它们是在寄存器中传递的,或者仅仅是因为函数调用本身已经完全消失并且根本没有传递。这样,笼统地谈论参数的内存使用并没有真正的意义。您通常可以谈论堆上对象的内存使用情况(但即使这样也需要进行各种优化),但堆栈就没有那么多了。
【解决方案4】:

这些方法是采用 O(1) 还是 O(N) 取决于编译器。 (这里 N 是 ii[0] 的值,取决于。)如果编译器使用尾递归优化,那么参数、局部变量和返回地址的堆栈空间可以被重用,然后实现将是 O (1) 空间。如果没有尾递归优化,空间复杂度与时间复杂度相同,O(N)。

基本上尾递归优化量(在这种情况下)编译器将您的代码重写为

public static int addUp(int i){
     while(i != 0) i = i-1 ;
     return 0;        
}

public static int addUp(int[] i){
     while(i[0] != 0) i[0] = i[0] - 1 ;
     return 0 ;
}

一个好的优化器可能会进一步优化循环。

据我所知,目前还没有Java编译器实现尾递归优化,但在很多情况下没有技术原因不能做到。

【讨论】:

  • 严格来说,我应该使用 big-Theta 而不是 big-Oh,因为我们正在讨论确切的复杂性,而不是复杂性的上限。
【解决方案5】:

实际上,当您将 数组 作为参数传递给方法时 - 对该数组的引用会在后台传递。数组本身存储在堆上。并且引用的大小可以是 48 字节(取决于 CPU 架构、JVM 实现等;更何况,JLS 并没有说明 a 有多大)参考在内存中)。

另一方面,原始 int 值始终只消耗 4 个字节并驻留在堆栈中。

【讨论】:

    【解决方案6】:

    当你传递一个数组时,数组的内容可能会被接收数组的方法修改。当您传递 int 原语时,这些原语可能不会被接收它们的方法修改。这就是为什么有时您可以使用原语,有时使用数组。

    通常,在 Java 编程中,您倾向于支持可读性,并让 JIT 编译器完成这种内存优化。

    【讨论】:

    • 但是如果我要通过以下方式保存原始 int[0] 值的值:int tempI = i[0];,那么最好只使用数组还是内存使用情况类似一天结束?
    • 这是一个很难回答的问题。您的示例代码并没有真正任何事情,因此我们不可能理解您正在操作的上下文,以至于我们可以推荐一种方法而不是另一种方法。
    【解决方案7】:

    int 数组引用实际上在堆栈帧中占用了比 int 原语更多的空间(8 字节 vs 4)。您实际上正在使用更多空间。

    但我认为人们喜欢第一种方式的主要原因是因为它更清晰易读。

    当涉及到更多整数时,人们实际上做的事情更接近于秒。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-01-30
      • 1970-01-01
      • 1970-01-01
      • 2010-09-28
      • 2011-02-14
      • 2019-04-18
      • 2011-04-08
      • 1970-01-01
      相关资源
      最近更新 更多