Java教程

难难难难难!对象的创建七连问

本文主要是介绍难难难难难!对象的创建七连问,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

难难难难难!对象的创建七连问

关于 Object o = new Object()

  1. 请解释一下对象的创建过程?(半初始化)
  2. 加问 DCLvolatile 问题?(指令重排)
  3. 对象在内存中的存储布局?(对象与数组的存储不同)
  4. 对象头具体包括什么?(markword classpointer)synchronized锁信息
  5. 对象怎么定位?(直接 间接)
  6. 对象怎么分配?(栈上-线程本地-Eden-Old)
  7. Object o = new Object() 在内存中占用多少字节?

1. 请解释一下对象的创建过程?

# 源码:
class T {
  int m = 8;
}
T t = new T();
复制代码
# 汇编码
0 new #2<T>
3 dup
4 invokespecial #3 <T.<init>>
7 astore_1
8 return
复制代码

针对汇编码做一下解释,相信你自己也能看懂的。
0 new #2<T> 申请内存,也就是说堆里面有了一个新的内存,new 出了个新对象
3 dup 复制过程,因为invokespecial会消耗一个引用,必须复制一份
4 invokespecial #3 <T.<init>> 初始化,调用它的构造方法

从上图动画可以看出,对象的创建过程分为步:

  • 申请内存:执行完0 new #2<T>,堆空间里内存就有了,但是内存有了 m = 0,这也叫做半初始化。这里的 0 指的是当你刚刚 new 出一个对象时它会给里面的成员变量设为它的默认值(int 的默认值就是 0)
  • 设初始值:接下来才执行 4 invokespecial #3 <T.<init>> 它的构造方法,构造方法执行完了之后才会设置它的初始值为8。
  • 建立关联:最后执行 7 astore_1 才会 t 成员变量和真正new对象建立关联。

2. 加问 DCLvolatile 问题?(指令重排)

为了理解什么是 DCL (双检锁/双重校验锁(DCL,即 double-checked-locking)),我们先回顾一下 单例模式(Singleton Pattern)。

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建最佳的对象,同时确保只有单个对象被创建。这个类提供类一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  1. 单例类只能有一个实例。
  2. 单例类必须直接创建直接的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

参考代码1

package com.nuih.DesignPatterns.singleton;

/**
 *  饿汉模式
 *  类加载到内存后,就实例化一个单例。JVM保证线程安全
 *  简单使用,推荐使用
 *  唯一缺点:不管用到与否,类装载时就完成实例化
 *  Class.forName("")
 *     (话说你不用的,你装载它干啥)
 */
public class Mgr01 {
    // 创建 Mgr01 的一个对象
    private static final Mgr01 INSTANCE = new Mgr01();

    //让构造函数为 private,这样该类就不会被实例化
    private Mgr01(){

    }

    // 获取唯一可用的对象
    public static Mgr01 getInstance(){
        return  INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}

复制代码

参考代码1,这种写法有人会说 INSTANCE还没用就直接 new 出来了,假如说创建的过程特别浪费资源,能不能够等我想用的时候再初始化出来。请看参考代码2。

参考代码2

package com.nuih.DesignPatterns.singleton;

import java.util.concurrent.TimeUnit;

/**
 * 虽然达到了按需初始化的目的,但却带来了线程不安全
 */
public class Mgr02 {
    private static Mgr02 INSTANCE;

    private Mgr02() {

    }

    public static Mgr02 getInstance() {
        if (INSTANCE == null) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            INSTANCE = new Mgr02();
        }
        return  INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i< 100; i++) {
            new Thread(() ->
                    System.out.println(Mgr02.getInstance().hashCode())
            ).start();
        }
    }
}
复制代码

还有人接着说,参考代码2,线程不安全,多线程访问情况下有可能会 new 出多个对象出来。自然而然我们想到加锁来解决,请看参考代码3。

参考代码3

package com.nuih.DesignPatterns.singleton;

import java.util.concurrent.TimeUnit;

/**
 * 增加synchronized,线程安全
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {

    }

    public static synchronized Mgr03 getInstance() {
        if (INSTANCE == null) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            INSTANCE = new Mgr03();
        }
        return  INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i< 100; i++) {
            new Thread(() ->
                    System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}
复制代码

可是有的人还会说,你上来二话不说整个方法全上锁,锁的粒度是不是太粗了。于是我们换个写法。请看参考代码4。

参考代码4

package com.nuih.DesignPatterns.singleton;

import java.util.concurrent.TimeUnit;

public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {

    }

    public static Mgr04 getInstance() {
        // 业务代码
        if (INSTANCE == null) {
            // 妄图通过减少同步代码块的方式提高效率,然后不可行
            synchronized (Mgr04.class) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                INSTANCE = new Mgr04();
            }
        }
        return  INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i< 100; i++) {
            new Thread(() ->
                    System.out.println(Mgr04.getInstance().hashCode())
            ).start();
        }
    }
}
复制代码

这个版本在多线程访问情况下,是线程不安全的。于是诞生了 “DCL” 写法。

参考代码5

package com.nuih.DesignPatterns.singleton;

import java.util.concurrent.TimeUnit;

public class Mgr05 {
    private static volatile Mgr05 INSTANCE;

    private Mgr05() {

    }

    public static Mgr05 getInstance() {
        // 业务代码
        if (INSTANCE == null) { // Double Check Lock
            // 双重检查
            synchronized (Mgr05.class) {
                if (INSTANCE == null) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    INSTANCE = new Mgr05();
                }
            }
        }
        return  INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i< 100; i++) {
            new Thread(() ->
                    System.out.println(Mgr05.getInstance().hashCode())
            ).start();
        }
    }
}
复制代码

对此,我们已经掌握了 DCl 的概念了,第二个问题是是否需要加 volatile 关键字。

volatile 主要有两个作用:

  • 线程可见性
  • 禁止指令重排序(上下都要加内存屏障)

那么到底需不需加 volatile 关键字,我们来分析下:

当第一个线程来的时候,判断它为空,开始对它进行初始化(new)。当 new 一半的时候,只拿到了默认值,还没获取初始化值。 这个时候下面两条指令有可能会发生 指令重排序 ,这时候就会先建立关联,再调用构造方法赋予初始值。目前 t 就执行了 半初始化 的这个状态对象 t 指向半初始化状态对象的时候,正好这个时候第二个线程来了,当前 t指向了半初始化状态的对象, 肯定不为空。那就直接用了,那就用半初始化状态的这个对象,就会发生不可预知的错误。

所以:百分之百要加 volatile

3. 对象在内存中的存储布局?

对象与数组的存储不同

作为普通对象来说,当new出一个对象放入内存的时候它由4项构成:

  • markword:锁状态、分代年龄、hashcode等
  • 类型指针(class pointer)
  • 实例数据(instance data)
  • 对齐(padding):如果前面3项加起来字节数不能被8整除,后面补齐。

markword与类型指针都是属于对象头

案例

这里使用一个JOL全称为Java Object Layout框架,是分析JVM中对象布局的工具,该工具大量使用了Unsafe、JVMTI来解码布局情况,所以分析结果是比较精准的。

package com.nuih.JOL;
import org.openjdk.jol.info.ClassLayout;

public class HelloJOL {
    public static void main(String[] args) {
        Object o = new Object();
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }
}
复制代码
java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
复制代码

4. 对象头具体包括什么?

(markword classpointer)synchronized锁信息 对象头主要包括 markwordclass pointer

简单来说,一个刚刚 new 出来的对象,如果开始上锁 (synchronized),它的一个升级过程是:\

** new -> 偏向锁 -> 自旋锁(无锁、lock-free、轻量级锁) -> 重量级锁**。这些信息都记录在 markword 里面。

markword 记录着锁状态、分代年龄、hashcode等

package com.nuih.JOL;
import org.openjdk.jol.info.ClassLayout;

public class HelloJOL {
    public static void main(String[] args) {
        Object o = new Object();
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);

        synchronized (o) {
            s = ClassLayout.parseInstance(o).toPrintable();
            System.out.println(s);
        }
    }
}
复制代码

5. 对象怎么定位?

两种方式:句柄方式直接指针 句柄方式
优点:对象小,垃圾回收时不用频繁改动 t
缺点:两次访问,效率低 \

6.对象怎么分配

其中,AGE(分代年龄)记录在 markword 里面(4byte)。

栈上分配示例:

package com.nuih.jvm.c5_gc;

/**
 *
 *  -XX: -DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB -Xlog:c5_gc
 *  逃逸分析 标量替换 线程专有对象分配
 *
 */
public class TestTLAB {
    // User u;
    class User {
        int id;
        String name;
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    void alloc(int i) {
        new User(i, "name " + i);
    }

    public static void main(String[] args) {
        TestTLAB t = new TestTLAB();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000_0000; i++) t.alloc(i);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}
复制代码

通过观察,关闭 逃逸分析 标量替换,结果接近差两倍。设置参考下图:-XX: -DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB

7. 一个Object占多少字节

Object o = new Object()

  • 其中 o 叫普通对象指针(oops),占 4byte。
  • new Object() 占 16byte。
    所以考虑 o,应该一共是 20byte。但是不一定,这里解释一下:

使用命令打印设置的XX选项及值: 有三个选项:

  • -XX:+PrintCommandLineFlags
  • -XX:+PrintFlagsInitial
  • -XX:+PrintFlagsFinal

-XX:+PrintCommandLineFlags:与-showversion类似,此选项可以在程序运行时首先打印出用户手动设置或者JVM自动设置的XX选项,建议加上这个选项以辅助问题诊断。 java -XX:+PrintCommandLineFlags -version

  • 其中 -XX:InitialHeapSize-XX:MaxHeapSize 初始化和最大堆内存大小,生产环境最好设置一致。
  • -XX:+PrintCommandLineFlags
  • -XX:+UseCompressedClassPointers,开启类指针压缩,这里默认是开启,如果不开启类型指针占用的字节就是8byte。
  • -XX:+UseCompressedOops,开启压缩OOP,这里默认是开启,所以如果不开启,应该是占8byte。

当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。这也会对 CPU 缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小,通过压缩 OOP 可以节省一定的内存。通过 -XX:+UseCompressedOops 选项,JVM 会使用 32 位的 OOP,而不是 64 位的 OOP。

通过了解上面的,你可能会问?什么时候不开启压缩? 作为4个字节寻址:,当堆内存超过这个值,自动不起作用,不开启压缩了。


部分图片来源于网络,版权归原作者,侵删。复制代码
这篇关于难难难难难!对象的创建七连问的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!