52

互联网上有各种文章试图凭经验估计java.lang.Object特定 JVM 实现的开销。例如,我已经看到在某些 JVM 中Object 估计为 8 字节的裸机的大小开销。

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

我想它是 O(1),因为虽然你需要成为 Java 的一些隐藏的蓬松东西Object(vtable,类链)的大小会随着继承层次结构的增长而增长,但它们会按类增长,而不是按实例增长,并且 JVM 实现可以将指向这些实体的恒定大小指针存储在附加到每个Object.

所以理论上,直接附加到任何 Java 对象的内存表示的开销对于继承深度 N 应该是 O(1)。有谁知道这在实践中是否正确?

4

5 回答 5

25

如有疑问,请查看源代码(嗯,一个源代码;每个 JVM 都可以自由选择如何做,因为标准不要求任何内部表示)。于是我看了一下,在JDK 7-u60的热点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。虽然类本身确实占用了内存空间(但每个类只有一次)。深度继承链的运行时效率是另一回事。

于 2014-05-30T08:48:21.083 回答
24

JVM 规范指出

Java 虚拟机不要求对象有任何特定的内部结构。

所以规范并不关心你是怎么做的。但是...

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

所以在典型的 Oracle 实现中,方法是 O(1)。这个方法表是每个类的方法区。

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

另外,关于方法条目

method_info结构体表示此类或接口类型声明的所有方法,包括实例方法、类方法、实例初始化方法(第 2.9 节)以及任何类或接口初始化方法(第 2.9 节)。方法表不包括表示从超类或超接口继承的方法的项。

于 2014-05-30T06:23:05.733 回答
4

一个实例通常需要以下数据,尽管具体做什么取决于实现:

  • 类及其父类的实例字段,我假设您并不打算将其包含在“开销”一词中
  • 一些锁定对象的方法
  • 如果垃圾收集器重新定位对象,那么有一些方法可以记录对象的原始哈希(对于Object.hashCode
  • 一些访问类型信息的方法

正如您在问题中猜测的那样,在“普通”Java 实现中,类型信息是按类存储的,而不是按实例存储的。“类型”的部分定义是同一类的两个实例必然具有相同的类型信息,没有明显的理由不共享它。所以你会期望每个实例的开销是恒定的,而不是依赖于类层次结构。

也就是说,向一个类添加额外的空类或接口不应该增加其实例的大小。不过,我认为无论是语言还是 JVM 规范都不能真正保证这一点,所以不要对允许“非正常”Java 实现做什么做太多假设。

顺便说一句,我清单上的第二个和第三个东西可以通过狡猾的诡计组合在一起,这样它们就成了一个指针。您链接到的文章指的是占用 4 个字节的引用,因此它为一个对象提供的 8 个字节是一个指向类型信息的指针,一个包含哈希码或指向监视器的指针的字段,以及最低的一些标志这些指针字段中的一个或两个的 2 位。Object在 64 位 Java 上会(您期望)更大。

于 2014-05-30T08:22:43.313 回答
2

Double和Integer,扩展Number,扩展Object,没有O(n)的行为,也就是说,Integer的大小不是Object的3X,所以我认为答案是O(1)。例如看到这个老问题

于 2014-05-30T06:54:27.630 回答
1

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

除非每个级别都有零个实例成员,否则它不可能是 O(1)。每个实例成员都需要每个实例的空间。

于 2014-05-30T06:05:18.820 回答