空值检查消除

理解空值检查消除,是理解 JIT 如何优化代码的关键。

为什么需要空值检查

Java 要求对空引用进行检查:

// 可能抛出 NullPointerException
public int length(String s) {
    return s.length();  // 需要检查 s 是否为 null
}

// 如果 s 为 null,会抛出 NullPointerException

空值检查的开销

// 空值检查的字节码
public int length(java.lang.String);
  0: aload_1           // 加载 s
  1: ifnonnull 4      // if s != null, goto 4
  2: aconst_null      // 加载 null
  3: athrow           // 抛出 NullPointerException
  4: aload_1           // 实际执行
  5: invokevirtual #2  // 调用 length()
  6: ireturn

空值检查消除的原理

路径消亡分析(PUBA)

PUBA(Path Unreachable After Uncheck)分析空值检查是否必要:

flowchart TD
    A["if (obj != null)"] --> B{"之前是否有赋值?"}
    B -->|"是,赋值非 null"| C["检查消除"]
    B -->|"赋值可能为 null"| D["保留检查"]
    
    style C fill:#00b894
    style D fill:#ff6b6b

优化示例

// 优化前
public void process(String s) {
    if (s != null) {  // 检查
        s.length();    // 使用
    }
}

// 优化后
public void process(String s) {
    // JIT 判断 s 在之前已赋值非 null
    s.length();  // 检查消除
}

空值检查消除的类型

1. 条件检查消除

// 条件检查消除
public void process(Object obj) {
    if (obj != null) {  // 条件检查
        obj.toString();
    }
}

// JIT 判断 obj 已知非 null
public void process(Object obj) {
    obj.toString();  // 检查消除
}

2. 提前检查消除

// 提前检查消除
public void process(Object obj) {
    if (obj == null) {
        throw new IllegalArgumentException();
    }
    obj.toString();  // 已知非 null
}

// 优化后
public void process(Object obj) {
    if (obj == null) {
        throw new IllegalArgumentException();
    }
    // else 分支已知 obj 非 null
    obj.toString();  // 检查消除
}

3. 循环检查消除

// 循环检查消除
public void process(String[] arr) {
    for (String s : arr) {
        s.hashCode();  // 已知非 null
    }
}

// JIT 判断 arr 元素不可能为 null
public void process(String[] arr) {
    for (String s : arr) {
        s.hashCode();  // 检查消除
    }
}

空值检查消除的条件

1. 明确的非 null 赋值

// 明确非 null - 可以消除检查
public void process() {
    Object obj = new Object();
    obj.toString();  // 检查消除
}

// 可能为 null - 保留检查
public void process() {
    Object obj = getObject();  // 未知
    obj.toString();  // 保留检查
}

2. 参数检查

// 参数检查后
public void process(@NonNull Object obj) {
    // 注解提示 JIT obj 非 null
    obj.toString();
}

// 但 JIT 主要基于实际分析,不依赖注解

3. 逃逸分析

// 逃逸分析辅助空值检查消除
public void process() {
    Object obj = new Object();
    // obj 可能在内部使用
    use(obj);
    // JIT 可能判断 obj 不逃逸
    // 即使逃逸,null 检查仍然需要
}

空值检查消除的效果

性能对比

// 优化前后对比
public int calculate(String s) {
    return s.length();  // 需要空值检查
}

// 优化后
public int calculate(String s) {
    // 如果 JIT 判断 s 已知非 null
    return s.length();  // 无空值检查
}

字节码对比

// 优化前
public int calculate(java.lang.String);
  0: aload_1
  1: ifnonnull 4
  2: aconst_null
  3: athrow
  4: aload_1
  5: invokevirtual #2
  8: ireturn

// 优化后(假设 s 已知非 null)
public int calculate(java.lang.String);
  0: aload_1
  1: invokevirtual #2
  4: ireturn

观察空值检查消除

PrintCompilation

# 观察编译优化
java -XX:+PrintCompilation \
     -XX:+UnlockDiagnosticVMOptions \
     -jar application.jar

JIT 日志

// JIT 日志中可能看到空值检查消除
// 但通常不单独显示

空值检查消除的限制

1. 外部输入

// 无法消除
public void process(String s) {
    // s 来自外部输入
    s.length();  // 必须检查
}

2. 动态类型

// 无法消除
public void process(Object obj) {
    // obj 可能是任何类型
    obj.toString();  // 必须检查
}

3. 多态调用

// 无法消除
public void process(Comparable c) {
    // c 可能是任何实现类
    c.compareTo(null);  // 必须检查
}

最佳实践

1. 使用 @NonNull 注解

// 使用 @NonNull 注解提示
import javax.annotation.Nonnull;

public void process(@Nonnull String s) {
    s.length();  // JIT 可能利用这个信息
}

2. 避免不必要检查

// 不推荐:检查后立即使用
public void process(Object obj) {
    if (obj != null) {
        obj.toString();
    }
}

// 推荐:JIT 会自动优化
public void process(Object obj) {
    obj.toString();  // JIT 判断是否需要检查
}

3. 合理使用断言

// 断言不会消除运行时检查
public void process(Object obj) {
    assert obj != null;  // 检查保留
    obj.toString();
}

空值检查消除与其他优化

空值检查消除与其他 JIT 优化密切相关:

flowchart TB
    A["空值检查消除"] --> B["内联"]
    A --> C["逃逸分析"]
    A --> D["常量传播"]
    
    B --> E["性能提升"]
    C --> E
    D --> E

与内联的关系

// 内联后可能触发空值检查消除
public int length(String s) {
    return s.length();
}

public void process() {
    String s = getNonNullString();  // 已知非 null
    length(s);  // 内联后,s 已知非 null
}

与逃逸分析的关系

// 逃逸分析可能辅助空值检查消除
public void process() {
    String s = "hello";  // 常量,不逃逸
    s.length();  // 检查消除
}