大家好,笔者最近在看juc包下原子类的源码,发现了这么一段代码
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
如大家所见,这是一段静态代码块,这个静态代码块通过unsafe实例获取value字段的偏移地址然后对静态字段valueOffset进行赋值操作。关于unsafe类,在这里就不展开叙述了,他是一个比较特别的类,提供了我们像c++一样对地址进行操作的功能,另外还有对线程的一些操作。读者想了解详情可以看深入理解unsafe这篇由美团技术团队的文章。
实际上这样的代码在java并发包中并不少见,比如FurureTask中的一个静态代码块,他的作用是获取FutureTask中的runner字段的偏移量,代码如下,
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long stateOffset;
private static final long runnerOffset;
private static final long waitersOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = FutureTask.class;
stateOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("state"));
runnerOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("runner"));
waitersOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("waiters"));
} catch (Exception e) {
throw new Error(e);
}
}
实际上他们是通过获取某个字段的偏移量,然后通过对象的基地址+偏移量(baseAddress+offset)就可以计算出某个字段在java内存中的地址,然后直接用指针进行访问,修改等等操作。
这里不禁引发了笔者的思考。java的对象在jvm中究竟是以怎么样的形式存在呢?
这就引发出了我们今天的主题,java对象模型。
我们知道,java是虚拟机语言。我们编写的代码通过javac编译生成字节码然后交给虚拟机帮我们执行。这样做的好处就是可以屏蔽程序运行环境的差异性,可移植性强。
我们的java对象一般存放在堆里面(这里只说一般情况,因为java6之后支持栈上分配),我们的对象是怎么存的呢。
很久之前我听到过这样的一句话,类是对象的模板,类是类的类型。我们今天就好好的理解一下这句话。
java的对象跟c++的对象有所不同。java由oop和klass两个体系构成了java的对象模型。下面我们分别介绍一下。
OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
//定义了oops共同基类
typedef class oopDesc* oop;
//表示一个Java类型实例
typedef class instanceOopDesc* instanceOop;
//表示一个Java方法
typedef class methodOopDesc* methodOop;
//表示一个Java方法中的不变信息
typedef class constMethodOopDesc* constMethodOop;
//记录性能信息的数据结构
typedef class methodDataOopDesc* methodDataOop;
//定义了数组OOPS的抽象基类
typedef class arrayOopDesc* arrayOop;
//表示持有一个OOPS数组
typedef class objArrayOopDesc* objArrayOop;
//表示容纳基本类型的数组
typedef class typeArrayOopDesc* typeArrayOop;
//表示在Class文件中描述的常量池
typedef class constantPoolOopDesc* constantPoolOop;
//常量池告诉缓存
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class klassOopDesc* klassOop;
//表示对象头
typedef class markOopDesc* markOop;
如上面代码所示, oops模块包含多个子模块, 每个子模块对应一个类型, 每一个类型的oop都代表一个在JVM内部使用的特定对象的类型。其中有一个变量oop的类型oopDesc是oops模块的共同基类型。而oopDesc类型又包含instanceOopDesc (类实例)、arrayOopDesc (数组)等子类类型。
java对象一般有三个信息(这里说一般是因为这也要看虚拟机的具体实现,笔者说的是hotspot,网文默认也是hotspot),分别是对象头,实例数据,对象填充。
- 对象头记录的数据与这个对象实例本身并没有太大的关联,可以理解成记录的是这个对象的标记信息,分为两个部分,class-pointer和mark-word。class-pointer是一个类型指针,指向对象所属的类(这个说法见仁见智吧,看虚拟机实现,其实并不一定要通过对象来访问这个类的元数据的,目前有访问句柄和直接访问两种方式)。mark-word记录的是对象的运行时数据,方便虚拟机的管理,比如对象分代年龄,偏向线程id,偏向时间戳等。
- 实例数据不多赘述。
- 对象填充是因为对象字段之间占的空间大小是有差别的,所以需要额外的空间来填充一下。
Klass体系:
//klassOop的一部分,用来描述语言层的类型
class Klass;
//在虚拟机层面描述一个Java类
class instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class instanceRefKlass;
//表示methodOop的Klass
class methodKlass;
//表示constMethodOop的Klass
class constMethodKlass;
//表示methodDataOop的Klass
class methodDataKlass;
//作为klass链的端点,klassKlass的Klass就是它自身
class klassKlass;
//表示instanceKlass的Klass
class instanceKlassKlass;
//表示arrayKlass的Klass
class arrayKlassKlass;
//表示objArrayKlass的Klass
class objArrayKlassKlass;
//表示typeArrayKlass的Klass
class typeArrayKlassKlass;
//表示array类型的抽象基类
class arrayKlass;
//表示objArrayOop的Klass
class objArrayKlass;
//表示typeArrayOop的Klass
class typeArrayKlass;
//表示constantPoolOop的Klass
class constantPoolKlass;
//表示constantPoolCacheOop的Klass
class constantPoolCacheKlass;
和oopDesc是其他oop类型的父类一样,Klass类是其他klass类型的父类。
Klass向JVM提供两个功能:
- 实现语言层面的Java类(在Klass基类中已经实现)
- 实现Java对象的分发功能(由Klass的子类提供虚函数实现)
下面我们主要讲讲instanceKlass
JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个InstanceClass对象,用来在JVM层表示Java类。
InstanceClass内部结构:
//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;
....
在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点, 它的klass就是它自身。
讲到这里,所有的概念都介绍完了,我们可以开始分析了。
我们编写的代码以javac编译之后以class文件的形式存在,系统在运行时会触发类加载将class文件加进应用。在JVM加载java类的时候, JVM会给这个类创建一个instanceKlass并保存在方法区, 用来在JVM层表示该java类。当我们使用new关键字创建一个对象时, JVM会创建一个instanceOopDesc对象, 这个对象包含了对象头和元数据两部分信息。对象头中有一些运行时数据, 其中就包括和多线程有关的锁的信息。而元数据维护的则是指向对象所属的类的InstanceKlass的指针。
如此说来,一个对象的实例数据(注意,是实例数据)的大小实际类文件加载之后就确定好了,用静态方法获取实例字段的偏移,然后每个对象的bassAddress+offsetValue获取对象的字段地址进行操作,一点毛病没有。
参考文章: