oop-klass模型

前言

看任何JVM的书,oop-klass总是绕不去的坎
我一直想理解这些,但是就是理解不了,但是也说不出哪里不明白。

这篇文章不会对这个模型做系统的阐述,假设读者已经看过oop-klass模型,但是还是有点一知半解的状态。

oop-klass

为什么要这么设计?
其实很多书也提到,既然HotSpot完全基于C++去编写,要实现多态完全可以进行C++层面的转换就行。
但是C++的多态其实每一个对象都维护了一个VTable,就是虚函数表,函数表可以理解为一个函数指针的数组,这个数组在内存上和一个对象是一起的。
但是很多书中提到,为了避免每个对象都有一个VTable,JVM定义了oop-klass模型,其中oop就是保存数据结构的,而klass则担当了一部分VTable的功能,这样的话,VTable就是每个类只存在一个
也就是说,对oop而言,它对应的klass是单例的,而Java层面每New一个对象,都会在JVM生成一个oop。

所以在Java中,类和对象的关系更像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
Student student1 = new Student("zhang", 19);
Student student2 = new Student("wang", 20);
}
}
class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}
}

动态绑定与VTable

这里再提一个我对动态绑定的理解。
其实动态绑定这个概念,大家都知道大概是个什么意思,但是再详细的说,就理不清很多细节。

现在我们都知道动态绑定是和虚表有关,在Java字节码中,需要动态绑定的指令是invokevirtual
https://cs.au.dk/~mis/dOvs/jvmspec/ref–35.html
这个ref中讲的是,动态绑定需要一个寻找函数的过程

invokevirtual retrieves the Java class for objectref, and searches the list of methods defined by that class and then its superclasses, looking for a method called methodname, whose descriptor is descriptor.

这当然我感觉是一种很扯淡的说法,要是这么来,运行时时间都花在匹配函数上去了。
而寻找函数的过程,我理解完全可以放在编译期去完成
我更认同是这种方式,就是不需要进行运行时的函数匹配,动态绑定的意思是需要在运行时改变代码段的函数指针,类似于下面文章中提到的
https://www.jianshu.com/p/fa50296b301c

编译器内部会发生转换,产生类似下面的代码:
( ( p->vptr )[0] ) (p); //( p->vptr )[0]是函数入口地址

这段代码的生成,对于C++而言是编译时,对于Java则是ClassLoader的时候,并不是运行时。

关于动态绑定,还有一些其他模棱两可的说法
比如这个文章中
https://zhuanlan.zhihu.com/p/24317613

它提到在invokevirtual的调用过程,需要去查父类的方法表

(2) 在Father类型的方法表中查找方法f1,如果找到,则将方法f1在方法表中的索引项11(如上图)记录到AutoCall类的常量池中第15个常量表中(常量池解析 )。这里有一点要注意:如果Father类型方法表中没有方法f1,那么即使Son类型中方法表有,编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。

这我也是觉得是脱裤子放屁的说法,为啥要去查父类的方法表,要知道父类的方法表,是在另外一个Klass对象里,要是继承链比较长,那么需要很多次指针寻址才能找到。

其实这个和另外一个比较经典的动态绑定的解释很像:

如果子类Son中定义了 method() 的方法,则直接调用子类中的相应方法;如果子类Son中没有定义相应的方法,则到其父类中寻找method()方法。

很多人把这个过程理解为动态绑定,这个的问题也是一样的,它的假设是子类的Klass对象中没有父类的方法指针,所以需要去父类的Klass的VTable中去找。

但是通过一些文章我们可以看出,其实子类的VTable完全的Copy了一份父类的VTable。
https://stackoverflow.com/questions/18082651/how-does-dynamic-binding-happens-in-jvm
https://cloud.tencent.com/developer/article/1180981

所以至此,整个动态绑定的过程我们就已经理解,所谓动态绑定,就是比静态绑定多了一个指针寻址,去Klass中找VTable的过程。

1.7到1.8

很多书中都提到,oop-klass模型在1.8中改变较大。
原因是1.8中去掉了永久代(Perm),而改为了元空间(MetaSpace)。

我们先看看1.7中的oop-klass的继承链
https://github.com/openjdk-mirror/jdk7u-hotspot/blob/master/src/share/vm/oops/oopsHierarchy.hpp

oop继承链

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef class oopDesc*                            oop;
typedef class instanceOopDesc* instanceOop;
typedef class methodOopDesc* methodOop;
typedef class constMethodOopDesc* constMethodOop;
typedef class methodDataOopDesc* methodDataOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;
typedef class constantPoolOopDesc* constantPoolOop;
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
typedef class klassOopDesc* klassOop;
typedef class markOopDesc* markOop;
typedef class compiledICHolderOopDesc* compiledICHolderOop;

klass继承链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Klass;
class instanceKlass;
class instanceMirrorKlass;
class instanceRefKlass;
class methodKlass;
class constMethodKlass;
class methodDataKlass;
class klassKlass;
class instanceKlassKlass;
class arrayKlassKlass;
class objArrayKlassKlass;
class typeArrayKlassKlass;
class arrayKlass;
class objArrayKlass;
class typeArrayKlass;
class constantPoolKlass;
class constantPoolCacheKlass;
class compiledICHolderKlass;

可以说是非常多

但是到了1.8中,就变的很少
oop继承链

1
2
3
4
5
typedef class oopDesc*                            oop;
typedef class instanceOopDesc* instanceOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;

klass继承链

1
2
3
4
5
6
7
8
class Klass;
class InstanceKlass;
class InstanceMirrorKlass;
class InstanceClassLoaderKlass;
class InstanceRefKlass;
class ArrayKlass;
class ObjArrayKlass;
class TypeArrayKlass;

而少掉的那部分,其实只是换了个名字,叫做Metadata

1
2
3
4
5
6
7
8
9
//      class MetaspaceObj
class ConstMethod;
class ConstantPoolCache;
class MethodData;
// class Metadata
class Method;
class ConstantPool;
// class CHeapObj
class CompiledICHolder;

可能你已经猜到了,Metadata的这部分,已经全部转移到了元空间。

以下是 JDK 1.7 中的类在 JDK 1.8 中的存在形式:

  • klassOop -> Klass*
  • klassKlass 不再需要
  • methodOop -> Method*
  • methodDataOop -> MethodData*
  • constMethodOop -> ConstMethod*
  • constantPoolOop -> ConstantPool*
  • constantPoolCacheOop -> ConstantPoolCache*

Klass少掉的部分,还可以理解,但是为啥oop会少了这么多。

这里就牵扯到永久代和元空间的区别了。

首先的问题是,为什么撤销永久代而换成元空间
我找到了当初的JEP
http://openjdk.java.net/jeps/122?spm=a2c4e.11153940.blogcont20279.13.13fd33dbw7ltIv

永久代的调优非常难,永久代的大小很难确定,其中涉及到太多因素,如类的总数、常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。

这里就要提到元空间的特点

Symbols were moved to the native heap
Interned strings were moved to the Java Heap
Class statics were moved to the Java Heap

  • 永久代属于堆,有大小限制。元空间使用堆外内存,理论上无内存限制
  • JDK7之前的HotSpot,字符串常量池的字符串被存储在永久代中,因此可能导致一系列的性能问题和内存溢出错误。在JDK8中,字符串常量池中只保存字符串的引用。

而如果你去看JDK1.7的oopDesc的定义,你会发现一个奇怪的事

1
2
3
4
5
6
7
8
9
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
}

这里的Klass,为什么是Oop对象?

这里引用R大的解释
https://rednaxelafx.iteye.com/blog/858009

因为HotSpot 1.7之前,包括Class在内的元数据对象都需要被GC管理,因此这四列的对象其实都是oopDesc类型,只不过第一列是描述实例的instanceOopDes, 第二三四列为klassOopDesc;这个klassOopDesc可以看作是klass的一个wrapper,仅仅为了被gc更容易滴管理和表示,它的内部有一个klass成员来表达klass的信息。
所以第二列的 klassOopDesc 内部的klass 乃Integer类的klass,第三列的klass为 klassOopDesc这个对象的klass——instanceKlassKlass,那第三列这个类的klass是什么呢?由于描述instanceKlassKlass,methodKlassKlass,xxxxKlassKlass等一大票KlassKlass需要的元数据实际上是相同的,他们就是第四列的KlassKlass,第四列的KlassKlass的klass可以用它自己来描述,于是就圆满了。

简单说就是为了偷懒,用Oop包裹一层,让GC一同管理了。
到了元方法区,已经不属于堆了,自然不需要这个了,自然可以去掉。

所以到了1.8,就变成了正常的状态

1
2
3
4
5
6
7
class oopDesc {
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}

MarkOop

还有一些细节,我还是比较困惑的。
比如这个MarkOop的定义

1
2
class markOopDesc: public oopDesc {
}

它是继承于oopDesc的
但是在oopDesc的定义中

1
2
3
4
5
6
7
class oopDesc {
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}

又用到了markOop

好吧,这个是一件比较奇怪的是。

上面R大提到,继承与oopDesc的都是被GC管理的,
但是这里有个奇怪的点

MarkOop存在于OopDesc中,讲道理应该是个对象才是,但是它的作用却只是作为对象头。
在Java层面没有与之对应的东西。
更没有道理要被GC管理着啊

我翻阅文档的注释
发现了这么一句话

1
2
3
4
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//

既然官方解释是历史原因,那就不追究这个问题了。

JIT热点探测

这个算是一个小发现
我们查看oop的体系,发现Method也有对应的oop
在method的oop中,有个变量叫MethodCounters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MethodCounters : public Metadata {
friend class VMStructs;
friend class JVMCIVMStructs;
private:
#if INCLUDE_AOT
Method* _method; // Back link to Method
#endif
#if defined(COMPILER2) || INCLUDE_JVMCI
int _interpreter_invocation_count; // Count of times invoked (reused as prev_event_count in tiered)
u2 _interpreter_throwout_count; // Count of times method was exited via exception while interpreting
#endif
#if INCLUDE_JVMTI
u2 _number_of_breakpoints; // fullspeed debugging support
#endif
InvocationCounter _invocation_counter; // Incremented before each activation of the method - used to trigger frequency-based optimizations
InvocationCounter _backedge_counter; // Incremented before each backedge taken - used to trigger frequencey-based optimizations

这个类维护了几个关于方法调用次数的计数器,和JIT的热点探测有关

具体的细节可以查看
https://www.jianshu.com/p/1ea9b3d1abb9

http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2011-June/005750.html

HSDB

这个工具,可以用来查看运行时的oop和klass数据
简单的使用,网上可以随便百度到,这里介绍下怎么查看虚表
https://cloud.tencent.com/developer/article/1180981

参考文档