性能文章>从new Class()入手浅看JVM的oop-klass模型>

从new Class()入手浅看JVM的oop-klass模型原创

https://a.perfma.net/img/2382850
3年前
497558

首先讲下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模型,毕竟源码…我自己也没看完哈哈哈啊。

点赞收藏
分类:标签:
豆大侠

一只菜鸡.

请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

从 Linux 内核角度探秘 JDK MappedByteBuffer

从 Linux 内核角度探秘 JDK MappedByteBuffer

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

8
5