关于Object=new Object();的美团六问

问题的解说都是说的理论,真要面试的时候描述大概内容就可以了。

1:请说明一下对象的创建过程

     在我们new一个对象的时候,JVM遇到new指令会首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经加载,解析,和初始化过。如果没有,那必须先执行相应的类加载过程。

    如果上面的过程已经完成,那么就可以分配内存了,首先会尝试在(1)栈上分配内存,(可能会问,内存不是分配在堆上吗?试想下,虚拟机栈上的栈帧随着方法调用而创建随着方法的调用结束而出栈,如果对象创建在栈上就不用垃圾收集器去回收对象的这块内存了,这也是虚拟机提高性能的一种优化。栈上分配也是有前提的,并不是所有的对象都可以栈上分配,首先需要进行逃逸分析的,逃逸分析是指判断对象的作用域是否有可能逃逸出函数体,Java SE 6u23版本之后,HotSpot中默认就开启了逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果)如果成功就分配成功。如果分配不成功就会在堆上进行分配,如果对象很大(这个大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少内存空间时就提前触发垃圾收集以获取足够的连续空间来安置它们。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置的对象直接在老年代分配,这样做的目的时避免Eden区以及两个Survivor区之间发生大量的内存复制,因为新生代会采用复制算法收集内存)则直接进入(2)老年代。如果对象不大则优先考虑在(3)TLAB(全称是Thread Local Allocation Buffer,即线程本地分配缓存区)上分配。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。其介绍可参考:TLAB   下图为对象分配流程,下红框为堆。

 

2:加DCL还需要volatile问题?

首先我们需要知道volatile有两个作用:1:被它修饰变量,线程之间具有可见性。2:被它修饰过的变量可以防止指令重排

在单例中使用DCL加volatile。

public class Singleton {
    private volatile static Singleton instance;

    private Singleton(){}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                 /* 如果不加volatile这个new的动作底层可能会发生
                  * 指令重排。
                  **/
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

了解加volatile之前,我们先来看一下我们new操作JVM会帮我门做什么事。

1)我们在一个java文件中创建一个对象:

public class Main{
	public static void main(String[] args){
		Object o=new Object();
	}
}

编译此文件:javac Main.java    生成Main.class

反编译classs生成jvm指令, javap -c Main.class  生成如下内容:

Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
        //  创建对象开始,分配内存
       0: new           #2                  // class java/lang/Object
       3: dup
       //  对象创建完对其进行初始化
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
        //  局部变量 o引用此对象     
        7: astore_1
       8: return
}

   如果不加volatile,  cpu执行的时候可能就会发生指令重排,把对象初始化和局部变量o赋值调换位置。

//指令重排了
//如果一个线程先执行到这个命令后,对象instance(对应单例中的对象) 已经不为空,  此时cpu调度到另外
//  一个线程,这个线程判断instance不为空,就直接拿来使用,但是这个时候对象还没初始化,就会产生问题
7: astore_1
4: invokespecial #1                  // Method java/lang/Object."<init>":()V

 

加上volatile防止指令重排就可以避免这个问题。

3:对象在内存中的存储布局

<<深入理解java虚拟机>>一书中有专门的对象的内存布局小节来讲这块内容。

       在我们常用的HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),

实例数据(Instance Data)和对齐填充。

         HotSpot虚拟机的对象头包括两部分内容,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有锁,偏向线程id,偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启指针压缩)中分别为32bit,64bit,官方称为“ Mark Word”。对象需要存储的运行时数据很多,其实已经超过32位,64位BitMap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit中25bit用于存储对象哈希码值,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0。然而在64位机中存储位数见下图:

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定对象是那个类的实例。并不是所有的虚拟机实现都在对象数据上保留类型指针,换句话说,对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个数组,在对象头中还必须包含一块用于数组长度的数据,因为虚拟机可以通过普通对象的元数据信息确定java对象的大小,但是从数组的元数据中却无法确定数组的大小。

接下来的实例数据部分是对象真正存储有效信息也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机默认的分配策略参数(FieldsAllcationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Pointers),从分配策略看,相同宽度的字段总是被分配到一起。在满足这个前提的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数为true(默认为true),那么子类中的较窄的变量也可能插入到父类变量的空隙之中。

   第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍(1/2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

总结一下的图:

4:对象头的具体信息

对象头的信息见上问的内容。

5:对象怎么定位的

这个问题<<深入理解java虚拟机>>中也有提到。

  java程序通过栈上的reference数据(局部变量引用数据类型)来操作堆上的具体对象。由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。

       如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

       书中描绘的信息如下:

如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,见下图:

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的时稳定的句柄地址,在对象被移动(垃圾收集时移动对象时非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

  使用直接指针访问的最大好处就是速度快。它节省了一次指针定位的时间开销。在我们经常使用的HotSpot中使用第二种方式,但是在整个软件系统中句柄池的方式也很常见。

6:对象怎么分配的

也可以参考第一个问题。

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姑苏冷

您的打赏是对原创文章最大的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值