查看原文
其他

Java 新建对象过程分析

ImportNew ImportNew 2021-12-02

(给ImportNew加星标,提高Java技能)


编译:ImportNew/唐尤华

shipilev.net/jvm/anatomy-quarks/6-new-object-stages/


1. 写在前面


“[JVM 解剖公园][1]”是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟。限于篇幅,仅对某个主题按照问题、测试、基准程序、观察结果深入讲解。因此,这里的数据和讨论可以当轶事看,不做写作风格、句法和语义错误、重复或一致性检查。如果选择采信文中内容,风险自负。


Aleksey Shipilёv,JVM 性能极客   

推特 [@shipilev][2]   

问题、评论、建议发送到 [aleksey@shipilev.net][3]


[1]:https://shipilev.net/jvm-anatomy-park

[2]:http://twitter.com/shipilev

[3]:aleksey@shipilev.net


2. 问题


听说分配与初始化不同。Java 有构造函数,它究竟会执行分配还是做初始化呢?


3. 理论


如果打开 [GC Handbook][4],它会告诉你创建一个新对象通常包括三个阶段:


> 译注:GC Handbook 中文版《垃圾回收算法手册》


  1. "分配":从进程空间中分配实例数据。

  2. "系统初始化":按照 Java 语言规范进行初始化。在 C 语言中,分配新对象不需要初始化;在 Java 中,所有新创建的对象都要进行系统初始化赋默认值,设置完整的对象头等等。

  3. "二次初始化(用户初始化)":执行与该对象类型关联的所有初始化语句和构造函数。


在前面 [TLAB 分配][5]中我们对此进行过讨论,现在介绍详细的初始化过程。假如你熟悉 Java 字节码,就会知道 `new` 语句对应了几条字节码指令。例如:


```java
public Object t()
{
return new Object();
}
```


会编译为:


```java
public java.lang.Object t();
descriptor: ()Ljava/lang/Object;
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: new #4 // java/lang/Object 类
3: dup
4: invokespecial #1 // java/lang/Object."<init>":()V 方法
7: areturn
```


[4]:http://gchandbook.org/

[5]:https://shipilev.net/jvm/anatomy-quarks/4-tlab-allocation/


看起来 `new` 会执行分配和系统初始化,同时调用构造函数(`<init>`)执行用户初始化。然而,智能的 Hotspot 虚拟机会不会优化?比如在构造函数执行完成以前查看对象使用情况,优化可以合并的任务。接下来,让我们做个实验。


4. 实验


要解除这个疑问,可以编写下面这样的测试。初始化两个不同的类,每个类只包含一个 `int` 属性:


```java
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class UserInit {

@Benchmark
public Object init()
{
return new Init(42);
}

@Benchmark
public Object initLeaky()
{
return new InitLeaky(42);
}

static class Init {
private int x;
public Init(int x) {
this.x = x;
}
}

static class InitLeaky {
private int x;
public InitLeaky(int x) {
doSomething();
this.x = x;
}

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
void doSomething() {
// 此处留白
}
}
}
```


设计测试时,为防止编译器对 `doSomething()` 空方法进行内联优化加上了限制,迫使优化程序认为接下来可能有代码访问 `x`。换句话说,这样就无法判断 `doSomething()` 是否真的泄露了对象,从而可以有效地把对象暴露给某些外部代码。


建议启用 `-XX:+UseParallelGC -XX:-TieredCompilation -XX:-UseBiasedLocking` 参数运行测试,这样生成的代码更容易理解。JMH `-prof perfasm` 参数可以完美地转储测试生成的代码。


下面是 `Init` 测试结果:


```asm
0x00007efdc466d4cc: mov 0x60(%r15),%rax ; 下面是 TLAB 分配
0x00007efdc466d4d0: mov %rax,%r10
0x00007efdc466d4d3: add $0x10,%r10
0x00007efdc466d4d7: cmp 0x70(%r15),%r10
0x00007efdc466d4db: jae 0x00007efdc466d50a
0x00007efdc466d4dd: mov %r10,0x60(%r15)
0x00007efdc466d4e1: prefetchnta 0xc0(%r10)
; ------- /分配 ---------
; ------- 系统初始化 ---------
0x00007efdc466d4e9: movq $0x1,(%rax) ; header 设置 mark word
0x00007efdc466d4f0: movl $0xf8021bc4,0x8(%rax) ; header 设置 class word
; ...... 系统/用户初始化 .....
0x00007efdc466d4f7: movl $0x2a,0xc(%rax) ; x = 42.
; -------- /用户初始化 ---------
```


上面生成的代码中可以看到 TLAB 分配、对象元数据初始化,然后对字段执行系统+用户初始化。`InitLeaky` 的测试结果有很大区别:


```asm
; ------- 分配 ----------
0x00007fc69571bf4c: mov 0x60(%r15),%rax
0x00007fc69571bf50: mov %rax,%r10
0x00007fc69571bf53: add $0x10,%r10
0x00007fc69571bf57: cmp 0x70(%r15),%r10
0x00007fc69571bf5b: jae 0x00007fc69571bf9e
0x00007fc69571bf5d: mov %r10,0x60(%r15)
0x00007fc69571bf61: prefetchnta 0xc0(%r10)
; ------- /分配 ---------
; ------- 系统初始化 ---------
0x00007fc69571bf69: movq $0x1,(%rax) ; header 设置 mark word
0x00007fc69571bf70: movl $0xf8021bc4,0x8(%rax) ; header 设置 class word
0x00007fc69571bf77: mov %r12d,0xc(%rax) ; x = 0 (%r12 的值恰好是 0)
; ------- /系统初始化 --------
; -------- 用户初始化 ----------
0x00007fc69571bf7b: mov %rax,%rbp
0x00007fc69571bf7e: mov %rbp,%rsi
0x00007fc69571bf81: xchg %ax,%ax
0x00007fc69571bf83: callq 0x00007fc68e269be0 ; call doSomething()
0x00007fc69571bf88: movl $0x2a,0xc(%rbp)
; x = 42
; ------ /用户初始化 ------
```


由于优化程序无法确定是否需要 `x` 值,因此这里必须假定出现最坏的情况,先执行系统初始化,然后再完成用户初始化。


5. 观察


虽然教科书的定义很完美,而且生成的字节码也提供了佐证,但只要不出现奇怪的结果,优化程序还是会做一些不为人知的优化。从编译器的角度看,这只是一种简单优化。但从概念上说,这个结果已经超出了“阶段”的范畴。


推荐阅读

(点击标题可跳转阅读)

JVM 源码分析之 Java 对象的创建过程

Redis 的各项功能解决了哪些问题?

一遍记住 Java 常用的八种排序算法与代码实现


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

好文章,我在看❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存