内存中一个字节的大小 - Java

59

我听到了有关Java程序中字节占用的内存量的不同意见。

我知道在Java字节中最多只能存储+127,而文档中说字节只有8位,但在这里我被告知它实际上占用与Int相同的内存空间,因此只是一种有助于代码理解而不是效率的类型。

有人能澄清这个问题吗?这是否是一种具体实现的问题?


一个字节根据CPU架构需要占用4/8个字节,而在byte[]中的一个字节则恰好占用一个字节+对象头(+尾部对齐)。 - bestsss
2
“我知道在Java中一个byte最多只能存储+127”-- 从某种意义上来说,这不是真的。你可以在一个byte中存储256个不同的值,因此你可以存储比127更多的值:如果你从0开始,那么可以存储高达255. 这完全取决于你如何处理这8位。只为了追求严谨而已 :P - Unai Vivi
13个回答

68

好的,有很多讨论但是代码很少 :)

这里是一个快速的基准测试。在这种情况下,测试内存存在奇怪的问题,因为JIT等原因,但是对于足够大的数字,它仍然很有用。它有两种类型,每种类型都有80个成员 - LotsOfBytes有80个字节,LotsOfInts有80个整数。我们建立了许多这样的对象,确保它们不会被垃圾回收,并检查内存使用情况:

class LotsOfBytes
{
    byte a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
    byte b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
    byte c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
    byte d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
    byte e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}

class LotsOfInts
{
    int a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
    int b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
    int c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
    int d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
    int e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}


public class Test
{
    private static final int SIZE = 1000000;

    public static void main(String[] args) throws Exception
    {        
        LotsOfBytes[] first = new LotsOfBytes[SIZE];
        LotsOfInts[] second = new LotsOfInts[SIZE];

        System.gc();
        long startMem = getMemory();

        for (int i=0; i < SIZE; i++)
        {
            first[i] = new LotsOfBytes();
        }

        System.gc();
        long endMem = getMemory();

        System.out.println ("Size for LotsOfBytes: " + (endMem-startMem));
        System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));

        System.gc();
        startMem = getMemory();
        for (int i=0; i < SIZE; i++)
        {
            second[i] = new LotsOfInts();
        }
        System.gc();
        endMem = getMemory();

        System.out.println ("Size for LotsOfInts: " + (endMem-startMem));
        System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));

        // Make sure nothing gets collected
        long total = 0;
        for (int i=0; i < SIZE; i++)
        {
            total += first[i].a0 + second[i].a0;
        }
        System.out.println(total);
    }

    private static long getMemory()
    {
        Runtime runtime = Runtime.getRuntime();
        return runtime.totalMemory() - runtime.freeMemory();
    }
}

我的电脑上的输出:

Size for LotsOfBytes: 88811688
Average size: 88.811688
Size for LotsOfInts: 327076360
Average size: 327.07636
0

显然这里有一些开销 - 看起来是8个字节,尽管对于LotsOfInts只有7个(?像我之前说的,这里有一些奇怪的地方)- 但关键是这些字节字段似乎被紧密打包到了LotsOfBytes中,经过去除开销后,它所占用的内存只有LotsOfInts的四分之一。


2
这取决于JVM。Sun对8字节边界进行了对齐。 - kohlerm
2
@kohlerm:那是使用 Sun JVM。 - Jon Skeet
1
不错的测试,但如果您使用以下代码进行测试,class LotsOfBytes { byte a0; } class LotsOfInts { int a0; } 将没有任何显着差异。 - Ivan Balashov
请解释一下我的输出结果:LotsOfBytes 的大小为 -914712,平均大小为 -914.712。LotsOfInts 的大小为 336000,平均大小为 336.0。0。 - Bitterblue
@mini-me:没有头绪——我需要了解更多上下文信息(你是如何运行它的等等)。听起来像是你有一些被单独垃圾回收的东西... - Jon Skeet
显示剩余2条评论

20

是的,在Java中,byte变量实际上占据了4个字节的内存。然而,对于数组来说并非如此。一个长度为20个字节的byte数组实际上只在内存中占据了20个字节的空间。

这是因为Java字节码语言只知道两种整数类型:int和long。因此,它必须将所有数字在内部处理为这两种类型,这两种类型在内存中分别占据4个字节和8个字节。

但是,Java可以使用每种整数数字格式的数组。因此,short数组的存储实际上是每个元素占用两个字节,byte数组的存储实际上是每个元素占用一个字节。

我一直强调“存储”,是因为在Java中,数组也是一个对象,每个对象都需要自己的多个字节的存储空间,无论实例变量还是数组存储所需的存储空间。


哦,是的,我忘记了那个不太小的细节! - Steve McLeod
1
不要忘记,字节数组也有作为对象的常规开销和长度。哦,你的变量就是一个引用(4或8个字节)。因此,要实际拥有20个可用且有用的字节将需要36个字节,假设没有别名。我建议坚持使用20字节字段 :) - Jon Skeet
@Jon @Mecki,你们能否给出计算int[]数组大小的更加精确的公式?它会是4[=length] + 4[=int_size]*length(array) + 8_byte_align吗? - dma_k
1
@Mecki,我在另一个帖子中找到了这个链接,它满足了我的好奇心:http://kohlerm.blogspot.com/2008/12/how-much-memory-is-used-by-my-java.html - dma_k
请注意:这些只适用于SUN的JVM,正如博客文章所述(而且SUN现在是Oracle;Sun JVM仅适用于Windows、Linux和Solaris),不一定适用于其他任何存在的JVM。此外,SUN可能会在每个新版本中更改它们(例如,Java 1.7/1.8可能具有完全不同的值)。如果您想确定,请自行测试(创建大量数组,测量JVM的内存消耗)-如果JVM存在源代码,请查看源代码,您将获得绝对正确的值。 - Mecki
显示剩余3条评论

7

Java在实现或平台方面从来不是特定的(至少就原始类型大小而言)。无论您在哪个平台上,它们的原始类型始终保持相同。这与C和C ++有所不同(并被认为是一种改进),其中一些原始类型是特定于平台的。

由于底层操作系统以每次处理四个字节(在64位系统中为八个字节)的速度更快,因此JVM可能会分配更多字节来存储原始字节,但您仍然只能在其中存储-128到127的值。


1
即使使用4个字节来存储一个字节,字节数组可能会被压缩。如果一个byte[4]使用16个字节而不是4个字节,我会感到惊讶。 - Kip
1
可能。这将取决于具体的实现。我不确定哪种方法会更快。 - Bill the Lizard
1
这篇文章是正确的,但评论是错误的。在内存中,一个单字节变量需要1个字节+对齐空间。例如,在Sun JVM上,一个8字节的变量需要8个字节的空间。 - kohlerm

5
一个揭示性的练习是在一些对字节和整数进行简单处理的代码上运行javap。你会看到期望整数参数操作字节的字节码,并插入强制转换一个类型到另一个类型的字节码。
但请注意,字节数组不会作为4字节值的数组存储,因此1024长度的字节数组将使用1k内存(忽略任何开销)。

5
我使用http://code.google.com/p/memory-measurer/进行了一项测试。请注意,我使用的是64位的Oracle/Sun Java 6,没有对引用等内容进行任何压缩。
每个对象都占用一定的空间,此外JVM还需要知道该对象的地址,而“地址”本身就占据8个字节。
对于基本类型,似乎会将其转换为64位以获得更好的性能(当然!)。
byte: 16 bytes,
 int: 16 bytes,
long: 24 bytes.

用数组:
byte[1]: 24 bytes
 int[1]: 24 bytes
long[1]: 24 bytes

byte[2]: 24 bytes
 int[2]: 24 bytes
long[2]: 32 bytes

byte[4]: 24 bytes
 int[4]: 32 bytes
long[4]: 48 bytes

byte[8]: 24 bytes => 8 bytes, "start" address, "end" address => 8 + 8 + 8 bytes
 int[8]: 48 bytes => 8 integers (4 bytes each), "start" address, "end" address => 8*4 + 8 + 8 bytes
long[8]: 80 bytes => 8 longs (8 bytes each), "start" address, "end" address => 8x8 + 8 + 8 bytes

现在猜猜看...

    byte[8]: 24 bytes
 byte[1][8]: 48 bytes
   byte[64]: 80 bytes
 byte[8][8]: 240 bytes

P.S. Oracle Java 6,最新版本,64位,1.6.0_37,MacOS X


4

这取决于JVM如何应用填充等技术。在任何理智的系统中,字节数组将被打包成每个元素1字节,但具有四个字节字段的类可以紧密地打包或填充到字边界上 - 这取决于实现。


这是否意味着仅使用一个字节将无法节省内存,但如果我使用多个字节变量(或字节数组),我可以节省大量内存。(即,byte [10] [10]应该比int [10] [10]占用更少的内存) - Ben Page
可能吧 :)(我肯定一个字节数组占用的空间会比一个整数数组小 - 但是四个字节变量和四个整数变量呢?不清楚。) - Jon Skeet
(请参见我的另一个回答,证明至少一些JVM存在packing。) - Jon Skeet

2
您所听到的是完全正确的。Java字节码规范只有4字节类型和8字节类型。
byte、char、int、short、boolean、float每个都存储在4个字节中。
double和long存储在8个字节中。
然而,字节码只是故事的一半。还有JVM,它是特定于实现的。Java字节码中有足够的信息来确定变量被声明为一个字节。JVM实现者可能会决定仅使用一个字节,尽管我认为这是高度不可能的。

1
实际上它也有数组,而字节数组实际上就是字节数组,每个字节都确实是一个字节。 - Mecki
1
是的,它确实如此。但是Java堆栈被定义为一系列4字节插槽。将元素推入堆栈时总是使用一个插槽(对于4字节类型)或两个插槽(对于8字节类型)。bipush将使用一个插槽。 - Steve McLeod
1
JVM 肯定知道一个字段是字节字段而不是整数字段,对吧?它可能选择不紧密地打包它们,但这肯定是一种实现决策。 - Jon Skeet
3
即使Java堆栈基于整数,也不意味着它的对象布局必须如此。我正在准备一个基准测试... - Jon Skeet
嗨,Jon,基准测试显示了什么?顺便说一下,我听说另一个栈式JVM只处理int/long的原因是性能:在单个堆栈行中使用其他类型的字节不快:移位操作、某些逻辑等会减慢执行速度。 - yetanothercoder
显示剩余6条评论

2

您可以始终使用长整型并自行打包数据以增加效率。这样,您就可以保证始终使用所有4个字节。


甚至可以在long中存储所有8个字节的数据 :) - JeeBee
1
如果您真的考虑这种类型的内存管理,我认为您应该使用C++或其他能让您自己进行内存管理的语言。在Java中使用这种技巧会导致更多的JVM开销损失,而无法达到节省内存的目的。 - rmeador
啊,在32位系统上,C/C++中的int和long都是32位或4字节;我忘记了在其他系统上long实际上是一个长整型 - 当他们添加“longlong”以表示8字节长时,这总是让我笑... 哎呀。 - Christopher Lightfoot
你可以通过使用int类型来处理4个字节,而不是节省内存(通常会有损失),从而获得更好的性能。你不需要打包byte[]数组。在对象中,应避免使用单个字节字段,因为对齐将增加内存开销。 - kohlerm

2

0

想要指出的是,这个语句

在Java字节中,你最多只能存储+127

并不完全正确。

你总是可以在一个字节中存储256个不同的值,因此你可以轻松地将0..255范围内的值视为“无符号”字节。

这完全取决于如何处理这8位。

例子:

byte B=(byte)200;//B contains 200
System.out.println((B+256)%256);//Prints 200
System.out.println(B&0xFF);//Prints 200

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