Java int内存使用情况

Acc*_*ate 33 java memory arrays int jvm

当我在思考各种类型的内存使用情况时,我开始对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);         
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我最初为整数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);         
}
Run Code Online (Sandbox Code Playgroud)

- 因为我创建了一个大小为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)还是假设错了?

Gho*_*ica 56

你在这里缺少的东西:你的例子中的int值在堆栈上,而不是在堆上.

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

换句话说:使用"指针"意味着您必须在堆上创建一个新对象.所有对象都存在于堆中; 有没有堆栈阵列!在您停止使用对象后,对象立即变为垃圾收集.另一方面,当您调用方法时,堆栈会来来去去!

除此之外:请记住,编程语言为我们提供的抽象是为了帮助我们编写易于阅读,理解和维护的代码而创建的.您的方法基本上是进行某种微调,导致更复杂的代码.这不是Java如何解决这些问题.

含义:使用Java,真正的"性能魔术"在运行时发生,当即时编译器启动时!你看,当"足够频繁"调用方法时,JIT可以内联对小方法的调用.然后,将数据"关闭"在一起变得更加重要.如下所示:当数据存在于堆上时,您可能必须访问内存才能获取值.而生活在堆栈上的项目可能仍然是"关闭"(如:在处理器缓存中).因此,优化内存使用的小想法实际上可能会使程序执行速度降低几个数量级.因为即使在今天,访问处理器缓存和读取主存储器之间也存在数量级.

简而言之:避免对性能或内存使用进行这种"微调":JVM针对"正常,典型"的用例进行了优化.因此,尝试引入巧妙的解决方法很容易导致"不太好"的结果.

所以 - 当你担心表现时:做其他人正在做的事情.如果 一个 真正关心-再学习JVM是如何工作的.事实证明,即使我的知识稍微过时 - 因为评论意味着JIT可以内联堆栈中的对象.从这个意义上说:专注于编写干净,优雅的代码,以直接的方式解决问题!

最后:这在某些方面可能会发生变化.有想法将真正的价值对象引入java.它基本上存在于堆栈中,而不是堆.但是不要指望在Java10之前发生这种情况.或者11.或......(我认为与此相关).

  • 它使水变得混乱,但是现代JIT实际上可以进行转义分析并在堆栈上(而不是在堆上)分配对象,如果它可以证明对象的生命周期永远不会超出堆栈帧.因此,即使问题中的示例代码,如果它被识别为热点,也可能被JIT编译成机器代码,无论如何都保持堆栈上的值. (5认同)
  • @CortAmmon:"相当聪明"的编译器在Java中似乎更难做到.AFAIK,Sun的JVM(和OpenJDK)仍然没有做TCO(显然IBM的JVM确实做了https://softwareengineering.stackexchange.com/questions/157684/what-limitations-does-the-jvm-impose-on-tail-call -optimization#comment555950_157685).Java安全模型涉及检查给定调用者是否具有调用给定被调用者的权限.此外,改变堆栈跟踪是TCO的潜在问题. (2认同)

pfr*_*nza 18

几件事:

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

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

然而,这一切都没有实际意义(因为在实践中,当你拥有复杂的变量和对象集合时,你可能最终会将它们组合成一个类.通常,您应该编写以提高可读性和可维护性,而不是试图从JVM中挤出最后一滴性能.JVM非常快,并且摩尔定律总是作为支持者.

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

请记住唐纳德克努特的明智话语"过早优化是万恶之源"

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


har*_*old 10

如果你的假设是传递给函数的参数必然会占用内存(顺便说一下这是假的),那么在第二个传递数组的例子中,会产生对数组的引用的副本.该引用实际上可能大于int,它不可能更小.


The*_*ell 7

这些方法是否采用O(1)或O(N)取决于编译器.(这里N是ior 的值i[0],取决于.)如果编译器使用尾递归优化,则可以重用参数,局部变量和返回地址的堆栈空间,然后实现空间为O(1).缺少尾递归优化,空间复杂度与时间复杂度O(N)相同.

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

public static int addUp(int i){
     while(i != 0) i = i-1 ;
     return 0;        
}
Run Code Online (Sandbox Code Playgroud)

要么

public static int addUp(int[] i){
     while(i[0] != 0) i[0] = i[0] - 1 ;
     return 0 ;
}
Run Code Online (Sandbox Code Playgroud)

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

据我所知,目前没有Java编译器实现尾递归优化,但没有技术上的理由在许多情况下无法完成.

  • 严格来说,我应该使用big-Theta而不是big-Oh,因为我们正在讨论确切的复杂性,而不是复杂性的上限. (2认同)

Ole*_*hov 5

实际上,当您将数组作为参数传递给方法时 - 对此数组的引用将在引擎盖下传递.数组本身存储在堆上.并且引用的大小可以是48个字节(取决于CPU架构,JVM实现等;更重要的是,JLS没有说明引用在内存中的大小).

另一方面,原始int值总是只消耗4个字节并驻留在堆栈上.