关于 Object o = new Object()
DCL
与 volatile
问题?(指令重排)# 源码: 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对象建立关联。DCL
与 volatile
问题?(指令重排)为了理解什么是 DCL
(双检锁/双重校验锁(DCL,即 double-checked-locking)),我们先回顾一下 单例模式(Singleton Pattern
)。
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建最佳的对象,同时确保只有单个对象被创建。这个类提供类一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
参考代码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
对象与数组的存储不同
作为普通对象来说,当new出一个对象放入内存的时候它由4项构成:
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 复制代码
(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); } } } 复制代码
两种方式:句柄方式 、 直接指针
句柄方式
优点:对象小,垃圾回收时不用频繁改动 t
缺点:两次访问,效率低 \
其中,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
Object o = new Object()
o
叫普通对象指针(oops),占 4byte。new Object()
占 16byte。o
,应该一共是 20byte。但是不一定,这里解释一下:使用命令打印设置的XX选项及值: 有三个选项:
-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个字节寻址:,当堆内存超过这个值,自动不起作用,不开启压缩了。
部分图片来源于网络,版权归原作者,侵删。复制代码