难难难难难!对象的创建七连问
关于 Object o = new Object()
1、 请解释一下对象的创建过程?(半初始化)
2、 加问 DCL
与 volatile
问题?(指令重排)
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. 加问 DCL
与 volatile
问题?(指令重排)
为了理解什么是 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锁信息 对象头主要包括 markword
与 class 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个字节寻址:,当堆内存超过这个值,自动不起作用,不开启压缩了。
部分图片来源于网络,版权归原作者,侵删。