专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

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

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

关于 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>> 初始化,调用它的构造方法 100_1.png

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

  • 申请内存:执行完 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 一半的时候,只拿到了默认值,还没获取初始化值。 100_3.png 这个时候下面两条指令有可能会发生 指令重排序 ,这时候就会先建立关联,再调用构造方法赋予初始值。目前 t 就执行了 半初始化 的这个状态对象 100_4.pngt 指向半初始化状态对象的时候,正好这个时候第二个线程来了,当前 t指向了半初始化状态的对象, 肯定不为空。那就直接用了,那就用半初始化状态的这个对象,就会发生不可预知的错误。 100_5.png

所以:百分之百要加 volatile

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

对象与数组的存储不同 100_6.png

作为普通对象来说,当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); } } 

100_7.png

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); } } } 

100_8.png

5. 对象怎么定位?

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

6.对象怎么分配

100_10.png

其中,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 100_11.png

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 100_12.png

  • 其中 -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个字节寻址:equation_tex_2_32_20_204GB,当堆内存超过这个值,自动不起作用,不开启压缩了。

101_13.png

部分图片来源于网络,版权归原作者,侵删。

文章永久链接:https://tech.souyunku.com/33855

未经允许不得转载:搜云库技术团队 » 难难难难难!对象的创建七连问

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们