【问题标题】:Object size difference in 32 bit and 64 bit systems in JavaJava中32位和64位系统的对象大小差异
【发布时间】:2014-07-19 04:48:10
【问题描述】:

我遇到了一个面试问题:

class Test {
    int a ; 
    int b;
    char c;
}

这个类的内存对象会占用多少内存以及为什么实现时:

a) 32 位计算机

b) 64 位计算机

我得到的答案是:

For 32-bit: 4+4+2+8 = 18 bytes
For 64-bit: 4+4+2+16 = 26 bytes

由于分配了一些额外的内存,在 32 位系统中为 8 字节,在 64 位系统中为 16 字节,除了对象的正常大小。

请您对此声明做一些解释。

P.S.:我也想分享一个我从另一个来源得到的答案(不能依赖,想验证):

在 32 位 pc 对象中,比其数据成员定义的实际 obj 大小多 8 个字节.....而在 64 位 pc 对象中,比其数据成员定义的实际对象大小多 16 个字节.. ..现在问题出现了它为什么会发生......据我所知,其背后的原因是 ::: 在 32 位 pc 中,前 8 个字节由 JVM 保留用于引用类定义、对齐、超类和子类的空间类等..对于64位,它为这些保留16个字节..有一个公式可以计算对象在创建时需要多少堆空间,计算方式为...... .浅堆大小 = [引用类定义] + 超类字段空间 + 实例字段空间 + [对齐]

如何计算对象“本身”需要多少内存?显然有一个公式:

Shallow Heap Size = [对类定义的引用] + 超类字段的空间 + 实例字段的空间 + [对齐]

似乎没有太大帮助,是吗?让我们尝试使用以下示例代码应用公式:

class X {
  int a;
  byte b;
  java.lang.Integer c = new java.lang.Integer();
}
class Y extends X {
  java.util.List d;
  java.util.Date e;
}

现在,我们努力回答的问题是——一个 Y 的实例需要多少浅堆大小?让我们开始计算它,假设我们在 32 位 x86 架构上:

作为起点——Y 是 X 的子类,因此它的大小包括来自超类的“某些东西”。因此,在计算 Y 的大小之前,我们先考虑计算 X 的浅层大小。

跳到 X 上的计算,前 8 个字节用于引用它的类定义。此引用始终存在于所有 Java 对象中,并被 JVM 用于定义以下状态的内存布局。它还具有三个实例变量——一个 int、一个 Integer 和一个 byte。这些实例变量需要堆如下:

一个字节就是它应该是的。内存中的 1 个字节。 我们的 32 位架构中的 int 需要 4 个字节。 对 Integer 的引用也需要 4 个字节。请注意,在计算保留堆时,我们还应该考虑包装到 Integer 对象中的基元的大小,但由于我们在这里计算浅堆,因此我们在计算中仅使用 4 字节的参考大小。 所以——是这样吗? X 的浅堆 = 8 字节来自类定义的引用 + 1 字节(字节)+ 4 字节(int)+ 4 字节(引用整数)= 17 字节?事实上——不。现在起作用的是对齐(也称为填充)。这意味着 JVM 以 8 字节的倍数分配内存,因此如果我们创建 X 的实例,我们将分配 24 字节而不是 17 字节。

如果你能跟随我们直到这里,很好,但现在我们试图让事情变得更复杂。我们不是在创建 X 的实例,而是创建 Y 的实例。这意味着——我们可以从对类定义和对齐的引用中减去 8 个字节。一开始可能不太明显,但是——你有没有注意到,在计算 X 的浅大小时,我们没有考虑到它也像所有类一样扩展了 java.lang.Object,即使你没有在其中明确声明它你的源代码?我们不必考虑超类的标头大小,因为 JVM 足够聪明,可以从类定义本身检查它,而不必一直将其复制到对象标头中。

对齐也是如此——创建对象时,您只需对齐一次,而不是在超类/子类定义的边界处。所以我们可以肯定地说,在为 X 创建子类时,您只会从实例变量中继承 9 个字节。

最后我们可以跳转到初始任务并开始计算 Y 的大小。正如我们所看到的,我们已经向超类字段丢失了 9 个字节。让我们看看当我们实际构造一个 Y 的实例时会添加什么。

Y 引用其类定义的标头占用 8 个字节。和以前的一样。 Date 是对对象的引用。 4字节。简单的。 List 是对集合的引用。又是 4 个字节。琐碎的。 因此,除了来自超类的 9 个字节之外,我们还有来自标头的 8 个字节,来自两个引用(列表和日期)的 2×4 个字节。 Y 实例的总浅层大小为 25 字节,与 32 对齐。

【问题讨论】:

  • 我想答案应该是“未定义”。抛开这个作为面试题的无用,面试官是否指定了某个 JVM 实现?
  • @MarkPeters 我在网上看到了这个问题,所以我想在这里确认一下。
  • 显然这是一个荒谬的问题,因为(除其他原因外)不同的“64 位计算机”具有不同的处理器。我的理解是,基于 64 位 80x86/Pentium 的处理器仍然具有其 32 位表亲所具有的所有 32 位(和 16 位指令),因此没有内在的理由占用 64 位来存储 32位整数。除非它以其他方式以某种方式简化了实现。
  • 已编辑。从其他来源添加了答案,但确实想弄清楚。
  • 如果你在采访中被问到这个问题,不要说“它是未定义的”。任何人都可以这么说——它对你没有任何积极的意义。尝试回答并解释您对标题、字长、体系结构、内存对齐等的了解。如果必须在最后确定答案。

标签: java object memory memory-management jvm


【解决方案1】:

取决于 JVM。至于HotSpot JVM,正确答案是:

  • 32 位 JVM:24 字节 = align8(4 字节 mark_word + 4 字节类引用 + 4 + 4 + 2 字节字段数据)
  • 64-bit JVM -XX:+UseCompressedOops: 24 bytes = align8(8 bytes mark_word + 4 bytes class reference + 4 + 4 + 2 bytes field data)
  • 64-bit JVM -XX:-UseCompressedOops: 32 bytes = align8(8 bytes mark_word + 8 bytes class reference + 4 + 4 + 2 bytes field data)

【讨论】:

  • 您确定 align8 可以在整个对象上应用一次吗?根据this article,每个超类都分别与 WORD 大小对齐。对于带有 UseCompressedOops 的 64 位 JVM,这将为我们提供 align8(align8(8 mark_word + 4 class ref) + align8(4 + 4 + 2 field data)) = 28 个字节,而不是 24 个。除非标头字节不遵循超类对齐策略。
  • @shmosel 是的,在类 ref 之后的标头中的 4 个字节也用于对象字段(除非字段是 longdouble,它们应该是 8 字节对齐的)。跨度>
【解决方案2】:

我会这样回答这样的面试问题:

  • 未指定...

  • 这可能取决于 Java 实现(即 Java 版本、供应商和目标指令集/架构)以及 32 位还是 64 位。

  • 对象由字段(其对齐方式因平台而异)和对象标头(其大小因平台而异)组成,然后将大小向上舍入为特定于平台的堆对象大小粒度。

  • 更好的方法是测量对象大小(例如,在现代 HotSpot JVM 上使用 TLAB)。

  • 虽然对象大小可能会有所不同,但 intchar 的含义不会。 int 始终是 32 位有符号的,char 始终是 16 位无符号的。


如果面试官告诉你:

对于 32 位:4+4+2+8 = 18 个字节

这可能意味着 intintchar 加上 2 个 32 位字的对象头。然而,我认为他错了。事实上,2 应该是4,因为我相信字段通常存储为在 32 位字边界上对齐的 4(或 8)字节

对于 64 位:4+4+2+16 = 26 字节

同上,但对象头有 2 个 64 位字。再说一次,我不认为这是正确的。

由于分配了一些额外的内存,在 32 位系统中为 8 字节,在 64 位系统中为 16 字节,除了对象的正常大小。

堆分配的粒度是2个字的一些倍数。

但我要强调的是,这不一定是正确的。当然,官方 JVM 规范中没有任何内容要求这样表示对象。

【讨论】:

  • +1 同意您只能谈论特定版本的 JVM。 HotSpot/OpenJDK 是一个很好的起点 ;)
【解决方案3】:

要获得准确的会计信息,您可以关闭 TLAB。 GC changeignt eh 结果的风险仍然存在,但它的命中率和失误率要低得多。

public class Main {
    static class Test {
        int a ;
        int b;
        char c;
    }

    public static void main(String sdf[]) throws Exception {
        // warmup
        new Test();
        long start = memoryUsed();
        Object o = new Test();
        long used = memoryUsed() - start;
        System.out.printf("Memory used for %s, was %,d bytes%n", o.getClass(), used);

    }

    public static long memoryUsed() {
        return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    }
}

注意:对于大型测试,将 Eden 大小增加到 1 GB 或更大会有所帮助。

Memory used for class Main$Test, was 24 bytes

对于 64 位:4+4+2+16 = 26 字节

这是不可能的,因为 HotSpot JVM 只分配 8 个字节的倍数。顺便说一句,您可以将 Java 8 中的最小分配单元增加到 16 或 32 字节。

标头是 12 字节,所以大小是

4 + 4 + 2 + 12(标题)+ 2(填充到 8 字节倍数)= 24

-XX:ObjectAlignmentInBytes=16 运行你会看到的

Memory used for class Main$Test, was 32 bytes

这就提出了一个问题,如果这会浪费内存,你为什么要这样做?

好处与压缩 oops 有关,它允许您在 64 位 JVM 中使用 32 位引用。通常压缩的 oops 可以寻址 32 GB,因为它知道所有对象都按 8 个字节对齐,即您可以寻址 4 G * 8 个字节。但是,如果将字节对齐增加到 16,则可以使用压缩 oops 寻址 4G * 16 字节或 64 GB。我相信您可以使用 32 字节对齐,但在大多数情况下,这会浪费更多的内存。

【讨论】:

【解决方案4】:

这是一个棘手的问题。

如果您考虑可变大小,则它不会改变。 32 位和 64 位都将为您提供 24 个字节。但是,64 位会在内存上加载更多,这会消耗更多的内存。

记住 32 和 64 是 CPU 可以处理的,而不是可以分配的内存量。

有关参考,请参阅 Red Hat 的以下声明:

9.1。 32 位与 64 位 JVM

在讨论性能时通常会提出一个问题,即哪个提供更好的整体性能:32 位还是 64 位 JVM?看起来,由 64 位操作系统托管的 64 位 JVM 在现代、支持 64 位的硬件上应该比 32 位 JVM 性能更好。为了尝试提供有关该主题的一些定量数据,使用行业标准工作负载进行了测试。所有测试均在同一系统上运行,具有相同的操作系统、JVM 版本和设置,但有一个例外情况如下所述。

来源:https://access.redhat.com/documentation/en-US/JBoss_Enterprise_Application_Platform/5/html/Performance_Tuning_Guide/chap-Performance_Tuning_Guide-Java_Virtual_Machine_Tuning.html

【讨论】:

  • 说,以后有面试官问我这个问题,那我应该怎么回答呢?因为,坦率地说,到目前为止我还没有这个概念。
  • 只需说“堆内存量相同”。
  • 如果您关闭 TLAB -XX:-UseTLAB,您将获得更准确的会计信息。
猜你喜欢
  • 2019-01-03
  • 1970-01-01
  • 1970-01-01
  • 2023-03-04
  • 2011-01-26
  • 1970-01-01
  • 2011-03-24
  • 2012-08-10
  • 1970-01-01
相关资源
最近更新 更多