扒一扒java对象的格式

1 篇文章 0 订阅

下午放假睡觉导致晚上睡不着,闲来无聊读了一下AtomicInteger的部分源码(主要想看一下Java的CAS实现方式),发现其使用了一个sun.miscUnsafe

	public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update); //此处便是CAS操作
    }

compareAndSwapInt的方法声明如下

 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

得知compareAndSwapInt是一个Native方法(由于sun.misc的类看不到源码,这里展示的源码是由IDEA反编译得到的),暂时不再继续往下深挖,回到AtomicInteger
在AtomicInteger类中Unsafe对象的初始化代码如下

	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 static Unsafe getUnsafe2() {
        return Unsafe.getUnsafe();
    }

不出意料的发生了错误

Exception in thread "main" java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at club.xyes.nettest.unsafe.UnsafeTest.getUnsafe2(UnsafeTest.java:30)
	at club.xyes.nettest.unsafe.UnsafeTest.main(UnsafeTest.java:34)

查看Unsafe.getUnsafe源码得知,做了类加载器的检查

	@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    public static boolean isSystemDomainLoader(ClassLoader var0) {
        return var0 == null;
    }

于是乎,反射获取一下

	private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

成功了
接下来研究一下Unsafe方法compareAndSwapIntvalueOffset参数是何物

package club.xyes.nettest.unsafe;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;

public class UnsafeTest {
    private static final Unsafe UNSAFE;
    private volatile int value;
    private int value2;
    private long value3;
    private long value4;

    static {
        UNSAFE = getUnsafe();
    }

    private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("value " + getOffset(UnsafeTest.class, "value"));
        System.out.println("value2 " + getOffset(UnsafeTest.class, "value2"));
        System.out.println("value3 " + getOffset(UnsafeTest.class, "value3"));
        System.out.println("value4 " + getOffset(UnsafeTest.class, "value4"));
        System.out.println("value " + getOffset(AtomicInteger.class, "value"));
    }

    private static long getOffset(Class<?> klass, String name) throws NoSuchFieldException {
        Field field = klass.getDeclaredField(name);
        return UNSAFE.objectFieldOffset(field);
    }
}

得到的结果如下

value 12
value2 32
value3 16
value4 24
value 12

不考虑最后一个输出,把结果按照offset从小到大排序后得到下面结果
ps:
1 我把field的数据类型和所占字节数量也一起标了出来
2 发现offset是杂乱的,所以我猜测编译器把field的顺序重新排了序

4 int value1 12
8 long value3 16
8 long value4 24
4 int value2 32

是不是发现了神奇的规律,其实这个offset就是这个字段在对象结构中的起始地址(其实是偏移量,为方便起见,以下统称地址),那就不难猜测compareAndSwapInt是怎么实现的了。
那么新的问题就来了,为什么第一个字段的起始地址的12呢,对象头部的12个自己二究竟是干嘛用的?

正题:java对象格式

首先想到的便是百度(滑稽),搜索java对象头,得知对象头分为三个部分

  1. MarkWord 4字节
  2. class指针 8字节(64位系统)
  3. 数组长度(仅数组对象存在)
    总长度刚好4+8 = 12字节,得到问题答案

markword的格式

java对象头
原文地址:https://blog.csdn.net/lkforce/article/details/81128115
所以对象hashcode的长度是25位的咯?不清楚,待会儿深扒一下,
关于markword后面慢慢深扒,再回到字段内存的分配方式上看看
为了更直观的看到对象的格式,我使用了jol(java object layout)工具包获取了对象的格式详情
jol地址

public class JolTest {
    public static void main(String[] args) {
        val obj = new JolTest();

        ClassLayout layout = ClassLayout.parseInstance(obj);
        System.out.println(layout);
        System.out.println(layout.toPrintable());
    }
}

得到如下输出

size = 16

club.xyes.nettest.jol.JolTest 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        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

注意输出中的loss due to the next object alignment这行数据,意思是“为了对齐下一个对象造成的内存损失”,也就是说我一个没有声明任何字段的类new出来的对象默认要在对象结尾填充4个字节对齐内存。

0-3132-63
markword类指针
类指针为了对齐而存在的数据

那我猜测一下,一个没有任何字段类的对象和一个存在一个int字段类的对象的长度都是16byte,测试一下

public class ObjectLengthTest {
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new A()).toPrintable());
        System.out.println(ClassLayout.parseInstance(new B()).toPrintable());
    }
}

class A {
    private int v;
}

class B {

}

这理就不贴输出了,测试结果与猜测一致,我是用jol获取的对象长度,当然可以用序列化到ByteArrayOutputSteram中获取二进制数组长度实现。
此时我又有了一个疑问:当我的类中存在多个字段时,其内存结构有以下几种情况

  1. 每个字段独占一行内存(8字节) (感觉不太可能)
  2. 后一个字段直接怼在前一个字段结束的地址
  3. java存在某种不可描述的处理方式
    写个代码测试一下
public class ObjectLengthTest2 {
    private int a;
    private long b;
    private float c;
    private double d;
    private int e;
    private int f;
    private int g;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new ObjectLengthTest2()).toPrintable());
    }
}

输出如下

club.xyes.nettest.jol.ObjectLengthTest2 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 ObjectLengthTest2.a                       0
     16     8     long ObjectLengthTest2.b                       0
     24     8   double ObjectLengthTest2.d                       0.0
     32     4    float ObjectLengthTest2.c                       0.0
     36     4      int ObjectLengthTest2.e                       0
     40     4      int ObjectLengthTest2.f                       0
     44     4      int ObjectLengthTest2.g                       0
Instance size: 48 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

排序结果为

0-3132-63
markwordclass pointer
class pointerfield a int
field b long 64位 占满了
field d double 64位
field c float 32 位field e int 32位
field f int 32 位field g int 32位

所以结果就是后一个字段直接怼在前一个字段结束的地址,好像研究的偏了,我继续去看CAS了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值