美文网首页JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统
UNSAFE和Java 内存布局(深入理解:锁/反射/线程挂起/

UNSAFE和Java 内存布局(深入理解:锁/反射/线程挂起/

作者: staconfree | 来源:发表于2018-05-31 18:11 被阅读5次

    最近在翻ReentrantLock源码的时候,看到AQS(AbstractQueuedSynchronizer.java)里面有一段代码

        protected final boolean compareAndSetState(int expect, int update) {
            // See below for intrinsics setup to support this
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }
    

    这就是经典的CAS的算法,这里包含两个陌生的东西,unsafe,stateOffset。

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    stateOffset = unsafe.objectFieldOffset
                    (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
    

    又发现stateOffset是跟AQS里面的state字段相关

    private volatile int state;
    

    然后我们又发现state是volatitle类型的,当然这是实现LOCK必备的。

    思考

    这个stateOffset是什么,值是多少,由stateOffset能得到什么?由CAS的算法我们知道需要跟原值进行对比,所以大胆推测通过stateOffset可以得到state字段的值。

    另外还有一个东西很让人好奇,UNSAFE是什么,能做什么?

    粗略认识

    带着这两个问题,查了不少资料,这里我希望尽量能用白话的方式说明一下。
    UNSAFE,顾名思义是不安全的,他的不安全是因为他的权限很大,可以调用操作系统底层直接操作内存空间,所以一般不允许使用。
    可参考:java对象的内存布局(二):利用sun.misc.Unsafe获取类字段的偏移地址和读取字段的值
    我们注意到上面有一个方法

    • stateOffset=unsafe.objectFieldOffset(field) 从方法名上可以这样理解:获取object对象的属性Field的偏移量。

    要理解这个偏移量,需要先了解java的内存模型

    Java内存模型

    Java对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),简单的理解:

    • 对象头,对象是什么?
    • 实例数据,对象里有什么?
    • 对齐填充,不关键,目的是补齐位数达到8的倍数。

    举个简单的例子,如下类:

    class VO {
        public int a = 0;
        public int b = 0;
    }
    

    VO vo=new VO();的时候,Java内存中就开辟了一块地址,包含一个固定长度的对象头(假设是16字节,不同位数机器/对象头是否压缩都会影响对象头长度)+实例数据(4字节的a+4字节的b)+padding。

    这里直接说结论,我们上面说的偏移量就是在这里体现,如上面a属性的偏移量就是16,b属性的偏移量就是20。

    在unsafe类里面,我们发现一个方法unsafe.getInt(object, offset);
    通过unsafe.getInt(vo, 16) 就可以得到vo.a的值。是不是联想到反射了?其实java的反射底层就是用的UNSAFE(具体如何实现,预留到以后研究)。

    进一步思考

    如何知道一个类里面每个属性的偏移量?只根据偏移量,java怎么知道读取到哪里为止是这个属性的值?

    查看属性偏移量,推荐一个工具类jol:http://openjdk.java.net/projects/code-tools/jol/
    用jol可以很方便的查看java的内存布局情况,结合一下代码讲解

            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
            </dependency>
    
    public class VO {
        public int a = 0;
        public long b = 0;
        public String c= "123";
        public Object d= null;
        public int e = 100;
        public static int f= 0;
        public static String g= "";
        public Object h= null;
        public boolean i;
    }
    
        public static void main(String[] args) throws Exception {
            System.out.println(VM.current().details());
            System.out.println(ClassLayout.parseClass(VO.class).toPrintable());
            System.out.println("=================");
            Unsafe unsafe = getUnsafeInstance();
            VO vo = new VO();
            vo.a=2;
            vo.b=3;
            vo.d=new HashMap<>();
            long aoffset = unsafe.objectFieldOffset(VO.class.getDeclaredField("a"));
            System.out.println("aoffset="+aoffset);
            // 获取a的值
            int va = unsafe.getInt(vo, aoffset);
            System.out.println("va="+va);
        }
    
        public static Unsafe getUnsafeInstance() throws Exception {
            // 通过反射获取rt.jar下的Unsafe类
            Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeInstance.setAccessible(true);
            // return (Unsafe) theUnsafeInstance.get(null);是等价的
            return (Unsafe) theUnsafeInstance.get(Unsafe.class);
        }
    
    

    在我本地机器测试结果如下:

    # Running 64-bit HotSpot VM.
    # Using compressed oop with 0-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]
    
    com.ha.net.nsp.product.VO object internals:
     OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
          0    12                    (object header)                           N/A
         12     4                int VO.a                                      N/A
         16     8               long VO.b                                      N/A
         24     4                int VO.e                                      N/A
         28     1            boolean VO.i                                      N/A
         29     3                    (alignment/padding gap)                  
         32     4   java.lang.String VO.c                                      N/A
         36     4   java.lang.Object VO.d                                      N/A
         40     4   java.lang.Object VO.h                                      N/A
         44     4                    (loss due to the next object alignment)
    Instance size: 48 bytes
    Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
    
    =================
    aoffset=12
    va=2
    

    在结果中,我们发现:

    • 1、我本地的虚拟机环境是64位并且开启了compressed压缩,对象都是8字节对齐
    • 2、VO类的内存布局包含12字节的对象头,4字节的int数据,8字节的long数据,其他String和Object是4字节,最后还有4字节的对齐。
    • 3、VO类属性的内存布局跟属性声明的顺序不一致。
    • 4、VO类的static属性不在VO的内存布局中,因为他是属于class类。
    • 5、通过VO类就可以确定一个对象占用的字节数,这个占用空间在编译阶段就已经确定(注:此占用空间并不是对象的真实占用空间,)。
    • 6、如上,通过偏移量12就可以读取到此处存放的值是2。

    引申出新的问题:
    1、这里的对象头为什么是12字节?对象头里都具体包含什么?
    答:正常情况下,对象头在32位系统内占用一个机器码也就是8个字节,64位系统也是占用一个机器码16个字节。但是在我本地环境是开启了reference(指针)压缩,所以只有12个字节。
    2、这里的String和Object为什么都是4字节?
    答:因为String或者Object类型,在内存布局中,都是reference类型,所以他的大小跟是否启动压缩有关。未启动压缩的时候,32位机器的reference类型是4个字节,64位是8个字节,但是如果启动压缩后,64位机器的reference类型就变成4字节。
    3、Java怎么知道应该从偏移量12读取到偏移量16呢,而不是读取到偏移量18或者20?
    答:这里我猜测,虚拟机在编译阶段,就已经保留了一个VO类的偏移量数组,那12后的偏移量就是16,所以Java知道读到16为止。

    更多内存布局问题请参考:
    java对象的内存布局(一):计算java对象占用的内存空间以及java object layout工具的使用
    Java对象内存结构
    JVM内存堆布局图解分析

    对象头包含什么内容

    java中的对象头的解析

    image.png
    • 1、对象头有几位是锁标志位
      可以参考如下文章,对象头跟锁有很重要的关联,并且文章中提到另外一个概念:Monitor,预留到以后研究
      死磕Java并发:深入分析synchronized的实现原理

    • 2、对象头有几位代表分代年龄,与回收算法有关
      CMS标记-清除回收算法,标记阶段的大概过程是从栈中查找所有的reference类型,递归可达的所有堆内对象的对象头都标记为数据可达,清除阶段是对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除。
      在 gc回收的时候,会更新还存活的对象的对象头的分代年龄,同时如果这些对象还有发生位置移动(碎片清理),那么还要重新计算对象头的hash值,以及栈中相应的reference引用的值。

    说到回收算法,再参考下这篇也更能理解对象的创建和回收:
    垃圾回收机制中,引用计数法是如何维护所有对象引用的?

    UNSAFE与线程的关系

    unsafe中有一个park方法,与线程挂起有关,预留到以后研究

    参考资料

    一个Java对象到底占多大内存?

    JVM内存模型及String对象内存分配

    相关文章

      网友评论

        本文标题:UNSAFE和Java 内存布局(深入理解:锁/反射/线程挂起/

        本文链接:https://www.haomeiwen.com/subject/moetsftx.html