难难难难难难难!美团追魂七连问!关于Object o = new Object()创建对象的问题

一,请解释一下对象的创建过程?(半初始化)

第一步,申请空间,设置默认值

第二步,调用构造方法,设置初始值

第三步,建立关联(对象名称和对象地址的引用)

二,加问DCL单例(Double Check Lock)要不要加volatile问题?(指令重排)

引入一个新问题:java代码一定是按照顺序执行的吗?答案是不一定!

例如:

int x=1;

int y=1;

这两行代码,也有可能是先执行第二行,再执行第一行,这就是指令重排序,cpu为了提高执行效率,进行重排序(cpu会判断是否有指令重排的条件,只要能保证最终结果的一致性,就可以指令重排)

例如:

int x=1;

x++;

这两行代码,就不会指令重排序,因为第二行代码对第一行代码进行了依赖,重排序逻辑就乱了

再结合问题一(对象的创建过程)看下面的这段代码,经典的单例模式,如果10000个线程同时执行下面这段代码,会不会有什么问题?

答案揭晓:肯定有问题

看下图,比如thread1第一次进来,判断是否为空,为空,拿到锁之后创建对象,但是创建对象有三步(申请空间,初始化,关联),但是这里发生了指令重排序,申请了空间,并进行了关联,但此刻并未初始化(对象有了,数据为空),此时thread2进来了,判断是否为空,不为空,不为空的话,直接就拿走对象用了,其实这个对象的数据都是未初始化的数据,因此就错了。

Java并发编程:volatile关键字解析

volatile作用:保证线程之间的可见性&&禁止指令重排序

三,对象在内存中的存储布局?(对象与数组的存储不同)

对象在内存中的存储布局

markword占8个字节,王八的屁股:规定,只要是64位的虚拟机,都是8个字节。

类型指针默认压缩,占4个字节,不压缩占8个字节,实例数据有多少就占多少,类型指针指向创建的类,此处指针指向T.class

对齐:如果占用的总内存能被8整除,例如16字节,就不做处理,如果不能被8整除,例如18,则需要补位到8整除的子节数,18 > 24,补充六个子节,那么这个类就占用了内存共24个字节。

四,对象头具体包括什么?(markword klasspointer)

第七个问题解决之后,再来看这个问题

对象头包括markword标记位和类型指针两部分。

markword包含了三大信息(锁信息、hashcode、GC信息)

先来看锁信息

public class study {
    private static class T{
      
    }

    public static void main(String[] args) {
        T t = new T();
        //原始状态
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        //加锁
        synchronized (t){
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
    }
}

再来看hashcode信息

public class study {
    private static class T{

    }

    public static void main(String[] args) {
        T t = new T();
        //原始状态
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        t.hashCode();
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
    }
}

最后GC信息,不太容易能看见,垃圾回收时的三色标记(黑白灰),markword有标记颜色的标记位子。

五,对象怎么定位?(直接 间接)

什么叫对象定位?例如,String s = new String() 创建了一个string对象,放在了堆里面,s存对象的地址引用,定位就是s引用如何找到这个对象。

一共两种方式,直接定位(直接指针)&&间接定位(句柄方式)hotspot默认使用直接定位。

直接指针:直接定位到x字节到y字节的连续字节,找到类,优点:直接访问,缺点:GC需要移动对象的时候稍麻烦,因为字节顺序变了之后,t引用就找不到对象原地址了,还得配合修改地址。

句柄方式:对象小,垃圾回收时不用频繁改动t 缺点:两次访问

六,对象怎么分配?(栈上 - 现成本地 - Eden - Old)

看图说话

  1. 创建一个对象,通过逃逸分析,看是否可以分配在栈上,如果可以分配到栈上,就分配到栈上
    1. 分配到栈上的好处是,方法执行完随着栈帧的弹出,对象也随之消亡,不用GC回收
  2. 如果不可以分配到栈上,看对象是否够大,如果够大,直接放在堆区的老年代(对象大小的标准可以通过jvm参数进行配置),如果不够大,则放在伊甸园区
    1. 放在伊甸园区之前还有一步,就是TLAB,多个工作线程并发工作(把对象放在伊甸园区),存在并发问题,所以每个工作线程都有自己独立的空间,优先把对象存在自己独立的空间中,如果空间不够再从伊甸园中取内存,这样就不存在并发问题了,效率更高了
  3. 伊甸园区进行GC垃圾回收,垃圾对象直接消亡腾出内存,非垃圾对象进入S1存活区,再次GC时,伊甸园区和S1存活区的垃圾对象直接消亡,非垃圾对象全复制到S2对象中,伊甸园区和S1区删除垃圾对象,清空,重新存储新来的对象,如此往复,S1 > S2 && S1 < S2

七,Object o = new Object() 在内存中占用多少子节?

看了第三个问题(对象在内存中的存储布局)之后,再回到这个问题

这里进行测试,创建一个类,然后使用JOL工具对这个类进行打印

使用方式如下

ava openjdk 提供jol 工具,可以查看class的头信息

下载 jol 工具包

https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/

选择一个版本,进去后下载 jol-cli-.-full.jar 一定要下载full 的jar 导入包。

打印:

System.out.println(ClassLayout.parseInstance(byte.class).toPrintable());

public class study {
    private static class T{

    }

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

打印结果

com.ftx.camel.study$T 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) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

加了一个变量之后

public class study {
    private static class T{
        int m;
    }

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

又加了一个字符串变量

public class study {
    private static class T{
        int m;
        String s = "xxxxxxxxxxxxxxx";
    }

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

上面的16字节加上字符串4个字节等于20字节,对齐之后(被8整除进行补位)24字节

有同学想问,字符串的长度不一样长,为何字节一直是4个字节?因为类中只存了字符串的地址,并没有存字符串!

posted @ 2022-03-23 18:24  你樊不樊  阅读(71)  评论(0编辑  收藏  举报