从new Class()入手浅看JVM的oop-klass模型原创
首先讲下Java中一个类创建实例如ClassX x = new ClassX()时在整个JVM(Hotspot)中的大致流程:
初始JVM会加载ClassX这个类到perm区,也叫方法区(以前在Hotspot存放于永久代,后来被移到Metaspace),解析完字节码文件以后JVM会创建一个与ClassX类相对应的类模板对象,这个所谓的类模板对象其实就是个C++类的实例(hotspot中就是klassOop的实例对象instanceKlass)。
即类加载的结果便是在jvm的方法区中创建了一个与java类对等的instanceKlass,instanceKlass中保存了ClassX这个Java类所定义的所有信息如:变量、方法、接口、构造函数、属性等等。
此时要注意,jvm在创造instanceKlass后,还会创造一个mirror镜像:这个镜像就是传说中的java.lang.Class,但是需要注意的是这个所谓的镜像类也就是java_lang_Class其实也是一个instanceKlass实例对象,是之前instanceKlass实例对象的镜像,instanceKlass是给jvm内部访问用的而java.lang.Class是暴露给java程序用的。
PS1:在openJDK8中instanceKlassHandle ClassFileParser::parseClassFile里面调用的了一个java_lang_Class::create_mirror,来创建了一个mirror镜像InstanceMirrorKlass,而class InstanceMirrorKlass是继承于 public InstanceKlass的 ,(甚至jdk6的create_mirror直接创建的就是InstanceKlass,在JDK 6及之前的HotSpot VM里,静态字段依附在InstanceKlass对象的末尾;而在JDK 7开始的HotSpot VM里,静态字段依附在java.lang.Class对象的末尾,所以7之后包装成了InstanceMirrorKlass,其实本质还是差不多的,详情请见永远的神R大的回答)。
PS2:在JDK7或之前的HotSpot里,InstanceKlass是被包装在由GC管理的klassOopDesc对象中,存放在GC堆中的所谓Permanent Generation(简称PermGen)中。从JDK 8开始的HotSpot VM则完全移除了PermGen,改为在native memory里存放这些元数据。新的用于存放元数据的内存空间叫做Metaspace,InstanceKlass对象就存在这里。至于java.lang.Class对象(mirror),它们从来都是“普通”Java对象,跟其它Java对象一样存在普通的Java堆(GC堆的一部分)里。
在加载处理完ClassX类的一系列逻辑之后,Hotspot会在堆中为其实例对象x开辟一块内存空间存放实例数据,即JVM在实例化ClassX时,又会创建一个instanceOop,这个instanceOop就是ClassX对象实例x在内存中的对等体,用来存储实例对象的成员变量。
让我们来看下instanceOop.hpp(位于OpenJDK\hotspot\src\share\vm\oops\目录下),发现instanceOop是继承自oopDesc的(class instanceOopDesc : public oopDesc),继续看下Oop的结构就会发现:
class oopDesc {
//友元类
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
//用于内存屏障
static BarrierSet* _bs;
........
}
_mark和_metadata加起来就是我们传说中的对象头了,其中markOop的变量_mark用来标识GC分代信息、线程状态、并发锁等信息。而_metadata的联合结构体则用来标识元数据(指向元数据的指针),什么是元数据呢,概念一点就是这个类包含的父类、方法、变量、接口等各种信息,通俗一点来说就是我们上文中所提到的instanceKlass。仔细看_metadata包含两个元素,根据名我们也可以轻易地看出来_klass是个正常的指针/或者说宽指针(OpenJDK7以前叫做wideKlassOop),而_compressed_klass则是被压缩过得指针。
OpenJDK8的oopDesc跟OpenJDK7里的是略有不同。不过主要的不同并不是在mark word上,而是在metadata部分:以前如klassOopDesc、methodOopDesc等元数据,虽然并非Java对象,但也由GC直接管理;到OpenJDK 8时,这些metadata都被移到GC掌管之外的native memory,于是oopDesc这个对象头里metadata的指针就不是一个oop,而是native直接指针(或者压缩的native直接指针了)。具体不做解释了,感兴趣请出门左拐yyds R大的回答。
由于是union类型,所以_metadata还是占用了8字节的空间,但是JVM会根据计算把合适的field字段放入闲置的4字节空间,当然如果不合适就保持一个4字节的padding填充来保持对齐(如果是数组对象则会用来存放描述长度的length,如typeArrayOop或objArrayOop)。关于内存对齐和字段重排,又是个可以写的话题,不如有时间单独写一篇文章(一般这么说就是没时间,摔)。
至此,JVM已经为ClassX类创建了对应的oop-klass模型,oop对应instanceOop对应类实例,klass对应instanceKlass对应java类。整个三者的关系大致是这样(偷得R大的图):
Java object InstanceKlass Java mirror
[ _mark ] (java.lang.Class instance)
[ _klass ] --> [ ... ] <-\
[ fields ] [ _java_mirror ] --+> [ _mark ]
[ ... ] | [ _klass ]
| [ fields ]
\ [ klass ]
(在堆中) (在prem区) (在堆中)
我们来创建一个ClassX:
public class ClassX {
boolean b;
Object o1;
int i;
long l;
Object o2;
float f;
}
这个ClassX的实例的内存布局,我们可以使用JOL(Java Object Layout)来进行查看,使用方法及简介请移步。
64位下开启指针压缩:
Running 64-bit HotSpot VM.
Using compressed oop with 3-bit shift.
Using compressed klass with 3-bit shift.
Objects are 8 bytes aligned.
Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
ClassX object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 int ClassX.i 0
16 8 long ClassX.l 0
24 4 float ClassX.f 0.0
28 1 boolean ClassX.b false
29 3 (alignment/padding gap)
32 4 java.lang.Object ClassX.o1 null
36 4 java.lang.Object ClassX.o2 null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
ClassX@2ef1e4fad object externals:
ADDRESS SIZE TYPE PATH VALUE
76b5e9a98 40 ClassX (object)
64位下不开启指针压缩:
Running 64-bit HotSpot VM.
Objects are 8 bytes aligned.
Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
ClassX object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 90 35 20 1c (10010000 00110101 00100000 00011100) (471872912)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 8 long ClassX.l 0
24 4 int ClassX.i 0
28 4 float ClassX.f 0.0
32 1 boolean ClassX.b false
33 7 (alignment/padding gap)
40 8 java.lang.Object ClassX.o1 null
48 8 java.lang.Object ClassX.o2 null
Instance size: 56 bytes
Space losses: 7 bytes internal + 0 bytes external = 7 bytes total
ClassX@2ef1e4fad object externals:
ADDRESS SIZE TYPE PATH VALUE
129ed5378 56 ClassX (object)
我们可以清楚地看到,在未开启指针压缩时:
+0: [ _mark (8 bytes) ]
+8: [ _klass (8 bytes) ]
+16: [ field : long ClassX.l (8 bytes) ]
…
+40: [ field : java.lang.Object ClassX.o1 (8 bytes) ]
+48: [ field : java.lang.Object ClassX.o2 (8 bytes) ]
在开启指针压缩后:
+0: [ _mark (8 bytes) ]
+8: [ _narrow_klass (4 bytes) ]
+12: [ padding or first field (4 bytes) ] (此时为int ClassX.i 4bytes)
+16: [ field : long ClassX.l (8 bytes) ]
…
+32: [ field : java.lang.Object ClassX.o1 (4 bytes) ]
+36: [ field : java.lang.Object ClassX.o2 (4 bytes) ]
ps:关于指针压缩与8 bytes aligned的渊源,我们留到以后再嗦(
又挖坑写完了,右转《字节对齐与Java的指针压缩》)。
当然整个oop-klass体系是很庞大的,不仅类是对象,方法是对象,字节码常量池也是对象,如果我们查看描述Hotspot中类型信息的oppsHierarchy.hpp就会发现oop分为很多:
typedef class oopDesc* oop;
typedef class instanceOopDesc* instanceOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;
......
同理klass也有很多:
class Klass;
class InstanceKlass;
class InstanceMirrorKlass;
class InstanceClassLoaderKlass;
class InstanceRefKlass;
class ArrayKlass;
class ObjArrayKlass;
class TypeArrayKlass;
......
JVM还是很博大精深的,这里只是浅谈一下oop-klass模型,毕竟源码…我自己也没看完哈哈哈啊。