专题 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入口可以同时存在。
查看 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。
查看 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,这个简单例子不足以保证读到的是同一批次快照,需额外协议、锁或发布不可变对象引用。
查看 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、内存节点和目标后端。上述内容是源码静态推导,本章未采集目标代码反汇编。
查看 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。
- AdvancedThresholdPolicy::submit_compile · HotSpot 8u462-b08
- Parse::do_get_xxx / load · HotSpot 8u462-b08
- Parse::do_get_xxx / acquire · HotSpot 8u462-b08
- Parse::do_put_xxx / release · HotSpot 8u462-b08
- Parse::do_put_xxx / volatile · HotSpot 8u462-b08
- membar_volatile · HotSpot 8u462-b08
- Deoptimization::uncommon_trap · HotSpot 8u462-b08
- JLS8 §17.3 / sleep与yield
- JLS8 §17.4 / 内存模型与HB
- Oracle HotSpot分层编译与逃逸分析
- Oracle JDK8 java命令与诊断参数
先记住这几件事
- 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只是可控环境中的对照工具。