前言
看任何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
15public 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
13typedef 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
18class 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
5typedef 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
8class Klass;
class InstanceKlass;
class InstanceMirrorKlass;
class InstanceClassLoaderKlass;
class InstanceRefKlass;
class ArrayKlass;
class ObjArrayKlass;
class TypeArrayKlass;
而少掉的那部分,其实只是换了个名字,叫做Metadata1
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
9class 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
7class oopDesc {
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}
MarkOop
还有一些细节,我还是比较困惑的。
比如这个MarkOop的定义1
2class markOopDesc: public oopDesc {
}
它是继承于oopDesc的
但是在oopDesc的定义中1
2
3
4
5
6
7class 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中,有个变量叫MethodCounters1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class MethodCounters : public Metadata {
friend class VMStructs;
friend class JVMCIVMStructs;
private:
Method* _method; // Back link to Method
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
u2 _number_of_breakpoints; // fullspeed debugging support
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