Java 对象深入探究

这篇博客是为了深入探究 Java 中对象的知识。

对象的创建

首先我们先看下一个简单创建对象的代码,看一个对象到底是如何在内存中创建的。

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Person obj = new Person();
}


public class Person {
private int age=8;
private String name="fu";

}

对应的 JVM 指令:

1
2
3
4
5
0 new #2 <learnJava/AQS/Person>
3 dup
4 invokespecial #3 <learnJava/AQS/Person.<init>>
7 astore_1
8 return

对应指令含义:

new: 创建一个实例对象。

dup: 复制栈顶数值并将复制值压入栈顶

invokespecial:调用超类构建方法, 实例初始化方法, 私有方法

astore_1:将栈顶引用类型数值存入指定本地变量

具体的初始化过程如下:

image-20210307232154928

DCL 和 volatitle 的问题

在常用的单例模式中,有一个 double check 模式 ,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private volatile static Singleton05 singleton05;
//double check 性能最好
public static Singleton05 getInstance() {
if (singleton05 == null) {
synchronized (Singleton05.class) {
if (singleton05 == null) {
singleton05 = new Singleton05();
}
}
}
return singleton05;
}

在诸多的单例模式中, double check lock 是性能和简单的方式之一,在上面对象的定义中,使用了 volatitle 的关键字描述,如果此时我们没有使用 volatitlte 关键字会怎样?

image-20210308231542469

因为在初始化的时候,存在一个半初始化的状态,其实是已经创建的对象,但是对象中的字段为 0,此时 DCL 检查不为空的时候,则满足了条件,即会直接返回,导致意想不到的结果。

Volatitle 关键字有两个作用

  1. 对象的内存区域加一个内存屏障,防止指令重排序。
  2. 锁定 CPU 和内存空间总线,让每一个线程的数据保持最新。

对象在内存中的存储布局

Java 对象一般分为 3 块空间:对象头实例数据对齐空间。在数组对象中又单独增加的数据长度的空间,具体几个对象布局如下:

image-20210309223629055

问题: 一个 Object 占用几个字节?

答案:16 字节,算法分为两种:

  1. 不开启指针压缩: 8+8+0+0 = 16
  2. 开启类指针压缩: 8+4+0+4 = 16

如果是 Object obj=new Object(); 则 obj 占用了 4 个字节,所以一共占用了 20 字节。

压缩对类指针和 压缩对象指针

在对象中一般会保留类的引用,称为类指针(Class Pointer)。同样,指向实例也会有一个指针,称为对象指针(OOP)。这个指针一般是 8 个字节,压缩后变成 4 个字节。

image-20210309230720613

为什么要存在指针压缩呢?目的肯定是为了节省内存,为什么压缩指针能行的通呢?我个人觉得有两个原因:

  1. 对象都是 8 字节对齐的,所以指针后都是相对的 “整数“(能整除 8)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    例如: (不是 JVM 的指针压缩,只是表达指针压缩的含义)
    16 00000000 00001000
    32 00000000 00010000
    64 00000000 00100000
    可以转为:
    //舍弃 3 个 0
    16 00000000 00001 000
    32 00000000 00010 000
    64 00000000 00100 000
  1. 类指针和对象指针的寻址范围一般不会超过 4 字节,2^32*8 =32G(一旦超过这个值,则压缩失效)

在 JVM 中有两个参数来控制指针

UseCompressClassPointers : 类指针压缩

UseCompressOop : 对象指针压缩

对象的分配规则

  • 允许在栈上直接分配开关打开,优先在栈上分配,对象如果想在栈上分配需要有两个条件:
  1. 标量替换:用成员变量代替对象。
  2. 不产生逃逸:不会被其他方法引用
  • 在栈上分配空间效率比较高,直接 pop ,没有 GC 的过程。
  • 如果栈空间不够用了,如果对象过大,直接进入老年区。
  • 如果不大,会进入 edian 区,符合线程 ==本地分配== (进入线程独有的 buffer TLAB,不存在锁竞争,效率比较高),进入线程本地分配,不满足进入 edian 区。

01