You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenJDK 21:resolve_get_put仅记录首次字段访问,如何实现全量记录?

字段/静态变量全量访问记录问题(OpenJDK 21)

问题描述

我正在构建一款分析工具,需要记录字段/静态变量的每次访问。目前使用InterpreterRuntime::resolve_get_put方法实现,但该方法仅能记录首次访问,推测后续访问被缓存,需明确:

  1. 缓存发生在何处?
  2. 如何实现每次访问的记录?
  3. 是否需要修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 21:07:35