Java继承的大小成本是什么?

52

在互联网上有很多文章试图通过实证估算特定JVM实现中java.lang.Object的开销。例如,我看到在某些JVM中,一个裸的Object的大小开销被估计为8个字节

我想知道的是,extends关系的典型JVM实现是否在类继承层次结构的每个级别都会引入增量大小开销。换句话说,假设您有一个包含N级子类的类层次结构。那么类实例的内存表示的开销是O(1)还是O(N)?

我认为是O(1),因为虽然作为Java Object所需要的隐藏松软内容(vtable、类链)的一些大小随着继承层次结构的增长而增长,但它们是按类增长的,而不是按实例增长的,而JVM实现可以在每个Object上附加一个常量大小的头,并存储指向这些实体的常量大小指针。

因此,理论上,任何Java对象直接附加到它的内存表示的开销对于继承深度N应该是O(1)。有人知道在实践中是否是真的吗?


你试图解决什么问题?因为如果你想使用Java,而且你不能使用原始类型(如果可以的话,你应该使用原始类型);那么你只能使用继承。我预计这是O(N),其中N是继承树的长度。 - Elliott Frisch
@BrunoReis 那也是我的第一个问题。 - Elliott Frisch
2
@Merhdad 没有人说过会有“好处”,也没有人陈述或暗示这是一个选择 - user207421
2
我不会担心实例内存(我认为大多数JVM只是存储对类的引用,然后进行继承查找)。在实践中,您将遇到性能问题,因为调度继承方法并不一定便宜,并且当JVM内联方法时,您将在内存中支付此费用。 - David Ehrmann
很有道理,@Trillian,我认为你是对的。然而,我的问题更多地涉及到与给定JVM实现中继承方式设计相关的开销,而不是与数据对齐相关的内容。 - 0xbe5077ed
显示剩余2条评论
5个回答

25

当有疑问时,请查看源代码(或者说是其中的一个源代码;每个JVM都可以自由选择如何实现,因为标准没有规定任何内部表示)。所以我查看了一下,并在JDK 7-u60的hotspot JVM实现中找到了以下注释

// A Klass is the part of the klassOop that provides:
//  1: language level class object (method dictionary etc.)
//  2: provide vm dispatch behavior for the object
// Both functions are combined into one C++ class. The toplevel class "Klass"
// implements purpose 1 whereas all subclasses provide extra virtual functions
// for purpose 2.

// One reason for the oop/klass dichotomy in the implementation is
// that we don't want a C++ vtbl pointer in every object.  Thus,
// normal oops don't have any virtual functions.  Instead, they
// forward all "virtual" functions to their klass, which does have
// a vtbl and does the C++ dispatch depending on the object's

我的理解是,对于这个非常流行的实现方式,对象实例只存储指向它们类的指针。每个类的实例成本,与该类具有更长或更短的继承链的成本实际上为0。

但是,这些类本身确实占用内存空间(每个类仅占用一次)。深度继承链的运行时效率是另一个问题。

虚方法调用的时间成本是否存在O(N)的复杂度?我怀疑instanceof和类型转换表达式存在这种情况... - 0xbe5077ed
1
@0xbe5077ed JVM可以将类关系存储为图形,并执行传递闭包以获得O(1)的instanceof实现...代价是更大的图形(它可能具有O(N ^ 2)弧线...)。通常,继承链的深度不会随着N的增长而增加,或者与非常小的比例增长,因此该图通常不会那么大。 - Bakuriu
2
多年前,我们(我和我的同事)发现我们实际上需要一个用于instanceof的技巧,尽管我们并没有使用那个确切的技巧。原因是我们不想在异常处理期间计算instanceof时耗尽堆栈。这对于单继承来说不是问题,堆栈使用固定循环就可以解决。但是多重继承(接口)意味着简单的树搜索在最坏情况下需要更多的堆栈。我不知道当时Sun做了什么:我们与他们的许可证意味着我们只能在受控环境中借鉴他们的代码。 - Steve Jessop

24

JVM规范 指出:

Java虚拟机对对象的内部结构没有任何具体规定。

换句话说,规范并不在意你如何实现。但是...

在Oracle的一些Java虚拟机实现中,对类实例的引用是指向一个句柄的指针。该句柄本身是一对指针:一个指向包含对象方法的表格和表示对象类型的Class对象的指针,另一个指向为对象数据从堆中分配的内存。

所以在典型的Oracle实现中,方法的时间复杂度为O(1)。这个方法表格是方法区,每个类都有一个。

Java虚拟机具有可被所有Java虚拟机线程共享的方法区。方法区类似于传统语言的编译代码存储区或操作系统进程中的"text"段。它存储了每个类的结构,例如运行时常量池、字段和方法数据以及方法和构造函数的代码,包括在类和实例初始化中使用的特殊方法(§2.9)和接口初始化。

另外,关于方法项

method_info 结构代表了该类或接口类型声明的所有方法,包括实例方法、类方法、实例初始化方法(§2.9)以及任何类或接口初始化方法(§2.9)。 方法表格不包括元素。

代表从超类或超接口继承的方法。


1
换句话说,在最流行的实现中,答案是肯定的。由于实例的类的继承链更长或更短,每个实例没有额外的空间开销。 - tucuxi
我敢打赌OpenJDK也会是一样的。 - Donal Fellows
2
值得注意的是,虽然继承没有内存成本,但在序列化形式中存在额外的数据,因为它描述了对象的整个类型层次结构。 - MikeFHay

4
一个实例通常需要以下数据,尽管实现方式取决于实现本身要做什么:
- 类及其父类的实例字段,我认为你不打算将其包括在“开销”一词中。 - 某种方式锁定对象 - 如果垃圾收集器重定位对象,则需要记录对象的原始哈希值 (用于 `Object.hashCode`) - 访问类型信息的某些方式
如您在问题中所猜测的,"正常"的Java实现中,类型信息是按类存储的,而不是按实例存储的。 "类型"的定义的一部分是,同一类的两个实例必须具有相同的类型信息,没有明显的理由不共享它。因此,您期望每个实例的开销是恒定的,不依赖于类层次结构。
也就是说,向类添加额外的空类或接口不应增加其实例的大小。但是,我认为语言或JVM规范并没有实际保证这一点,因此不要对 "非正常 " Java 实现能够做什么过多做出假设。
另外,我的清单中的第二和第三项可以通过狡猾的技巧进行组合,从而使它们两个成为单个指针。您链接的文章提到引用占用 4 字节,因此它为对象提供的 8 字节是一个指向类型信息的指针,一个包含哈希码或监视器指针的字段,以及可能在其中一个或两个指针字段的最低 2 位上有一些标志。在 64 位 Java 上,`Object` 的大小将更大。

2

Double和Integer都扩展自Number,而Number又扩展自Object,它们不具有O(n)行为,也就是说,一个Integer的大小并不是一个Object的3倍,因此我认为答案是O(1)。例如,请参见这个旧的SO问题


1
它们通常也会自动装箱/拆箱(当将它们用作原子而不是实际对象时),并且由实现进行了大量优化,因此它们可能不是最具代表性的示例。链接的问题涉及Java 1.3,在自动装箱之前。 - tucuxi

1
在理论上,任何Java对象的内存表示直接附加的开销应该是O(1),对于继承深度N。但是否实践中也是如此呢?
除非每个层级都没有任何实例成员,否则它不可能是O(1)。每个实例成员都需要每个实例的空间。

我认为这个问题是关于从java.lang.Object继承树的深度。 - Elliott Frisch
@ElliotFrisch:“类实例的内存表示的开销是O(1)还是O(N)?” - user207421
我重新阅读了你的答案……我认为这个复杂度应该是O(N*log k),其中N是树的深度,(log k)表示成员变量的影响(假设在实际操作中确实是对数)。 - Elliott Frisch
@ElliotFrisch 如果这是你的答案,请发布它。但我不明白你从哪里得到这个*log(k)*的东西,或者成员变量的空间如何在实践中是“对数的”,无论那意味着什么。 - user207421
这不是一个答案。对于OP的问题,这个成本是在运行时还是在其他方面的成本?在运行时,您必须考虑当前对象的每个成员变量(以及每个父级别的所有成员)沿着链路...或者它是O(N),并且被认为是实例化的摊销。 - Elliott Frisch
显示剩余2条评论

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