java直接引用_Java虚拟机 - 符号引用和直接引用理解

我了解了调用函数时符号引用如何转换为直接引用的,但是对于类变量,实例变量的解析方法还是不太清楚。

符号引用是只包含语义信息,不涉及具体实现的;而解析(resolve)过后的直接引用则是与具体实现息息相关的。所以当谈及某个符号引用被resolve成怎样的直接引用时,必须要结合某个具体实现来讨论才行。

查阅资料后很多人说了一个偏移量的问题,那这个偏移量是相对于什么的偏移量呢?

“相对于什么的偏移量”这就正好是上面说的“实现细节”的一部分了。

例如说,HotSpot VM采用的对象模型,在JDK 6 / 7之间就发生过一次变化。

在对象实例方面,HotSpot VM所采用的对象模型是比较直观的一种:Java引用通过直接指针(direct pointer)或语义上是直接指针的压缩指针(compressed pointer)来实现;指针指向的是对象的真实起始位置(没有在负偏移量上放任何数据)。

对象内的布局是:最前面是对象头,有两个VM内部字段:_mark 和 _klass。后面紧跟着就是对象的所有实例字段,紧凑排布,继承深度越浅的类所声明的字段越靠前,继承深度越深的类所声明的字段越靠后。在同一个类中声明的字段按字段的类型宽度来重排序,对普通Java类默认的排序是:long/double - 8字节、int/float - 4字节、short/char - 2字节、byte/boolean - 1字节,最后是引用类型字段(4或8字节)。每个字段按照其宽度来对齐;最终对象默认再做一次8字节对齐。在类继承的边界上如果有因对齐而带来的空隙的话,可以把子类的字段拉到空隙里。这种排布方式可以让原始类型字段最大限度地紧凑排布在一起,减少字段间因为对齐而带来的空隙;同时又让引用类型字段尽可能排布在一起,减少OopMap的开销。

class A {

boolean b;

Object o1;

}

class B extends A {

int i;

long l;

Object o2;

float f;

}

class C extends B {

boolean b;

}

它的实例对象布局就是:(假定是64位HotSpot VM,开启了压缩指针的话)

--> +0 [ _mark ] (64-bit header word)

+8 [ _klass ] (32-bit header word, compressed klass pointer)

+12 [ A.b ] (boolean, 1 byte)

+13 [ (padding) ] (padding for alignment, 3 bytes)

+16 [ A.o1 ] (reference, compressed pointer, 4 bytes)

+20 [ B.i ] (int, 4 bytes)

+24 [ B.l ] (long, 8 bytes)

+32 [ B.f ] (float, 4 bytes)

+36 [ B.o2 ] (reference, compressed pointer, 4 bytes)

+40 [ C.b ] (boolean, 1 byte)

+41 [ (padding) ] (padding for object alignment, 7 bytes)

所以C类的对象实例大小,在这个设定下是48字节,其中有10字节是为对齐而浪费掉的padding,12字节是对象头,剩下的26字节是用户自己代码声明的实例字段。

留意到C类里字段的排布是按照这个顺序的:对象头 - Object声明的字段(无) - A声明的字段 - B声明的字段 - C声明的字段——按继承深度从浅到深排布。而每个类里面的字段排布顺序则按前面说的规则,按宽度来重排序。同时,如果类继承边界上有空隙(例如这里A和B之间其实本来会有一个4字节的空隙,但B里正好声明了一些不宽于4字节的字段,就可以把第一个不宽于4字节的字段拉到该空隙里,也就是 B.i 的位置)。

同时也请留意到A类和C类都声明了名字为b的字段。它们之间有什么关系?——没关系。

Java里,字段是不参与多态的。派生类如果声明了跟基类同名的字段,则两个字段在最终的实例中都会存在;派生类的版本只会在名字上遮盖(shadow / hide)掉基类字段的名字,而不会与基类字段合并或令其消失。上面例子特意演示了一下A.b 与 C.b 同时存在的这个情况。

使用JOL工具可以方便地看到同样的信息:

$ sudo ~/sdk/jdk1.8.0/Contents/Home/bin/java -Xbootclasspath/a:. -jar ~/Downloads/jol-cli-0.5-full.jar internals C

objc[78030]: Class JavaLaunchHelper is implemented in both /Users/krismo/sdk/jdk1.8.0/Contents/Home/bin/java and /Users/krismo/sdk/jdk1.8.0/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.

# 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]

C object internals:

OFFSET SIZE TYPE DESCRIPTION VALUE

0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)

4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)

8 4 (object header) be 3b 01 f8 (10111110 00111011 00000001 11111000) (-134136898)

12 1 boolean A.b false

13 3 (alignment/padding gap) N/A

16 4 Object A.o1 null

20 4 int B.i 0

24 8 long B.l 0

32 4 float B.f 0.0

36 4 Object B.o2 null

40 1 boolean C.b false

41 7 (loss due to the next object alignment)

Instance size: 48 bytes

Space losses: 3 bytes internal + 7 bytes external = 10 bytes total

所以,对一个这样的对象模型,实例字段的“偏移量”是从对象起始位置开始算的。对于这样的字节码:

getfield cp#12 // C.b:Z

(这里用cp#12来表示常量池的第12项的意思)

这个C.b:Z的符号引用,最终就会被解析(resolve)为+40这样的偏移量,外加一些VM自己用的元数据。

这个偏移量加上额外元数据比原本的constant pool index要宽,没办法放在原本的constant pool里,所以HotSpot VM有另外一个叫做constant pool cache的东西来存放它们。

在HotSpot VM里,上面的字节码经过解析后,就会变成:

fast_bgetfield cpc#5 // (offset: +40, type: boolean, ...)

(这里用cpc#5来表示constant pool cache的第5项的意思)

于是解析后偏移量信息就记录在了constant pool cache里,getfield根据解析出来的constant pool cache entry里记录的类型信息被改写为对应类型的版本的字节码fast_bgetfield来避免以后每次都去解析一次,然后fast_bgetfield就可以根据偏移量信息以正确的类型来访问字段了。

然后说说静态变量(或者有人喜欢叫“类变量”)的情况。

从JDK 1.3到JDK 6的HotSpot VM,静态变量保存在类的元数据(InstanceKlass)的末尾。而从JDK 7开始的HotSpot VM,静态变量则是保存在类的Java镜像(java.lang.Class实例)的末尾。

在HotSpot VM中,对象、类的元数据(InstanceKlass)、类的Java镜像,三者之间的关系是这样的:

Java object InstanceKlass Java mirror

[ _mark ] (java.lang.Class instance)

[ _klass ] --> [ ... ]

[ fields ] [ _java_mirror ] --+> [ _mark ]

[ ... ] | [ _klass ]

| [ fields ]

\ [ klass ]

每个Java对象的对象头里,_klass字段会指向一个VM内部用来记录类的元数据用的InstanceKlass对象;InsanceKlass里有个_java_mirror字段,指向该类所对应的Java镜像——java.lang.Class实例。HotSpot VM会给Class对象注入一个隐藏字段“klass”,用于指回到其对应的InstanceKlass对象。这样,klass与mirror之间就有双向引用,可以来回导航。

这个模型里,java.lang.Class实例并不负责记录真正的类元数据,而只是对VM内部的InstanceKlass对象的一个包装供Java的反射访问用。

在JDK 6及之前的HotSpot VM里,静态字段依附在InstanceKlass对象的末尾;而在JDK 7开始的HotSpot VM里,静态字段依附在java.lang.Class对象的末尾。

假如有这样的A类:

class A {

static int value = 1;

}

那么在JDK 6或之前的HotSpot VM里:

Java object InstanceKlass Java mirror

[ _mark ] (java.lang.Class instance)

[ _klass ] --> [ ... ]

[ fields ] [ _java_mirror ] --+> [ _mark ]

[ ... ] | [ _klass ]

[ A.value ] | [ fields ]

\ [ klass ]

可以看到这个A.value静态字段就在InstanceKlass对象的末尾存着了。

这个情况我在前面提到的演讲稿的第121页有画过一张更好看的图。

而在JDK 7或之后的HotSpot VM里:

Java object InstanceKlass Java mirror

[ _mark ] (java.lang.Class instance)

[ _klass ] --> [ ... ]

[ fields ] [ _java_mirror ] --+> [ _mark ]

[ ... ] | [ _klass ]

| [ fields ]

\ [ klass ]

[ A.value ]

可以看到这个A.value静态字段就在java.lang.Class对象的末尾存着了。

所以对于HotSpot VM的对象模型,静态字段的“偏移量”就是:

JDK 6或之前:相对该类对应的InstanceKlass(实际上是包装InstanceKlass的klassOopDesc)对象起始位置的偏移量

JDK 7或之后:相对该类对应的java.lang.Class对象起始位置的偏移量。

其它细节跟实例字段相似,就不赘述了。

===========================================

好奇的同学可能会关心一下上面说的HotSpot VM里的InstanceKlass和java.lang.Class实例都是放哪里的呢?

在JDK 7或之前的HotSpot VM里,InstanceKlass是被包装在由GC管理的klassOopDesc对象中,存放在GC堆中的所谓Permanent Generation(简称PermGen)中。

从JDK 8开始的HotSpot VM则完全移除了PermGen,改为在native memory里存放这些元数据。新的用于存放元数据的内存空间叫做Metaspace,InstanceKlass对象就存在这里。

至于java.lang.Class对象,它们从来都是“普通”Java对象,跟其它Java对象一样存在普通的Java堆(GC堆的一部分)里。

===========================================

那么如果不是HotSpot VM,而是别的JVM呢?

——什么可能性都存在。总之“偏移量”什么的全看一个具体的JVM实现的内部各种细节是怎样的。

例如说,一个JVM完全可以把所有类的所有静态字段都放在一个大数组里,每新加载一个类就从这个数组里分配一块空间来放该类的静态字段。那么此时静态字段“偏移量”可能直接就是这个静态字段的地址(假定存放它们的数组不移动的话),或者可能是基于这个数组的起始地址的偏移量。

又例如说,一个JVM在实现对象模型时,可能会让指针不指向对象真正的开头,而是指向对象中间的某个位置。例如说,还是HotSpot VM那样的对象布局,指针可以选择指向很多种地方都合理:(下面还是假定64位HotSpot VM,开压缩指针)

指向对象开头:_mark位于+0,这是HotSpot VM选择的做法;

指向对象头的第二个字段:_klass位于+0,_mark位于-8。这种做法在某些架构上或许可以加快通过_klass做vtable dispatch的速度,所以也有合理性;

指向实际字段的开头:_mark位于-12,_klass位于-4,第一个字段位于+0。这主要就是觉得字段访问可能是更频繁的操作,而潜在可能牺牲一点对象头访问的速度。

Maxine VM的对象模型就可以在OHM模型和HOM模型之间选择。所谓OHM就是Origin-Header-Mixed,也就是指针指向对象头第一个字段的做法;所谓HOM就是Header-Origin-Mixed,也就是指针指向对象头之后(也就是第一个字段)的做法。

还有更有趣的对象布局方式:双向布局(bidirectional layout),例如Sable VM所用的布局。一种典型的方案是把引用类型字段全部放在负偏移上,指针指向对象头,然后原始类型字段全部放在正偏移量上。这样的好处是GC在扫描对象的引用类型字段时只需要扫描一块连续的内存,非常方便。

typedef union java_value_tag {

int32_t int_val;

int64_t long_val;

/* ... */

object_slot* ref_val;

} java_value;

typedef struct object_slot_tag {

java_value val;

struct object_slot_tag* next;

} object_slot;

然后假如一个类有3个字段,那么这个类的实例就有4个这样的object_slot节点组成的链表而构成:对象头 -> 第一个字段 -> 第二个字段 -> 第三个字段 -> NULL。

谁会这么做(掀桌了!

其实还真有。有些有趣的实现,为了简化GC堆的实现,便于减少外部碎片的堆积,而可以把GC堆实现为一个object_slot大数组。这里面由于每个单元的数据都必然一样大,所以可以有效消除外部碎片——代价则是人为的打碎了一个对象的数据的连续性,增加了内部碎片。

当然做这种取舍的实现非常非常少,所以大家没怎么见过也是正常… >_<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值