难难难难难!对象的创建七连问
关于 Object o = new Object()
- 请解释一下对象的创建过程?(半初始化)
- 加问
DCL
与volatile
问题?(指令重排) - 对象在内存中的存储布局?(对象与数组的存储不同)
- 对象头具体包括什么?(markword classpointer)synchronized锁信息
- 对象怎么定位?(直接 间接)
- 对象怎么分配?(栈上-线程本地-Eden-Old)
- 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>>
初始化,调用它的构造方法
从上图动画可以看出,对象的创建过程分为 3 3 3步:
- 申请内存:执行完
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
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个字节寻址:
2
32
=
4
G
B
2^{32} = 4GB
232=4GB,当堆内存超过这个值,自动不起作用,不开启压缩了。
部分图片来源于网络,版权归原作者,侵删。