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

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

https://a.perfma.net/img/2382850
1年前
320512

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

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

一只菜鸡.

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

为你推荐

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

记一次线上RPC超时故障排查及后续GC调优思路

记一次线上RPC超时故障排查及后续GC调优思路

解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

【全网首发】从源码角度分析一次诡异的类被加载问题

【全网首发】从源码角度分析一次诡异的类被加载问题

2
1