我的面试题(一):关于Object o = new Object()的追魂8连问!

一、概述

    大厂面试一般喜欢针对一个简单的东西进行不断的深入追问,那么接下来看一下Object o = new Object()中会涉及多少知识点呢?

二、题目以及答案分析

    1. 解释一下对象的创建过程。(半初始化)

我的面试题(一):关于Object o = new Object()的追魂8连问!

    汇编码解释:

        (1) new #2 <T>:在内存中开辟一块空间并生成所有的成员变量,给t这个对象专用,此时成员变量m也有了,只是值是初始默认值0(long类型也是0;boolean类型是false;引用类型就是null)。

        (2) invokespecial #3 <T.<init>>:调用T类的构造方法,并给m赋值为8。

        (3) astore_1:将t与开辟的内存空间建立关联。

    如果当发生指令重排后,赋值指令与建立关联的指令交换顺序,使得后面来的线程在做判断时,发现对象已经创建,但是却不知道这个对象只是一个半初始状态,就直接拿来用,也就是拿到的对象m值并不是8而是默认值0,所以就下面这个问题。

    2. DCL单例是否需要加volatile?(指令重排)

        双重检查的单例模式中加volatile是为了防止指令重排,与线程间可见的作用没有关系。如果不加volatile修饰实例对象,有可能以为指令重排序导致读取到一个半初始化状态的对象,也就是还没有完全赋值的对象,从而引发系统的业务逻辑等错误。

    3. 对象在内存中的存储布局。(普通对象与数组的存储结构区别)

我的面试题(一):关于Object o = new Object()的追魂8连问!

        mack word:存放琐标志位、偏向锁位、线程ID、分代年龄等内容,下面详细解读。

        Klass word:也就是calss pointer,存储对象所属类的地址,就是为了标记到底是什么类的实例。jvm默认开启了指针压缩,所以占用4个字节,如果关闭指针压缩,就占用8个字节。此外,指针压缩还会影响instance data的实例对象的指针空间占用大小。如果开启了指针压缩,Long型的成员变量和long型的成员变量占用空间大小是有区别的:Long占用4个字节;long是基础类型占用8个字节。如果关闭了指针压缩:Long占用8个字节;long是基础类型占用8个字节。

        instance data:保存成员变量实例对象。

        padding:是为了保证整个内容加起来能被8个字节(Byte)整除而填充的空间,JVM读数据是一块一块读的,这样做效率是最高的。

        length:如果对象是数组,需要存储数组长度。

        详细解释请看我的往期文章《我的并发编程(二):java对象头以及synchronized升级过程》,里面描述得非常清楚。

    4. 对象头的组成(markword等)

我的面试题(一):关于Object o = new Object()的追魂8连问!

     详细解释:

        (1) 当我们创建一个无锁态对象的时候:25位没有用;31位装的identity Hashcode,但是只有在被调用的时候,才填充,没有调用的时候是空的;1位没有使用的;4位分代年龄(解释在下面);1位偏向锁位;2位锁标志位。

        (2) 偏向锁的时候:54位存下当前线程的ID;2位存批量撤销Epoch;1位没有使用;4位分代年龄;1位偏向锁位;2位锁标志位。

        (3) 自旋锁:62位指向线程中的Lock Record的指针。Lock Record与锁重入有关,synchronize默认是可重入的。自旋锁在竞争锁的时候,会在自己的内存的线程栈中创建一个Lock Record对象,抢到锁对象的资源时,锁对象头存的就是这个线程的Lock Record对象的指针,所以在重入的时候,会再创建一个Lock Record对象,利用Lock Record来记录到底琐了多少次。解锁的时候,就将一个Lock Record移除,移除的方式是FILO,也就是先进后出的原则。

        (4) 重量级琐:重量级琐是在C++代码层面进行的,会生成一个ObjectMonitor对象,这个对象中记录了一系列的队列,如下图:

        

我的面试题(一):关于Object o = new Object()的追魂8连问!

        (5) 分代年龄:JVM有10种垃圾回收器,前面7种都涉及分代年龄,采用分代算法。当我们创建一个对象的时候,把它放在年轻代中,每经过一次垃圾回收后年龄就+1,也就是垃圾回收无法回收掉这个对象,它的年龄就会不断的增长,到达15,因为4个字节最大为15,就转到老龄代,年轻代的回收就不再对它进行回收。

        (6) hashCode部分:对象头上的hashCode并不是我们调用重写的hashCode()方法生成的,而是为重写的hashCode()方法或者调用System.identityHashcode()方法才能获取并且存入对象头中。通俗来讲,这里的hashCode是按照原始内容计算的,重写过的hashCode()方法计算的结果并不会存在此处。如果对象没有重写hashCode()方法,那么默认调用的os::random产生hashCode,也可以通过System.identityHashcode()获取。os::random产生hashCode的规则是:next_rand = (16807seed)mod(2*31-1),因此可以使用31位存储空间进行存储,并且一旦产生这个hashCode,JVM就会记录在markword中。

    详细解释请看我的往期文章《我的并发编程(二):java对象头以及synchronized升级过程》,里面描述得非常清楚。

    5. 对象如何定位

    JVM中对象访问定位两种方式:

        (1) 直接指针访问:Java栈直接与对象进行访问,在Java堆中对象帆布中必须考虑存储访问类型的数据的相关信息 ,如下图:

我的面试题(一):关于Object o = new Object()的追魂8连问!

        直接指针访问的优点比较明显,就是访问速度快,不需要和句柄一样指针定位的开销 。缺点也比较明显,就是对象在GC过程中,在新生代区域复制移动时,会比较麻烦。

        (2) 通过句柄方式访问:在Java堆中分出一块内存进行存储句柄池,在栈中存储的是句柄的地址,如下图:

我的面试题(一):关于Object o = new Object()的追魂8连问!

        通过句柄访问有独特的优点,就是当对象移动的时候(垃圾回收的时候移动很普遍),这样值需要改变句柄中的指针,但是栈中的指针不需要变化,因为栈中存储的是句柄的地址。那么对应的缺点就是需要两次指针转换进行访问,访问速度比直接指针访问稍慢一些。

    6. 对象怎么分配

    堆内存逻辑分区图如下:

我的面试题(一):关于Object o = new Object()的追魂8连问!

    对象分配过程如下图:

    我的面试题(一):关于Object o = new Object()的追魂8连问!

    过程分析:

        (1) 当我们new出一个对象,JVM会首先尝试往栈上分配,如果能够分配得下,就分配到栈上分配到栈上的对象有好处就是不需要GC进行管理,什么时候不需要用到此对象了,将对象出栈就可以了。但是分配到栈上的对象是有要求的:第一,对象比较小,因为栈空间本来就不够大;第二,对象比较简答。

        (2) 如果栈上分配不下,我们就判断这个对象是不是够大,如果足够大就直接放在老年代区,在老年代区的对象经过一次全量垃圾回收FGC后,才有可能被回收掉。

        (3) 如果如果栈上分配不下并且对象不大,就会判断对象能否被存在线程本地分配缓冲区-TLAB(Thread Local Allocation Buffer)。但是不管放不放得下,都是放在新生代区的伊甸区eden。 但是因为堆是共享的,多个线程可以同时创建对象就可能会争夺同一块内存区域,所以为了保证线程安全,Eden区又被分配成一个个线程本地分配缓冲区,这个TLAB是线程私有的,每个线程都有自己的TLAB,避免了多线程环境下使用同步技术带来的性能损耗。

        (4) 伊甸区eden的对象在经过一次GC后,如果被回收掉了,那就结束了生命周期。

        (5) 伊甸区eden的对象在经过一次GC后,如果没有被回收掉,会被拷贝到幸存者区survivor1,对比上面的堆内存逻辑分区图。幸存者区survivor1中的对象再经过一次GC后如果对象还存活,那么就拷贝到幸存者区survivor2并且清理掉幸存者区survivor1中的所有对象,再有GC就反复这个操作,直到对象的分代年龄达到了移到老年代的界限(一般默认是15),就会被移到老年代中。

    7. Object o = new Object()在内存中占用多少字节?

    这里考察的知识点是第3点对象在内存中的存储布局结构和类指针以及普通对象指针的概念。存储布局不再多说,类指针就是存储布局中的class pointer,普通对象指针就是存储布局中的instance data中,成员变量如果不是基础类型而是引用类型,那么也会有普通对象指针指向所属类。默认情况下JVM是开启了类指针和普通对象指针的指针压缩,将8个字节压缩成了4个字节。我们用代码输出来观察对象的大小,实验代码如下:

    我的面试题(一):关于Object o = new Object()的追魂8连问!

    (1) 默认开启所有指针压缩的情况下输出如下:

我的面试题(一):关于Object o = new Object()的追魂8连问!

    (2) 关闭类指针压缩后,如下:

我的面试题(一):关于Object o = new Object()的追魂8连问!

    结论:针对这道题目,如果不考虑o这个对象引用本身,那就需要说清楚默认情况以及关闭类指针压缩后存储布局结构中的变化。

    8. Class对象是在堆还是在方法区?

    方法区,详见低5点对象如何定位的配图。

三、总结

    通过本文我们能对java对象创建过程、java内存以及并发的部分知识有了更加深刻的理解。

    更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!

我的面试题(一):关于Object o = new Object()的追魂8连问!