Java中使用原始数据类型的有效场景是什么时候和在哪里?

5

考虑以下两段 Java 代码:

Integer x=new Integer(100);
Integer y=x;
Integer z=x;


System.out.println("Used memory (bytes): " +   
(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));

在我的系统上测试时,内存使用情况为:已使用内存(字节):287848


以及

int a=100;
int b=a;
int c=a;

System.out.println("Used memory (bytes): " + 
(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));

在我的系统上测试时,内存使用情况如下: 使用的内存 (字节): 287872



以下是相关内容:

Integer x=new Integer(100);       
System.out.println("Used memory (bytes): " +  
(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));

并且
int a=100;        
System.out.println("Used memory (bytes): " + 
(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));

在上述两种情况下,在我的系统上测试时,内存使用情况完全相同:已用内存(字节):287872

该语句

System.out.println("Used memory (bytes): " + 
(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));

将显示目前使用的总内存 [总可用内存-当前可用内存](以字节为单位)。


我通过上述方法曾验证过,在第一个情况下内存使用量(287848)较低,而在第二个情况下(287872)则更高,而在其余两种情况下,它们完全相同(287872)。当然,并且显然应该如此,因为在第一个情况下,y和z包含x中保存的引用的副本,它们全部(x、y和z)指向同一/共同的对象(位置),这意味着第一个情况比第二个情况更好更合适,在其余两种情况下,存在具有完全相同的内存使用量(287872)的等价语句。如果是这样的话,那么在Java中使用基本数据类型应该是无用和可避免的,尽管它们基本上是为了更好地利用内存和CPU而设计的。那么为什么Java中会存在基本数据类型呢?


已经在此处发布了与此类似的问题,但它没有这样的情况。

该问题在此处。


7
无论你认为你在测试什么 - 你并没有。一个 Integer 必须比一个 int 使用更多的内存。就是这样。 - Matt Ball
你运行了这些测试多次吗?我理解对象会导致性能下降,因为"new"会将对象加载到堆上。然而,原始类型不应该有这个问题。 - BOMEz
如果您根据Runtime可用内存的打印输出来判断基元类型的有效性,那么在Java方面您还有很长的路要走。 - Perception
是的,我已经至少测试了以上所有情况三次,并且在所有情况下都得到了与我提到的相同的结果。 - Bhavesh
2
你的测试基本上有缺陷,因此这个问题毫无意义。 - Micah Hainline
不要测试这些东西(但如果你确实要测试,请至少创建一个数组,而不是测试单个值),只需查看实现即可,这样做a)是正确的,b)更简单。众所周知,在Hotspot上的每个Java对象都有2个字的开销(好吧,这是简化了,但对于这里来说足够了),大小必须是8的倍数。 - Voo
10个回答

2
我不建议关注Runtime.freeMemory——它非常模糊(它是否包括未使用的堆栈空间?PermGen空间?堆对象之间太小而无法使用的间隙?)并且在不停止所有线程的情况下给出任何精确的测量是不可能的。
整数比int占用更多的空间,因为只有对Integer的引用就需要32位(64位JVM没有压缩指针的话则需要64位)。
如果您真的想进行经验测试,请让多个线程深度递归然后等待。就像这样:
class TestThread extends Thread {
    private void recurse(int depth) {
        int a, b, c, d, e, f, g;
        if (depth < 100)
            recurse(depth + 1);
        for (;;) try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {}
    }

    @Override public void run() {
        recurse(0);
    }

    public static void main(String[] _) {
        for (int i = 0; i < 500; ++i)
            new TestThread().start();
    }
}

1

首先,Integer 包装了一个 int,因此 Integer 必须至少与 int 一样大。

docs(我真的怀疑这是必要的):

Integer 类在对象中包装了一个基本类型 int 的值。类型为 Integer 的对象包含一个 int 类型的字段。

所以显然原始的 int 仍然在使用。

不仅如此,对象有更多的开销,最明显的就是当你使用对象时,你的变量包含一个引用

Integer obj = new Integer(100);
int prim = 100;

例如:obj 存储一个对 Integer 对象的引用,该对象包含一个 int,而 prim 存储值 100。这足以证明使用 Integer 而不是 int 带来了更多的开销。而且还有更多的开销。


1

包装器包含一个原始类型作为字段,但它会导致额外的开销,因为它是一个对象。引用也占用空间,但你的示例并没有真正设计来展示这一点。

你设计的测试并不适合精确测量,但既然你已经使用了它们,请尝试使用这个示例:

public static void main(String[] args) {
  int numInts = 100000;
  Integer[] array = new Integer[numInts];
//  int[] array = new int[numInts];
  for(int i = 0; i < numInts; i++){
    array[i] = i; //put some real data into the arrays using auto-boxing if needed
  }

  System.out.println("Used memory (bytes): " +   
  (Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));
}

现在再试一次,但是取消原始行的注释并注释掉包装器行。你应该会发现包装器占用了更多的内存。


0

如果您查看java.lang.Integer的源代码,您会发现该值被存储为int。

private int value;

你的测试无效,就是这样。


在那个特定版本中,private int value; - jli

0

证明:

当你运行这些测试时,第二个测试会出现断言错误(因为内存变低了,即使你停止重置内存字段)。一旦你尝试使用10,000个循环运行这些测试,你将在两个测试中都遇到StackOverflowError。

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import org.junit.Test;


public class TestRedundantIntegers {

    private long memory;

    @Test
    public void whenRecursiveIntIsSet() { 
        memory = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); 
        recurseInt(0, 100);
    } 

    private void recurseInt(int depth, int someInt) { 
        int x = someInt;

        assertThat(memory,is(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));
        memory=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); 
        if (depth < 1000) 
            recurseInt(depth + 1, x); 
    } 

    @Test
    public void whenRecursiveIntegerIsSet() { 
        memory = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); 
        recurseInteger(0, new Integer(100));
    } 

    private void recurseInteger(int depth, Integer someInt) { 
        Integer x = someInt;

        assertThat(memory,is(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));
        memory=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); 
        if (depth < 1000) 
            recurseInt(depth + 1, x); 
    } 
}

0

在您的第一个示例中,您有相当于1个整数和2个指针的等效物。 因为Integer是一个对象,它具有指针属性并包含函数。

通过使用int而不是Integer,您正在复制该值3次。 您有24字节的差异,用于存储额外2个int的标题和值。尽管我不会信任您的测试:JVM可能有些随机,它的垃圾收集非常动态。就单个Integer与int所需的内存而言,Integer将占用更多空间,因为它是一个对象,因此包含更多信息。


0

Runtime.getRuntime().freeMemory():仅通过此项获取增量并不能给出正确的统计数据,因为存在许多移动部件,如垃圾回收和其他线程。

整数占用的内存比int原始类型更多。


0

你的测试用例太简单了,无法得出任何确定性的结果。

任何少于5秒的测试用例都没有意义。

你需要至少对你创建的这些对象做一些操作。JVM 可以简单地查看你的代码,然后不做任何事情,因为你的对象从未被使用,而你退出了。(不能确定 JVM 解释器会做什么,但 JIT 将使用逃逸分析将你的整个测试用例优化成无效代码)

首先,如果你想要内存效率,原始类型更小,因为它们的大小就是它们的大小。包装对象是对象,需要进行垃圾回收。它们内部有大量的字段可以使用,这些字段存储在某个地方...

原始类型并非“设计”得更有效。包装对象的设计是为了更加友好。你需要原始类型,否则你怎么存储一个数字?

如果你真的想看到内存差异,请使用一个真实的应用程序。如果你想自己编写它,那就去吧,但这需要一些时间。使用一些文本编辑器,将每个 int 声明替换为 Integer,long 替换为 Long 等等。然后查看内存占用情况。如果你看到你的电脑爆炸了,我也不会感到惊讶。

从编程角度来看,您需要在必要时使用基元类型,而在必要时使用包装对象。当两者都适用时,这取决于您的个人偏好。相信我,这种情况并不多见。

0

0
关于“何时何地”:在需要Object的非原始类型中使用它,而在其他任何地方都使用原始类型。例如,泛型的类型不能是原始类型,因此您无法与它们一起使用原始类型。即使在引入泛型之前,像HashSetHashMap这样的东西也无法存储原始类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接