java对象模型,一段代码引发的思考

大家好,笔者最近在看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),分别是对象头,实例数据,对象填充。

  1. 对象头记录的数据与这个对象实例本身并没有太大的关联,可以理解成记录的是这个对象的标记信息,分为两个部分,class-pointer和mark-word。class-pointer是一个类型指针,指向对象所属的类(这个说法见仁见智吧,看虚拟机实现,其实并不一定要通过对象来访问这个类的元数据的,目前有访问句柄和直接访问两种方式)。mark-word记录的是对象的运行时数据,方便虚拟机的管理,比如对象分代年龄,偏向线程id,偏向时间戳等。
  2. 实例数据不多赘述。
  3. 对象填充是因为对象字段之间占的空间大小是有差别的,所以需要额外的空间来填充一下。

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提供两个功能:

  1. 实现语言层面的Java类(在Klass基类中已经实现)
  2. 实现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获取对象的字段地址进行操作,一点毛病没有。

参考文章:

  1. https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html Unsafe
  2. https://www.cnblogs.com/qingshanli/p/9250491.html#_label1 java对象模型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值