← 学习库技术专题源码原理 · 故障排查 · 架构实践

专题 11 / 完整阅读

JIT即时编译、JMM与内存屏障

JIT的本质是利用程序运行时的反馈,把热点字节码编译成更高效的机器码。它既要承担编译成本,也要保存优化假设失效时恢复执行的能力。JMM则规定共享内存访问允许产生哪些结果;JIT必须在这个边界内优化,但不会自动补上程序缺失的同步协议。

本章以Java SE8规范与OpenJDK8u462-b08的HotSpot实现为基线,源码提交固定为943a5ea328fd2fc8eed0aed4ec9b1957d41f8144。规范、源码推导和教学示意分别标注。这里的C1/C2、编译日志和x86后端不能不加核对地推广到其他JVM、JDK版本与CPU。

核心知识与原理

从字节码到机器码:编译发生在什么时候

javac把源代码编译为class文件;HotSpot可以先解释执行字节码,同时收集调用次数、循环回边次数和接收者类型等反馈。热点方法进入编译队列,编译线程生成机器码并安装为nmethod。后续方法调用可进入编译代码;正在运行的长循环还可能通过OSR切换到编译版本。

static int add(int a, int b) {
    return a + b;
}

这一静态方法的典型字节码是iload_0 → iload_1 → iadd → ireturn,可以用javap -c核对。JIT不必为每条字节码保留一条机器指令:调用方内联它之后,参数可能已经在寄存器里;如果参数是常量,连加法也可能被折叠。机器码依赖目标架构、调用上下文和优化决策,不能把示意汇编当作固定输出。

机制 发生时点与输入 收益和代价
解释执行 运行时逐步执行字节码 无需先为全部方法生成机器码,但有解释分派开销
JIT 运行时使用字节码及反馈 针对真实负载优化,同时消耗编译CPU、元数据与Code Cache
AOT 运行前生成目标机器码 减少运行时编译工作;也可以结合离线反馈优化,能力取决于实现

因此,JIT的优势不能简单归结为“比AOT多做内联”。关键是反馈获取的时点、优化假设与回退能力。冷启动、预热阶段和稳态应分别测量;一个只执行一次的方法,编译成本未必值得付出。

分层编译、热点策略与OSR

在本章的HotSpot8分层编译实现中,level0表示解释执行;level1是C1无profiling,level2是C1有限profiling,level3是C1完整profiling,level4是C2。C1较快地产生代码,带反馈的层级继续积累信息;C2用这些信息进行更深入的优化。具体层级受启动参数、编译器可用性和策略影响。

这不是所有方法都必须走完的流水线。策略会综合计数、增长速率、已有MethodData、编译队列压力等决定下一层级;不同方法可能跳级或停在较低层级。阈值也不是语言规范承诺的固定次数。

OSR即On-Stack Replacement:方法还没有返回,长循环已足够热,JVM可在指定字节码位置转入编译执行。它通常编译带特定入口的方法版本,不能理解为单独编译一条循环指令。PrintCompilation里的%表示OSR编译,@后面给出入口BCI;普通入口与OSR入口可以同时存在。

解释执行 / level 0调用与回边计数 / 类型反馈分层策略选择层级和入口BCICompileBroker编译队列C1 / level 1至3C2 / level 4安装nmethod / 后续调用或OSR进入守卫失败或依赖失效 / 安全回退
图 1 · 对照上文阅读流程
查看 Mermaid 图表源码
flowchart TD
 A["解释执行 / level 0"] --> B["调用与回边计数 / 类型反馈"]
 B --> C["分层策略选择层级和入口BCI"]
 C --> D["CompileBroker编译队列"]
 D --> E["C1 / level 1至3"]
 D --> F["C2 / level 4"]
 E --> G["安装nmethod / 后续调用或OSR进入"]
 F --> G
 G --> B
 G --> H["守卫失败或依赖失效 / 安全回退"]
 H --> A

内联、逃逸分析与推测性优化

方法内联消除部分调用成本,也把调用双方放进同一个优化范围。常量传播、死代码消除、循环优化和逃逸分析因此可能继续生效。逃逸分析本身是分析;标量替换和锁消除是它可能促成的优化,不能把两者当成同义词。

static int distance(int x, int y) {
    Point p = new Point(x, y);
    return p.x + p.y;
}
static final class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

若优化器证明p不逃逸且能标量替换,计算可能直接使用x和y,不再分配这个Point。准确的说法是“对象分配可能被消除”,而不是“所有不逃逸对象都放到栈上”。分析失败、无法内联或对象身份需要保留时,仍可能在堆上分配。

推测性优化可利用类型反馈,例如接口调用长期只有一种接收者:

interface Op { int apply(int x); }
static int dispatch(Op op, int x) { return op.apply(x); }

JIT可能生成“若接收者是PlusOne,则执行已内联的快速路径;否则转入慢路径或uncommon trap”的代码。这是教学示意,实际策略依赖类型分布、类层次依赖和编译决策。出现第二种类型不意味着一定去优化,更不意味着全JVM重新编译。

去优化如何恢复程序状态

编译器删除了调用边界,甚至消除了对象分配,解释器却仍然需要局部变量、操作数栈、调用帧和对象。HotSpot因此为编译代码保留调试状态、oop位置与相关依赖;去优化时重建解释器所需的逻辑帧,必要时重新物化被标量替换的对象。它不是重新执行整个方法,也不能撤回已经发生的业务副作用。

编译代码进入not entrant后,不再接收相应的新入口调用;失效代码的在途执行和回收还有后续处理。日志出现made not entrant只能证明代码状态发生变化,可能来自更高层级替换或假设失效等原因,不能单独认定存在JIT缺陷。去优化保护优化假设下的语义,不会检测普通字段是否应该加volatile。

是否类型反馈 / 类层次信息优化假设与快速路径守卫与依赖仍满足?继续执行编译代码慢路径或uncommon trap按调试状态恢复帧和必要对象解释执行 / 后续可能重新编译
图 2 · 对照上文阅读流程
查看 Mermaid 图表源码
flowchart TD
 A["类型反馈 / 类层次信息"] --> B["优化假设与快速路径"]
 B --> C{"守卫与依赖仍满足?"}
 C -->|是| D["继续执行编译代码"]
 C -->|否| E["慢路径或uncommon trap"]
 E --> F["按调试状态恢复帧和必要对象"]
 F --> G["解释执行 / 后续可能重新编译"]

普通停止标志为什么可能读不到更新

下面是教学片段,running是普通共享字段。主线程的sleep只是给工作线程运行机会,不建立停止写入与循环读取之间的同步关系。

static boolean running = true;
static void work() {
    while (running) { /* 不在这里打印或加锁 */ }
}
// 主线程:worker.start(); Thread.sleep(1000); running = false;

在没有正确同步的情况下,编译器可能复用已读到的值,从效果上类似if (running) { while (true) {} }。这个变换只帮助理解合法结果,不声称某次运行一定生成这种机器码。即使解释器每次都读字段,程序也没有获得所需的跨线程保证。

将停止字段声明为static volatile boolean running = true,读写就带有volatile语义,不能把重复读取任意消除成一直使用旧值。但它不承诺写入后几毫秒内退出,线程仍需要获得调度机会。Thread.sleep()、Thread.yield()没有为普通字段建立内存同步;在循环里加入println会改变负载并可能引入额外同步,测试现象改变不能作为原代码正确的证明。

JMM与happens-before:优化的合法边界

JMM是一套并发执行语义,判断一次读取允许观察哪次写入,不能机械映射为CPU缓存或者一块物理“工作内存”。编译器和硬件都可以调整实现,只要可观察行为仍符合规范。单线程语义也包括异常、副作用等约束,不只是最后一个计算结果相同。

两个线程访问同一变量、至少一个是写入且没有happens-before排序,就存在数据竞争。对于正确同步的程序,JLS提供顺序一致性保证;对于未正确同步的程序,允许一些违反直觉的结果,但并非允许任意破坏类型安全和因果性。

HB来源 建立的关系 使用前提
程序次序 同一线程较早动作HB较晚动作 是语义顺序,不要求每条机器指令照抄源码位置
volatile 写HB同步顺序中后续的同一变量读 必须是同一个volatile变量
monitor unlock HB后续lock 必须是同一把锁
start start调用HB被启动线程的动作 不是新线程动作HB启动方之后的全部动作
线程终止 线程动作HB另一线程检测到它终止 成功join是常见方式;超时返回且线程仍活着不满足
传递性 A HB B且B HB C,得到A HB C 用它连接普通字段写入与跨线程读取
static int data;
static volatile boolean ready;
static void writer() {
    data = 42;        // A
    ready = true;     // B
}
static int reader() {
    if (ready) {      // C
        return data;  // D
    }
    return -1;
}

在一次性发布、初值为false且没有其他写入者的前提下,C读到B写入的true,得到A → B → C → D的HB链,D必须读到42。data自身无需volatile。若之后反复重置ready、覆写data,这个简单例子不足以保证读到的是同一批次快照,需额外协议、锁或发布不可变对象引用。

程序次序同一变量同步关系程序次序写data等于42volatile写ready为truevolatile读到true读data得到42
图 3 · 对照上文阅读流程
查看 Mermaid 图表源码
flowchart LR
 A["写data等于42"] -->|程序次序| B["volatile写ready为true"]
 B -->|同一变量同步关系| C["volatile读到true"]
 C -->|程序次序| D["读data得到42"]

重排序、Store Buffer与volatile全序

以下两个线程的字段初值均为0;它们的执行代码没有同步:

// 线程A                     // 线程B
x = 1;                       y = 1;
r1 = y;                      r2 = x;

JMM允许r1 == 0 && r2 == 0。一种硬件解释是两个核心的写入尚在各自Store Buffer,读取另一个地址时仍看到旧值;这不需要机器指令文本已经交换顺序。它只是可能机制,单次结果不能区分JIT重排、处理器排序与具体微架构时序。

若x和y都为volatile,则同步动作的全序必须保留线程内顺序。要让A读到y的0,A读y要位于B写y之前;要让B读到x的0,B读x要位于A写x之前。结合A写x → A读y和B写y → B读x就产生顺序环,因而两个读都为0被排除。只把其中一个字段改成volatile,不足以得到这个结论。Release/Acquire是理解单次发布的基础,但不足以概括Java volatile的全部排序要求。

volatile自增、CAS与业务不变量

static volatile int count;
static void increment() { count++; }

volatile使单次读写符合相应语义,并没有把读、加一、写回组合成一个原子动作。两个线程都读0并分别写1,在volatile同步全序下也完全合法。用AtomicInteger.incrementAndGet()可提供这个数值上的原子递增;多个变量共同维护的不变量,仍需要能覆盖整个协议的互斥、条件更新或其他设计。

CAS在指定位置上比较期望值并尝试原子更新。失败时调用方根据最新状态重试,不能把一串独立CAS当作一个事务。ABA是否构成问题取决于中间历史是否影响业务;必要时将版本号纳入状态。并发工具的内存语义由相应API规定,不能把所有弱CAS或其他语言的原子访问一概当作Java volatile。

“先AtomicInteger.get判断余额足够,再addAndGet扣款”虽然每一步都是原子的,整体仍会出现竞态条件。它说明业务竞态与数据竞争不同:没有普通字段的数据竞争,也不等于检查与修改这个业务组合已经安全。

源码级解析与调用链

热点事件如何提交编译任务

method_invocation_event → call_event → compile → submit_compile → CompileBroker::compile_method连接调用热度与编译请求;回边事件另经loop_event决定OSR层级和BCI。下面是固定版本的完整submit_compile函数,InvocationEntryBci区分普通方法入口和OSR入口;hot_count相应使用调用计数或回边计数。

AdvancedThresholdPolicy::submit_compileHotSpot 8u462-b08 · L452–L458 · 完整源码
// Update the rate and submit compile
void AdvancedThresholdPolicy::submit_compile(methodHandle mh, int bci, CompLevel level, JavaThread* thread) {
  int hot_count = (bci == InvocationEntryBci) ? mh->invocation_count() : mh->backedge_count();
  update_rate(os::javaTimeMillis(), mh());
  CompileBroker::compile_method(mh, bci, level, mh, hot_count, "tiered", thread);
}


完整函数:以入口BCI选择调用或回边计数,更新速率后向CompileBroker提交请求;它不直接生成机器码。

C2构图与机器码安装

CompileBroker的编译线程取出CompileTask,invoke_compiler_on_method选择编译器,进入C1或C2的compile_method。C2经Parse把字节码转成图,通过内联、图优化、匹配目标指令、寄存器分配等流程输出代码,最终安装nmethod。编译排队期间应用仍可运行原有版本;编译失败不代表Java方法不能执行。

定位入口:CompileBroker::invoke_compiler_on_method、C2编译驱动compile.cpp。下面进一步跟踪C2处理volatile字段的局部窗口,窗口不等于完整方法。

volatile读:加载属性与Acquire节点

Parse::do_get_xxx首先检查字段是否volatile。构造加载时选择acquire内存顺序;在这个版本的后续分支中,还插入MemBarAcquire节点。图中的内存依赖限制非法重排,不能把节点数量直接换算成硬件屏障数量。

Parse::do_get_xxx / loadHotSpot 8u462-b08 · L147–L280 · 完整源码
void Parse::do_get_xxx(Node* obj, ciField* field, bool is_field) {
  // Does this field have a constant value?  If so, just push the value.
  if (field->is_constant()) {
    // final or stable field
    const Type* stable_type = NULL;
    if (FoldStableValues && field->is_stable()) {
      stable_type = Type::get_const_type(field->type());
      if (field->type()->is_array_klass()) {
        int stable_dimension = field->type()->as_array_klass()->dimension();
        stable_type = stable_type->is_aryptr()->cast_to_stable(true, stable_dimension);
      }
    }
    if (field->is_static()) {
      // final static field
      if (C->eliminate_boxing()) {
        // The pointers in the autobox arrays are always non-null.
        ciSymbol* klass_name = field->holder()->name();
        if (field->name() == ciSymbol::cache_field_name() &&
            field->holder()->uses_default_loader() &&
            (klass_name == ciSymbol::java_lang_Character_CharacterCache() ||
             klass_name == ciSymbol::java_lang_Byte_ByteCache() ||
             klass_name == ciSymbol::java_lang_Short_ShortCache() ||
             klass_name == ciSymbol::java_lang_Integer_IntegerCache() ||
             klass_name == ciSymbol::java_lang_Long_LongCache())) {
          bool require_const = true;
          bool autobox_cache = true;
          if (push_constant(field->constant_value(), require_const, autobox_cache)) {
            return;
          }
        }
      }
      if (push_constant(field->constant_value(), false, false, stable_type))
        return;
    } else {
      // final or stable non-static field
      // Treat final non-static fields of trusted classes (classes in
      // java.lang.invoke and sun.invoke packages and subpackages) as
      // compile time constants.
      if (obj->is_Con()) {
        const TypeOopPtr* oop_ptr = obj->bottom_type()->isa_oopptr();
        ciObject* constant_oop = oop_ptr->const_oop();
        ciConstant constant = field->constant_value_of(constant_oop);
        if (FoldStableValues && field->is_stable() && constant.is_null_or_zero()) {
          // fall through to field load; the field is not yet initialized
        } else {
          if (push_constant(constant, true, false, stable_type))
            return;
        }
      }
    }
  }

  Node* leading_membar = NULL;
  ciType* field_klass = field->type();
  bool is_vol = field->is_volatile();

  // Compute address and memory type.
  int offset = field->offset_in_bytes();
  const TypePtr* adr_type = C->alias_type(field)->adr_type();
  Node *adr = basic_plus_adr(obj, obj, offset);
  BasicType bt = field->layout_type();

  // Build the resultant type of the load
  const Type *type;

  bool must_assert_null = false;

  if( bt == T_OBJECT ) {
    if (!field->type()->is_loaded()) {
      type = TypeInstPtr::BOTTOM;
      must_assert_null = true;
    } else if (field->is_constant() && field->is_static()) {
      // This can happen if the constant oop is non-perm.
      ciObject* con = field->constant_value().as_object();
      // Do not "join" in the previous type; it doesn't add value,
      // and may yield a vacuous result if the field is of interface type.
      type = TypeOopPtr::make_from_constant(con)->isa_oopptr();
      assert(type != NULL, "field singleton type must be consistent");
    } else {
      type = TypeOopPtr::make_from_klass(field_klass->as_klass());
    }
  } else {
    type = Type::get_const_basic_type(bt);
  }
  if (support_IRIW_for_not_multiple_copy_atomic_cpu && field->is_volatile()) {
    leading_membar = insert_mem_bar(Op_MemBarVolatile);   // StoreLoad barrier
  }
  // Build the load.
  //
  MemNode::MemOrd mo = is_vol ? MemNode::acquire : MemNode::unordered;
  Node* ld = make_load(NULL, adr, type, bt, adr_type, mo, LoadNode::DependsOnlyOnTest, is_vol);

  // Adjust Java stack
  if (type2size[bt] == 1)
    push(ld);
  else
    push_pair(ld);

  if (must_assert_null) {
    // Do not take a trap here.  It's possible that the program
    // will never load the field's class, and will happily see
    // null values in this field forever.  Don't stumble into a
    // trap for such a program, or we might get a long series
    // of useless recompilations.  (Or, we might load a class
    // which should not be loaded.)  If we ever see a non-null
    // value, we will then trap and recompile.  (The trap will
    // not need to mention the class index, since the class will
    // already have been loaded if we ever see a non-null value.)
    // uncommon_trap(iter().get_field_signature_index());
#ifndef PRODUCT
    if (PrintOpto && (Verbose || WizardMode)) {
      method()->print_name(); tty->print_cr(" asserting nullness of field at bci: %d", bci());
    }
#endif
    if (C->log() != NULL) {
      C->log()->elem("assert_null reason='field' klass='%d'",
                     C->log()->identify(field->type()));
    }
    // If there is going to be a trap, put it at the next bytecode:
    set_bci(iter().next_bci());
    null_assert(peek());
    set_bci(iter().cur_bci()); // put it back
  }

  // If reference is volatile, prevent following memory ops from
  // floating up past the volatile read.  Also prevents commoning
  // another volatile read.
  if (field->is_volatile()) {
    // Memory barrier includes bogus read of value to force load BEFORE membar
    assert(leading_membar == NULL || support_IRIW_for_not_multiple_copy_atomic_cpu, "no leading membar expected");
    Node* mb = insert_mem_bar(Op_MemBarAcquire, ld);
    mb->as_MemBar()->set_trailing_load();
  }
}

连续局部窗口:特殊平台可能先放置额外屏障;volatile加载带acquire属性和is_vol标记。ld后续还用于Acquire节点。

Parse::do_get_xxx / acquireHotSpot 8u462-b08 · L147–L280 · 完整源码
void Parse::do_get_xxx(Node* obj, ciField* field, bool is_field) {
  // Does this field have a constant value?  If so, just push the value.
  if (field->is_constant()) {
    // final or stable field
    const Type* stable_type = NULL;
    if (FoldStableValues && field->is_stable()) {
      stable_type = Type::get_const_type(field->type());
      if (field->type()->is_array_klass()) {
        int stable_dimension = field->type()->as_array_klass()->dimension();
        stable_type = stable_type->is_aryptr()->cast_to_stable(true, stable_dimension);
      }
    }
    if (field->is_static()) {
      // final static field
      if (C->eliminate_boxing()) {
        // The pointers in the autobox arrays are always non-null.
        ciSymbol* klass_name = field->holder()->name();
        if (field->name() == ciSymbol::cache_field_name() &&
            field->holder()->uses_default_loader() &&
            (klass_name == ciSymbol::java_lang_Character_CharacterCache() ||
             klass_name == ciSymbol::java_lang_Byte_ByteCache() ||
             klass_name == ciSymbol::java_lang_Short_ShortCache() ||
             klass_name == ciSymbol::java_lang_Integer_IntegerCache() ||
             klass_name == ciSymbol::java_lang_Long_LongCache())) {
          bool require_const = true;
          bool autobox_cache = true;
          if (push_constant(field->constant_value(), require_const, autobox_cache)) {
            return;
          }
        }
      }
      if (push_constant(field->constant_value(), false, false, stable_type))
        return;
    } else {
      // final or stable non-static field
      // Treat final non-static fields of trusted classes (classes in
      // java.lang.invoke and sun.invoke packages and subpackages) as
      // compile time constants.
      if (obj->is_Con()) {
        const TypeOopPtr* oop_ptr = obj->bottom_type()->isa_oopptr();
        ciObject* constant_oop = oop_ptr->const_oop();
        ciConstant constant = field->constant_value_of(constant_oop);
        if (FoldStableValues && field->is_stable() && constant.is_null_or_zero()) {
          // fall through to field load; the field is not yet initialized
        } else {
          if (push_constant(constant, true, false, stable_type))
            return;
        }
      }
    }
  }

  Node* leading_membar = NULL;
  ciType* field_klass = field->type();
  bool is_vol = field->is_volatile();

  // Compute address and memory type.
  int offset = field->offset_in_bytes();
  const TypePtr* adr_type = C->alias_type(field)->adr_type();
  Node *adr = basic_plus_adr(obj, obj, offset);
  BasicType bt = field->layout_type();

  // Build the resultant type of the load
  const Type *type;

  bool must_assert_null = false;

  if( bt == T_OBJECT ) {
    if (!field->type()->is_loaded()) {
      type = TypeInstPtr::BOTTOM;
      must_assert_null = true;
    } else if (field->is_constant() && field->is_static()) {
      // This can happen if the constant oop is non-perm.
      ciObject* con = field->constant_value().as_object();
      // Do not "join" in the previous type; it doesn't add value,
      // and may yield a vacuous result if the field is of interface type.
      type = TypeOopPtr::make_from_constant(con)->isa_oopptr();
      assert(type != NULL, "field singleton type must be consistent");
    } else {
      type = TypeOopPtr::make_from_klass(field_klass->as_klass());
    }
  } else {
    type = Type::get_const_basic_type(bt);
  }
  if (support_IRIW_for_not_multiple_copy_atomic_cpu && field->is_volatile()) {
    leading_membar = insert_mem_bar(Op_MemBarVolatile);   // StoreLoad barrier
  }
  // Build the load.
  //
  MemNode::MemOrd mo = is_vol ? MemNode::acquire : MemNode::unordered;
  Node* ld = make_load(NULL, adr, type, bt, adr_type, mo, LoadNode::DependsOnlyOnTest, is_vol);

  // Adjust Java stack
  if (type2size[bt] == 1)
    push(ld);
  else
    push_pair(ld);

  if (must_assert_null) {
    // Do not take a trap here.  It's possible that the program
    // will never load the field's class, and will happily see
    // null values in this field forever.  Don't stumble into a
    // trap for such a program, or we might get a long series
    // of useless recompilations.  (Or, we might load a class
    // which should not be loaded.)  If we ever see a non-null
    // value, we will then trap and recompile.  (The trap will
    // not need to mention the class index, since the class will
    // already have been loaded if we ever see a non-null value.)
    // uncommon_trap(iter().get_field_signature_index());
#ifndef PRODUCT
    if (PrintOpto && (Verbose || WizardMode)) {
      method()->print_name(); tty->print_cr(" asserting nullness of field at bci: %d", bci());
    }
#endif
    if (C->log() != NULL) {
      C->log()->elem("assert_null reason='field' klass='%d'",
                     C->log()->identify(field->type()));
    }
    // If there is going to be a trap, put it at the next bytecode:
    set_bci(iter().next_bci());
    null_assert(peek());
    set_bci(iter().cur_bci()); // put it back
  }

  // If reference is volatile, prevent following memory ops from
  // floating up past the volatile read.  Also prevents commoning
  // another volatile read.
  if (field->is_volatile()) {
    // Memory barrier includes bogus read of value to force load BEFORE membar
    assert(leading_membar == NULL || support_IRIW_for_not_multiple_copy_atomic_cpu, "no leading membar expected");
    Node* mb = insert_mem_bar(Op_MemBarAcquire, ld);
    mb->as_MemBar()->set_trailing_load();
  }
}

连续局部窗口:volatile读取之后创建MemBarAcquire,并关联ld,防止后续相关内存操作越过读取。

volatile写:Release与额外排序约束

Parse::do_put_xxx在volatile写之前插入MemBarRelease,写节点带release属性,之后按平台能力插入MemBarVolatile,并配对记录相关屏障。这解释了为什么仅用“volatile等于Acquire/Release”描述还不完整:额外约束也参与实现volatile同步动作的要求。

Parse::do_put_xxx / releaseHotSpot 8u462-b08 · L282–L357 · 完整源码
void Parse::do_put_xxx(Node* obj, ciField* field, bool is_field) {
  Node* leading_membar = NULL;
  bool is_vol = field->is_volatile();
  // If reference is volatile, prevent following memory ops from
  // floating down past the volatile write.  Also prevents commoning
  // another volatile read.
  if (is_vol) {
    leading_membar = insert_mem_bar(Op_MemBarRelease);
  }

  // Compute address and memory type.
  int offset = field->offset_in_bytes();
  const TypePtr* adr_type = C->alias_type(field)->adr_type();
  Node* adr = basic_plus_adr(obj, obj, offset);
  BasicType bt = field->layout_type();
  // Value to be stored
  Node* val = type2size[bt] == 1 ? pop() : pop_pair();
  // Round doubles before storing
  if (bt == T_DOUBLE)  val = dstore_rounding(val);

  // Conservatively release stores of object references.
  const MemNode::MemOrd mo =
    is_vol ?
    // Volatile fields need releasing stores.
    MemNode::release :
    // Non-volatile fields also need releasing stores if they hold an
    // object reference, because the object reference might point to
    // a freshly created object.
    StoreNode::release_if_reference(bt);

  // Store the value.
  Node* store;
  if (bt == T_OBJECT) {
    const TypeOopPtr* field_type;
    if (!field->type()->is_loaded()) {
      field_type = TypeInstPtr::BOTTOM;
    } else {
      field_type = TypeOopPtr::make_from_klass(field->type()->as_klass());
    }
    store = store_oop_to_object(control(), obj, adr, adr_type, val, field_type, bt, mo);
  } else {
    store = store_to_memory(control(), adr, val, bt, adr_type, mo, is_vol);
  }

  // If reference is volatile, prevent following volatiles ops from
  // floating up before the volatile write.
  if (is_vol) {
    // If not multiple copy atomic, we do the MemBarVolatile before the load.
    if (!support_IRIW_for_not_multiple_copy_atomic_cpu) {
      Node* mb = insert_mem_bar(Op_MemBarVolatile, store); // Use fat membar
      MemBarNode::set_store_pair(leading_membar->as_MemBar(), mb->as_MemBar());
    }
    // Remember we wrote a volatile field.
    // For not multiple copy atomic cpu (ppc64) a barrier should be issued
    // in constructors which have such stores. See do_exits() in parse1.cpp.
    if (is_field) {
      set_wrote_volatile(true);
    }
  }

  // If the field is final, the rules of Java say we are in <init> or <clinit>.
  // Note the presence of writes to final non-static fields, so that we
  // can insert a memory barrier later on to keep the writes from floating
  // out of the constructor.
  // Any method can write a @Stable field; insert memory barriers after those also.
  if (is_field && (field->is_final() || field->is_stable())) {
    set_wrote_final(true);
    // Preserve allocation ptr to create precedent edge to it in membar
    // generated on exit from constructor.
    if (C->eliminate_boxing() &&
        adr_type->isa_oopptr() && adr_type->is_oopptr()->is_ptr_to_boxed_value() &&
        AllocateNode::Ideal_allocation(obj, &_gvn) != NULL) {
      set_alloc_with_final(obj);
    }
  }
}

方法开头的连续窗口:判断volatile属性,在字段存储前插入Release节点;余下存储构造未在此节选展示。

Parse::do_put_xxx / volatileHotSpot 8u462-b08 · L282–L357 · 完整源码
void Parse::do_put_xxx(Node* obj, ciField* field, bool is_field) {
  Node* leading_membar = NULL;
  bool is_vol = field->is_volatile();
  // If reference is volatile, prevent following memory ops from
  // floating down past the volatile write.  Also prevents commoning
  // another volatile read.
  if (is_vol) {
    leading_membar = insert_mem_bar(Op_MemBarRelease);
  }

  // Compute address and memory type.
  int offset = field->offset_in_bytes();
  const TypePtr* adr_type = C->alias_type(field)->adr_type();
  Node* adr = basic_plus_adr(obj, obj, offset);
  BasicType bt = field->layout_type();
  // Value to be stored
  Node* val = type2size[bt] == 1 ? pop() : pop_pair();
  // Round doubles before storing
  if (bt == T_DOUBLE)  val = dstore_rounding(val);

  // Conservatively release stores of object references.
  const MemNode::MemOrd mo =
    is_vol ?
    // Volatile fields need releasing stores.
    MemNode::release :
    // Non-volatile fields also need releasing stores if they hold an
    // object reference, because the object reference might point to
    // a freshly created object.
    StoreNode::release_if_reference(bt);

  // Store the value.
  Node* store;
  if (bt == T_OBJECT) {
    const TypeOopPtr* field_type;
    if (!field->type()->is_loaded()) {
      field_type = TypeInstPtr::BOTTOM;
    } else {
      field_type = TypeOopPtr::make_from_klass(field->type()->as_klass());
    }
    store = store_oop_to_object(control(), obj, adr, adr_type, val, field_type, bt, mo);
  } else {
    store = store_to_memory(control(), adr, val, bt, adr_type, mo, is_vol);
  }

  // If reference is volatile, prevent following volatiles ops from
  // floating up before the volatile write.
  if (is_vol) {
    // If not multiple copy atomic, we do the MemBarVolatile before the load.
    if (!support_IRIW_for_not_multiple_copy_atomic_cpu) {
      Node* mb = insert_mem_bar(Op_MemBarVolatile, store); // Use fat membar
      MemBarNode::set_store_pair(leading_membar->as_MemBar(), mb->as_MemBar());
    }
    // Remember we wrote a volatile field.
    // For not multiple copy atomic cpu (ppc64) a barrier should be issued
    // in constructors which have such stores. See do_exits() in parse1.cpp.
    if (is_field) {
      set_wrote_volatile(true);
    }
  }

  // If the field is final, the rules of Java say we are in <init> or <clinit>.
  // Note the presence of writes to final non-static fields, so that we
  // can insert a memory barrier later on to keep the writes from floating
  // out of the constructor.
  // Any method can write a @Stable field; insert memory barriers after those also.
  if (is_field && (field->is_final() || field->is_stable())) {
    set_wrote_final(true);
    // Preserve allocation ptr to create precedent edge to it in membar
    // generated on exit from constructor.
    if (C->eliminate_boxing() &&
        adr_type->isa_oopptr() && adr_type->is_oopptr()->is_ptr_to_boxed_value() &&
        AllocateNode::Ideal_allocation(obj, &_gvn) != NULL) {
      set_alloc_with_final(obj);
    }
  }
}

连续完整分支:按平台能力选择尾随MemBarVolatile,建立store_pair,并记录方法包含volatile写。

编译器节点与CPU屏障不是一一对应

LoadLoad、LoadStore、StoreStore、StoreLoad是对两侧内存访问排序要求的分类。编译器屏障限制代码变换,硬件排序机制约束核心之间的观察顺序。屏障不是把所有缓存清空,也不能泛称为“每次去主内存读取”。

本章固定版本的x86-64后端在MemBarAcquire与MemBarRelease匹配规则中没有输出对应的独立硬件指令;编译器仍保留排序约束。MemBarVolatile规则调用Assembler::membar(StoreLoad),此版本x86实现使用带lock的操作。也存在带谓词的消除规则,所以不能断言每次volatile访问都会执行mfence。不同CPU后端可能需要其他指令,必须对照实际版本和反汇编。

membar_volatileHotSpot 8u462-b08 · L6329–L6346 · 完整源码
instruct membar_volatile(rFlagsReg cr) %{
  match(MemBarVolatile);
  effect(KILL cr);
  ins_cost(400);

  format %{
    $$template
    if (os::is_MP()) {
      $$emit$$"lock addl [rsp + #0], 0\t! membar_volatile"
    } else {
      $$emit$$"MEMBAR-volatile ! (empty encoding)"
    }
  %}
  ins_encode %{
    __ membar(Assembler::StoreLoad);
  %}
  ins_pipe(pipe_slow);
%}

完整匹配规则:调用Assembler::membar(StoreLoad)。同文件Acquire/Release规则为empty encoding;这里不把AD规则当作本机反汇编。

uncommon trap的恢复入口

Deoptimization::uncommon_trapHotSpot 8u462-b08 · L1794–L1802 · 完整源码
Deoptimization::UnrollBlock* Deoptimization::uncommon_trap(JavaThread* thread, jint trap_request) {

  // Still in Java no safepoints
  {
    // This enters VM and may safepoint
    uncommon_trap_inner(thread, trap_request);
  }
  return fetch_unroll_info_helper(thread);
}

完整函数:先进入uncommon_trap_inner处理,再返回fetch_unroll_info_helper构建的展开信息。帧恢复、对象重建还应继续追踪本文件其他函数。

OrderAccess是HotSpot运行时自身同步使用的架构抽象;它可帮助理解架构差异,但不是所有Java volatile访问都调用它。Java字段的C2编译应跟踪Parse、内存节点和目标后端。上述内容是源码静态推导,本章未采集目标代码反汇编。

JMM允许的执行结果C2字段解析 / acquire与release属性MemBar节点和内存依赖目标CPU后端匹配编译器排序约束 / 必要硬件指令实际可观察行为符合JMM
图 4 · 对照上文阅读流程
查看 Mermaid 图表源码
flowchart TD
 A["JMM允许的执行结果"] --> B["C2字段解析 / acquire与release属性"]
 B --> C["MemBar节点和内存依赖"]
 C --> D["目标CPU后端匹配"]
 D --> E["编译器排序约束 / 必要硬件指令"]
 E --> F["实际可观察行为符合JMM"]

三层原理问答

1. JIT的价值为什么不只是翻译字节码?

JIT能使用运行中的调用频率、分支和类型反馈,为真正热点生成机器码。内联除了省掉调用,也让常量传播、循环与对象分配优化跨越原本的方法边界。它仍需要付出编译成本,所以程序短暂运行时不一定得到收益。

推测的前提怎么保护? 优化器为类型和类层次等假设设置守卫或记录依赖。假设不成立时可以走慢路径,也可能去优化并恢复逻辑帧;不能继续拿已经不合法的假设执行。新增类型并不必然使所有代码失效。

怎么比较性能? 先区分冷启动、预热和稳态,记录相同负载下的吞吐、延迟及编译CPU。微基准需要防止结果未使用而被消除、常量折叠和测量代码被内联等因素,宜使用JMH并查看编译证据,而非只取一次纳秒差值。

2. C1、C2和OSR是不是固定的三步流程?

不是。HotSpot8有解释层和不同profiling强度的C1层,C2对应更高优化层。下一层由运行反馈和编译压力等共同决定,部分方法可以跳级或长期停在较低层;这是一种实现策略,不是Java语言规定的固定路径。

循环只进入方法一次怎么办? 回边事件能积累热度并触发OSR,在某个BCI把当前执行切到编译版本。普通方法入口版本和OSR版本是不同编译任务,所以同一方法在日志里出现多次并不直接意味着异常。

日志应该怎么读? PrintCompilation中的百分号标记OSR,层级列区分C1/C2,at后的数字是入口BCI。made not entrant可能只是新版本替换旧版本。还要结合运行参数、具体JDK和LogCompilation事件,不能用一行日志推断完整原因。

3. 普通字段的无限循环一定是JIT错误吗?

不是。普通共享停止字段没有跨线程同步,工作线程可能一直使用旧值,这是需要先检查的程序语义问题。JIT可以进行规范允许的读取复用;关闭优化后偶然退出,只能说明执行条件改变,不能证明原程序具备所需保证。

volatile解决到哪一层? 它为字段读写建立可见性和排序约束,使循环不能任意固定旧值。读到false后可以离开循环,但不承诺获得调度或退出的最大时间。sleep和yield没有替普通字段建立这条同步关系。

怀疑编译器缺陷需要什么? 固定正确同步的最小复现、完整JDK构建号和参数,对比-Xint及排除特定方法的结果,并保存编译和崩溃证据。JNI、Unsafe和时序变化同样可能影响结果,不能把时间相关性直接当根因。

4. volatile发布为何能让普通data字段可见?

因为HB可以传递。写data先于写volatile ready;读线程读到这次发布的true,再读取data,普通字段的写入与读取就被同一变量的同步关系连接起来。保证来自完整协议,而不只是某个字段的修饰符。

有没有适用边界? 本例限定一次性发布、初值false且没有后续覆写。若发布方继续修改数据,读方可能读到后续状态;用同一个布尔标志标识多轮发布也缺少批次信息。需要根据不变量设计锁、版本或不可变快照。

volatile禁止一切重排吗? 它只限制会破坏相关语义的变换,仍允许合法内联和算术优化。底层可用缓存一致性、编译器约束和必要硬件排序完成要求,不规定每次从物理内存读取,也不规定固定数量的屏障。

5. volatile count++为什么还会丢失更新?

两个线程可以先各自读到0,然后各写1。单次读写可见且有序,并不阻止两个读之间或读写之间的交错;volatile并没有把自增组合成一次不可分割的更新。最终只增加一次,是合法的丢失更新。

AtomicInteger能保护什么? incrementAndGet使一个数值的递增具有原子语义。但先get判断再addAndGet是两次操作,中间状态可能改变;CAS循环需把条件与更新放在同一协议,或使用覆盖全部相关状态的锁。

为什么JIT不自动加锁? 它的职责是按既定语义优化,没有义务猜测业务要求、锁范围与多个字段的不变量。锁消除则是反向证明不需要某段同步,并保留可观察语义;不能据此认为JIT会自动修复共享状态协议。

6. 去优化为什么需要局部变量和对象恢复信息?

优化代码里可能只剩寄存器中的值,调用已经内联,对象分配也可能消失。解释器继续执行却需要Java逻辑帧、操作数栈和对象,因此编译器必须保留可恢复的状态映射;需要时重新物化对象,而不是从方法起点再跑一遍。

它和GC信息是一回事吗? oop位置帮助GC找到引用,去优化还需要恢复字节码位置、内联调用层次及局部状态。二者相关但用途不同。uncommon trap是触发路径之一,类层次依赖失效等也可能导致已编译代码不再可用。

怎样判断实际发生了什么? 新增接收者类型本身不能证明发生了特定去优化,应结合对应编译事件区分慢路径、守卫失败与依赖失效。正文的快速路径和机器指令描述属于机制示意或源码推导,不能当作实际反汇编输出。

模拟生产案例:预热后停止线程偶发失效

现象与证据边界

教学模拟:服务用普通boolean通知后台循环停止,低负载时通常成功,长时间运行后偶发停不下来;加日志后又很难重现。这种描述首先提示数据竞争与测试条件改变,不能仅因“运行久了”就认定JIT有缺陷。本章用模拟情境解释排查思路,不声称重现了真实生产事故。

按语义、编译事件和版本逐层检查

先列出共享字段的所有读写,画出HB关系;检查对象发布、同一把锁、停止与清理的先后顺序。然后在可控环境记录JDK完整版本、CPU架构、VM参数和编译事件,分别比较默认运行、-Xint及只排除work方法。每次对照都改变了性能和调度,差异是线索,需要结合正确性证据判断。

JDK8可使用-XX:+PrintCompilation观察热点编译,配合-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining查看内联决策;-XX:+LogCompilation可记录更完整的编译事件。-Xint用于解释模式对照,-XX:CompileCommand=exclude,包名.类名::方法名用于排除指定方法编译。参数需要结合实际JDK核对,诊断对照仅是定位线索。

修复和进一步验证

停止标志可改为volatile,复杂状态切换则按整体不变量选择锁或原子协议。验证还要覆盖取消、异常退出、资源释放和并发重复关闭。可用OpenJDK jcstress研究允许结果、用OpenJDK JMH测量性能;本章不将有限次数正常运行当作并发正确性证明,也未执行jcstress、JMH或目标机器码反汇编。

知识梳理与核心总结

一分钟要点回顾

JIT在运行时使用热点与类型反馈生成机器码,通过内联、逃逸分析和循环等优化降低执行成本。HotSpot8的分层策略选择C1/C2层级,OSR让长循环不必等下次方法调用。推测路径必须有守卫或依赖保护,失效后靠状态恢复继续合法执行。

JMM规定合法共享内存行为,HB连接跨线程发布与读取。普通停止字段缺少同步,volatile修复相应读写语义,但不把count++变成原子更新。CAS或同一把锁应覆盖需要保护的协议;C2内存节点与CPU指令不是一一对应,关闭JIT后现象变化也不是编译器缺陷的单独证明。

深入理解与机制串联

沿两条链阅读:执行链是“热点事件 → 分层策略 → 编译队列 → C1/C2 → nmethod → 普通入口或OSR → 必要时去优化”;正确性链是“共享状态协议 → HB约束 → C2加载/存储与MemBar节点 → CPU后端 → 允许的观察结果”。两条链共同决定行为,但业务同步要求必须由程序清楚表达。

延伸问题、常见误解与速记

  • JIT不是每条字节码执行前即时重编译;固定调用次数也不是规范阈值。
  • 逃逸分析不保证对象栈上分配;标量替换可能让分配本身消失。
  • 去优化不自动修复数据竞争,made not entrant不单独证明JIT缺陷。
  • volatile发布靠HB链,重复发布还需要批次和快照协议。
  • x/y都为volatile才可用同步全序排除双0;仅Acquire/Release表述不足以概括全部volatile语义。
  • 没有硬件指令输出不等于没有编译器排序约束;MemBar节点也不等于每次执行mfence。
  • 原子单步仍可能组合成业务竞态;-Xint只是可控环境中的对照工具。

官方资料与版本来源

本文按上述版本阅读官方源码,函数窗口保留完整分支与边界,声明窗口聚焦所讨论的字段或配置。版权见 source-notices.txt,下载记录见 sources.json。

先记住这几件事

  • JIT用热点和类型反馈生成机器码,编译成本与稳态收益分开衡量。
  • C1/C2层级由策略选择,OSR可让长循环在方法返回前进入编译代码。
  • 守卫或依赖失效后恢复逻辑帧;去优化不会自动修复数据竞争。
  • volatile发布通过HB链,count++仍需原子更新或锁。
  • MemBar节点不等于固定CPU指令;-Xint对照只是定位线索。

四个重点问题

1. JIT的价值为什么不只是翻译字节码?

JIT能使用运行中的调用频率、分支和类型反馈,为真正热点生成机器码。内联除了省掉调用,也让常量传播、循环与对象分配优化跨越原本的方法边界。它仍需要付出编译成本,所以程序短暂运行时不一定得到收益。

推测的前提怎么保护? 优化器为类型和类层次等假设设置守卫或记录依赖。假设不成立时可以走慢路径,也可能去优化并恢复逻辑帧;不能继续拿已经不合法的假设执行。新增类型并不必然使所有代码失效。

怎么比较性能? 先区分冷启动、预热和稳态,记录相同负载下的吞吐、延迟及编译CPU。微基准需要防止结果未使用而被消除、常量折叠和测量代码被内联等因素,宜使用JMH并查看编译证据,而非只取一次纳秒差值。

2. C1、C2和OSR是不是固定的三步流程?

不是。HotSpot8有解释层和不同profiling强度的C1层,C2对应更高优化层。下一层由运行反馈和编译压力等共同决定,部分方法可以跳级或长期停在较低层;这是一种实现策略,不是Java语言规定的固定路径。

循环只进入方法一次怎么办? 回边事件能积累热度并触发OSR,在某个BCI把当前执行切到编译版本。普通方法入口版本和OSR版本是不同编译任务,所以同一方法在日志里出现多次并不直接意味着异常。

日志应该怎么读? PrintCompilation中的百分号标记OSR,层级列区分C1/C2,at后的数字是入口BCI。made not entrant可能只是新版本替换旧版本。还要结合运行参数、具体JDK和LogCompilation事件,不能用一行日志推断完整原因。

3. 普通字段的无限循环一定是JIT错误吗?

不是。普通共享停止字段没有跨线程同步,工作线程可能一直使用旧值,这是需要先检查的程序语义问题。JIT可以进行规范允许的读取复用;关闭优化后偶然退出,只能说明执行条件改变,不能证明原程序具备所需保证。

volatile解决到哪一层? 它为字段读写建立可见性和排序约束,使循环不能任意固定旧值。读到false后可以离开循环,但不承诺获得调度或退出的最大时间。sleep和yield没有替普通字段建立这条同步关系。

怀疑编译器缺陷需要什么? 固定正确同步的最小复现、完整JDK构建号和参数,对比-Xint及排除特定方法的结果,并保存编译和崩溃证据。JNI、Unsafe和时序变化同样可能影响结果,不能把时间相关性直接当根因。

4. volatile发布为何能让普通data字段可见?

因为HB可以传递。写data先于写volatile ready;读线程读到这次发布的true,再读取data,普通字段的写入与读取就被同一变量的同步关系连接起来。保证来自完整协议,而不只是某个字段的修饰符。

有没有适用边界? 本例限定一次性发布、初值false且没有后续覆写。若发布方继续修改数据,读方可能读到后续状态;用同一个布尔标志标识多轮发布也缺少批次信息。需要根据不变量设计锁、版本或不可变快照。

volatile禁止一切重排吗? 它只限制会破坏相关语义的变换,仍允许合法内联和算术优化。底层可用缓存一致性、编译器约束和必要硬件排序完成要求,不规定每次从物理内存读取,也不规定固定数量的屏障。

核心要点

JIT在运行时使用热点与类型反馈生成机器码,通过内联、逃逸分析和循环等优化降低执行成本。HotSpot8的分层策略选择C1/C2层级,OSR让长循环不必等下次方法调用。推测路径必须有守卫或依赖保护,失效后靠状态恢复继续合法执行。

JMM规定合法共享内存行为,HB连接跨线程发布与读取。普通停止字段缺少同步,volatile修复相应读写语义,但不把count++变成原子更新。CAS或同一把锁应覆盖需要保护的协议;C2内存节点与CPU指令不是一一对应,关闭JIT后现象变化也不是编译器缺陷的单独证明。

常见误解与源码

  • JIT不是每条字节码执行前即时重编译;固定调用次数也不是规范阈值。
  • 逃逸分析不保证对象栈上分配;标量替换可能让分配本身消失。
  • 去优化不自动修复数据竞争,made not entrant不单独证明JIT缺陷。
  • volatile发布靠HB链,重复发布还需要批次和快照协议。
  • x/y都为volatile才可用同步全序排除双0;仅Acquire/Release表述不足以概括全部volatile语义。
  • 没有硬件指令输出不等于没有编译器排序约束;MemBar节点也不等于每次执行mfence。
  • 原子单步仍可能组合成业务竞态;-Xint只是可控环境中的对照工具。

全文搜索

输入关键词,搜索本篇完整正文与答案。