OpenJDK 21:resolve_get_put仅记录首次字段访问,如何实现全量记录?
字段/静态变量全量访问记录问题(OpenJDK 21)
问题描述
我正在构建一款分析工具,需要记录字段/静态变量的每次访问。目前使用InterpreterRuntime::resolve_get_put方法实现,但该方法仅能记录首次访问,推测后续访问被缓存,需明确:
- 缓存发生在何处?
- 如何实现每次访问的记录?
- 是否需要修改CPU相关代码?
当前使用的代码如下:
void InterpreterRuntime::resolve_get_put(JavaThread* current, Bytecodes::Code bytecode) { // resolve field fieldDescriptor info; LastFrameAccessor last_frame(current); constantPoolHandle pool(current, last_frame.method()->constants()); methodHandle m(current, last_frame.method()); bool is_put = (bytecode == Bytecodes::_putfield || bytecode == Bytecodes::_nofast_putfield || bytecode == Bytecodes::_putstatic); bool is_static = (bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic); { JvmtiHideSingleStepping jhss(current); JavaThread* THREAD = current; // For exception macros. LinkResolver::resolve_field_access(info, pool, last_frame.get_index_u2_cpcache(bytecode), m, bytecode, CHECK); } // end JvmtiHideSingleStepping fprintf(stderr, "%s access to %s.%s%s\n", is_put ? "put" : "get", info.field_holder()->external_name(), info.name()->as_C_string(), info.signature()->as_C_string()); // check if link resolution caused cpCache to be updated ConstantPoolCacheEntry* cp_cache_entry = last_frame.cache_entry(); if (cp_cache_entry->is_resolved(bytecode)) return; // compute auxiliary field attributes TosState state = as_TosState(info.field_type()); // Resolution of put instructions on final fields is delayed. That is required so that // exceptions are thrown at the correct place (when the instruction is actually invoked). // If we do not resolve an instruction in the current pass, leaving the put_code // set to zero will cause the next put instruction to the same field to reresolve. // Resolution of put instructions to final instance fields with invalid updates (i.e., // to final instance fields with updates originating from a method different than <init>) // is inhibited. A putfield instruction targeting an instance final field must throw // an IllegalAccessError if the instruction is not in an instance // initializer method <init>. If resolution were not inhibited, a putfield // in an initializer method could be resolved in the initializer. Subsequent // putfield instructions to the same field would then use cached information. // As a result, those instructions would not pass through the VM. That is, // checks in resolve_field_access() would not be executed for those instructions // and the required IllegalAccessError would not be thrown. // // Also, we need to delay resolving getstatic and putstatic instructions until the // class is initialized. This is required so that access to the static // field will call the initialization function every time until the class // is completely initialized ala. in 2.17.5 in JVM Specification. InstanceKlass* klass = info.field_holder(); bool uninitialized_static = is_static && !klass->is_initialized(); bool has_initialized_final_update = info.field_holder()->major_version() >= 53 && info.has_initialized_final_update(); assert(!(has_initialized_final_update && !info.access_flags().is_final()), "Fields with initialized final updates must be final"); Bytecodes::Code get_code = (Bytecodes::Code)0; Bytecodes::Code put_code = (Bytecodes::Code)0; if (!uninitialized_static) { get_code = ((is_static) ? Bytecodes::_getstatic : Bytecodes::_getfield); if ((is_put && !has_initialized_final_update) || !info.access_flags().is_final()) { put_code = ((is_static) ? Bytecodes::_putstatic : Bytecodes::_putfield); } } cp_cache_entry->set_field( get_code, put_code, info.field_holder(), info.index(), info.offset(), state, info.access_flags().is_final(), info.access_flags().is_volatile() ); }
缓存位置说明
你推测的缓存逻辑是对的:resolve_get_put属于字段解析阶段的方法,仅在首次访问字段时触发,用于解析字段的元信息(如内存偏移量、所属类等),并将这些信息缓存到ConstantPoolCacheEntry中。
代码中cp_cache_entry->set_field(...)就是将解析后的字段访问信息写入常量池缓存条目。后续执行getfield/putfield/getstatic/putstatic字节码时,解释器会直接从缓存中读取字段的内存偏移量,直接访问内存,不再进入resolve_get_put方法,所以无法记录后续访问。
实现每次访问记录的可行方案
方案1:修改模板解释器的字段访问指令逻辑
OpenJDK的解释器是基于模板实现的,每个字节码对应一段平台相关的机器码模板。要捕获所有解释器中的字段访问,需要修改对应字节码的模板生成逻辑:
- 路径:
src/hotspot/cpu/<你的CPU架构>/templateInterpreter_<架构>.cpp(比如x86架构对应src/hotspot/cpu/x86/templateInterpreter_x86.cpp) - 找到
generate_getfield、generate_putfield、generate_getstatic、generate_putstatic这些函数,在生成实际访问字段的机器码之前/之后,插入调用自定义记录函数的逻辑(比如调用你写的日志输出或统计函数)。
方案2:使用JVM TI事件(无需修改JVM源码)
如果不想改动JVM核心代码,可以利用JVM TI提供的FieldAccess和FieldModification事件:
- 注册对应的回调函数,JVM会在每次字段被访问/修改时触发回调,你可以在回调中记录访问信息。
- 这种方式跨平台,但会有一定性能开销,适合对性能要求不高的场景。
方案3:覆盖JIT编译后的方法访问(若程序会被JIT编译)
如果目标程序会被C1/C2编译器编译为机器码,仅修改解释器还不够,需要同步修改JIT编译器的字段访问生成逻辑:
- C1编译器:修改
src/hotspot/share/opto/parse.cpp中处理字段访问的代码,插入记录逻辑 - C2编译器:修改
src/hotspot/share/opto/access.cpp等相关文件,在生成字段访问的机器码时加入记录逻辑
是否需要修改CPU相关代码?
- 若选择修改模板解释器或JIT编译器:需要修改对应CPU架构的代码,因为模板解释器是用平台相关的汇编/机器码模板实现的,JIT编译器也会生成平台特定的机器码。
- 若选择JVM TI方案:无需修改CPU相关代码,因为JVM TI是跨平台的标准API。
内容的提问来源于stack exchange,提问作者loukritios
相关产品推荐
相关产品推荐

