浅析JVM方法解析、创建和链接

简介: 上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。

1.前言

之前写了一篇文章《你知道Java类是如何被加载的吗?》,分析了HotSpot是如何加载Java类的,干脆趁热打铁,再来分析下Hotspot又是如何解析、创建和链接类方法的。


2.Class文件中的Java方法

Java类在编译后会被编译成 Class 文件。


先来看下 Class 文件的结构:

ClassFile {
       ......
       u2 methods_count;
       method_info methods[methods_count];
       ......
}
  • methods_count 记录了 Class 文件中一共有多少方法。
  • methods 是个数组,包含 Class 文件的所有方法。


methods 的数组类型为 method_info。


每个 method_info 对应一个 Java 方法。

method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
  • access_flags 是方法的访问权限。
  • name_index 是方法名在常量池中的索引。
  • descriptor_index 是方法描述符在常量池中的索引。
  • attributes_count 记录了方法一共有多少属性。
  • attributes是个数组,包含了方法的所有属性。


attributes 中的每一项都是方法的一个属性,其中代表字节码的属性为 Code_attribute:


Code_attribute {
    u2 attribute_name_index; 
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length; 
    {
        u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
  • max_stack 表示当前方法操作数栈的最大深度。
  • max_locals 表示当前方法局部变量的最大个数。
  • code[code_length] 记录了方法中的字节码指令


总的来说,Class 文件中对方法的描述还是很简洁清晰的。


3.HotSpot 如何解析 Java 方法

Class 文件相当于 Java 类的模板,JVM 在读取 Class 文件后,会根据这个模板,建立 Java 类在虚拟机中的模型。


在上篇文章《你知道Java类是如何被加载的吗?》中,我提到了 ClassFileParser,它是HotSpot 加载类所需要的一员大将,通过名字我们就能猜出它的作用:类文件解析器。


还记得Class在JVM中对应的 InstanceKlass 是如何创建的吗?不记得话可以看下述代码回忆下。


ClassFileParser parser(stream,
                         name,
                         loader_data,
                         protection_domain,
                         host_klass,
                         cp_patches,
                         ClassFileParser::BROADCAST, // publicity level
                         CHECK_NULL);

  InstanceKlass* result = parser.create_instance_klass(old_stream != stream, CHECK_NULL);


上面这段代码主要是创建了一个ClassFileParser,并调用了其create_instance_klass()来创建 InstanceKlass。但是对于Class文件的解析,是在 create_instance_klass()之前就完成了的。当经过一系列初始化操作后,ClassFileParser 便在其构造函数的末尾,调用 parse_stream(stream, CHECK),开始了 Class 文件的解析之旅。


在 parse_stream()中,ClassFileParser 会对整个Class文件解析解析,包括常量池、字段、父类、接口等信息,当然也包括类方法。用来解析所有类方法的函数为:parse_methods()。

void ClassFileParser::parse_methods(const ClassFileStream* const cfs,
                                    bool is_interface,
                                    AccessFlags* promoted_flags,
                                    bool* has_final_method,
                                    bool* declares_nonstatic_concrete_methods,
                                    TRAPS) {
  ......
  const u2 length = cfs->get_u2_fast();
  if (length == 0) {
    _methods = Universe::the_empty_method_array();
  } else {
    _methods = MetadataFactory::new_array<Method*>(_loader_data,
                                                   length,
                                                   NULL,
                                                   CHECK);

    for (int index = 0; index < length; index++) {
      Method* method = parse_method(cfs,
                                    is_interface,
                                    _cp,
                                    promoted_flags,
                                    CHECK);

      ......
  }
  ......
}


ClassFileParser 对于 Class文件的解析是流式的,parse_methods()先通过:

cfs->get_u2_fast() 拿到方法数量,接着便开始进行遍历,调用 parse_method()依次解析每个类方法。


从Class文件中method_info的定义可知,method 基本上所有信息都存储在method_info中的attributes[] 数组中,所以对于method的解析,基本上也就是在遍历attributes[] 数组。


method_info 中的attributes[] 数组是用来存放方法的各个属性的,其中包括Code属性、Exception属性、MethodParameters属性、Synthetic属性。parse_method()要做的主要工作,就是遍历attributes[] 数组,解析每个属性。


下面我们来便来各个击破,看看上面这些属性是如何被解析的。


3.1 解析 Code 属性

(1)获取maxStacks、maxLocals和code length


if (_major_version == 45 && _minor_version <= 2) {
  cfs->guarantee_more(4, CHECK_NULL);
  max_stack = cfs->get_u1_fast();
  max_locals = cfs->get_u1_fast();
  code_length = cfs->get_u2_fast();
} else {
  cfs->guarantee_more(8, CHECK_NULL);
  max_stack = cfs->get_u2_fast();
  max_locals = cfs->get_u2_fast();
  code_length = cfs->get_u4_fast();
}


(2)获取字节码指令首地址


code_start = cfs->current();


(3)解析方法中的异常处理表


exception_table_length = cfs->get_u2_fast();
if (exception_table_length > 0) {
  exception_table_start = parse_exception_table(cfs,
                                                code_length,
                                                exception_table_length,
                                                CHECK_NULL);
}

(4)解析Code属性中的属性表,如:

LineNumberTables、LocalVariableTables、LocalVariableTypeTables。主要是用于记录一些调试信息。


3.2 解析 Exception 属性

Exception 属性记录了方法可能抛出的异常。


checked_exceptions_start =
            parse_checked_exceptions(cfs,
                                     &checked_exceptions_length,
                                     method_attribute_length,
                                     CHECK_NULL);


3.3 解析 MethodParameters 属性

MethodParameters 属性记录了方法的参数信息。


method_parameters_seen = true;
method_parameters_length = cfs->get_u1_fast();
const u2 real_length = (method_parameters_length * 4u) + 1u;
if (method_attribute_length != real_length) {
  classfile_parse_error(
    "Invalid MethodParameters method attribute length %u in class file",
    method_attribute_length, CHECK_NULL);
}
method_parameters_data = cfs->current();


3.4 解析 Synthetic 属性

Synthetic 属性表示成员是在编译期自动为Class生成,如内部类提供给外部类用来访问内部成员的 access()方法。


access_flags.set_is_synthetic();

如果在解析到该属性,直接调用 set_is_synthetic()标志下即可。


由上面的解析过程可知,ClassFileParser 主要就是按照Java虚拟机规范对Class文件结构的定义进行流式解析。


4.HotSpot 如何创建 Java 方法

经过第三节的解析,ClassFileParser 已经从Class文件中获取到了方法的所有信息。接下来要做的,便是通过读取的信息,创建 Java 方法在 JVM 中的数据模型。


在HotSpot中,Java方法对应的数据结构为 Method,定义在 method.hpp 中:


class Method : public Metadata {
 ......
 private:
  // If you add a new field that points to any metaspace object, you
  // must add this field to Method::metaspace_pointers_do().
  ConstMethod*      _constMethod;                // Method read-only data.
  MethodData*       _method_data;
  MethodCounters*   _method_counters;
  AccessFlags       _access_flags;               // Access flags
  int               _vtable_index;   
  u2                _intrinsic_id;               // vmSymbols::intrinsic_id (0 == _none)
  // Entry point for calling both from and to the interpreter.
  address _i2i_entry;           // All-args-on-stack calling convention
  // Entry point for calling from compiled code, to compiled code if it exists
  // or else the interpreter.
  volatile address _from_compiled_entry;        // Cache of: _code ? _code->entry_point() : _adapter->c2i_entry()
  // The entry point for calling both from and to compiled code is
  // "_code->entry_point()".  Because of tiered compilation and de-opt, this
  // field can come and go.  It can transition from NULL to not-null at any
  // time (whenever a compile completes).  It can transition from not-null to
  // NULL only at safepoints (because of a de-opt).
  CompiledMethod* volatile _code;                       // Points to the corresponding piece of native code
  volatile address           _from_interpreted_entry; // Cache of _code ? _adapter->i2c_entry() : _i2i_entry
  ......
}

创建 Method 主要分为下面几步。


4.1 分配方法对应的 Method

Method* const m = Method::allocate(_loader_data,
                                   code_length,
                                   access_flags,
                                   &sizes,
                                   ConstMethod::NORMAL,
                                   CHECK_NULL);

4.2 将解析方法时读取到信息填充到 Method 中

m->set_constants(_cp);
m->set_name_index(name_index);
m->set_signature_index(signature_index);
......
// Fill in code attribute information
m->set_max_stack(max_stack);
m->set_max_locals(max_locals);
......
// Copy byte codes
m->set_code((u1*)code_start);
......
// Copy exception table
if (exception_table_length > 0) {
  Copy::conjoint_swap_if_needed<Endian::JAVA>(exception_table_start,
                                              m->exception_table_start(),
                                              exception_table_length * sizeof(ExceptionTableElement),
                                              sizeof(u2));
}
......

Method中将一些只读数据都存放到了它的_constMethod中。


_constMethod类型为ConstMethod,定义在 constMethod.hpp 中。


举个例子,方法的字节码指令就存放在 ConstMethod 中,不过这么说不太严谨,字节码指令并不是直接存放在 ConstMethod 内部,而是紧跟着 ConstMethod 存放在内存中。


我们再看看上面的填充逻辑,调用了 m->set_code((u1*)code_start) 来存放字节码指令首地址,Method 其实是直接调用了 ConstMethod 的 set_code():


void    set_code(address code)      { return constMethod()->set_code(code); }

ConstMethod的set_code()也很简单:


void    set_code(address code) {
  if (code_size() > 0) {
    memcpy(code_base(), code, code_size());
  }
}

首先调用code_base()获取存放字节码的地址,接着便调用memcpy(),将字节码指令从 code 处拷贝到code_base()处。


code_base()代码如下:

address code_base() const            { return (address) (this+1); }

因为 this 本身是指针,所以 this + 1 获取的地址为:

constMethod 首地址 + sizeOf(ConstMethod)

所以字节码指令存放在ConstMethod之后:

image.png

存放好字节码指令后,以后当调用该方法时,就可以从 ConstMethod 中获取到字节码指令首地址,从而进行取指执行了。


5.HotSpot 如何链接 Java 方法

上面只是在加载Class文件时对Java方法进行了解析和创建,而Java 方法的链接是发生在所属InstanceKlass 的初始化时期。


一般来说,Class文件在被加载成 InstanceKlass 后不会立即初始化,而是等到实例化 Obejct、反射获取字段、方法信息,或者调用static方法等时机才会初始化。


InstanceKlass在初始化时会调用 link_class()对类进行链接,在类的链接过程中,便会调用 InstanceKlass的 link_methods()方法,对类的所有方法进行链接。


对单个 Method 进行链接的方法为:

Method::link_method(const methodHandle& h_method, TRAPS)


方法链接主要就是做的事就是设置 Method 的 interpreter_entry:

address entry = Interpreter::entry_for_method(h_method);
set_interpreter_entry(entry);

上面首先通过 entry_for_method(h_method)获取方法的入口例程,关于这个例程是干什么的,简单来说,HotSpot对于Java方法的执行不是简单的从方法字节码首地址处进行取指执行即可,在进行字节码指令执行之前,需要为Java方法创建栈帧、局部变量表等事情,而这些事情是通用的,所以HotSpot将这些事情统一到一起,对Java方法的执行做了一层封装,而例程便是这个封装的入口。


HotSpot 提前为各种类型的方法创建好了一系列例程,所以 entry_for_method(h_method)便是根据方法类型,从例程表中查询到对应类型的例程。


查询到例程后,便调用:

set_interpreter_entry(entry),将例程的入口地址保存到Method中:

void set_interpreter_entry(address entry) {
  assert(!is_shared(), "shared method's interpreter entry should not be changed at run time");
  if (_i2i_entry != entry) {
    _i2i_entry = entry;
  }
  if (_from_interpreted_entry != entry) {
    _from_interpreted_entry = entry;
  }
}

保存例程的入口地址后,以后调用Java方法时,便可以从Method中获取例程的入口地址,跳到此处执行。


6.总结

通过上面的分析,我们了解了Java方法在Class文件中的结构,以及方法的解析、创建及链接。


解析过程主要是流式读取Class文件,获取方法在Class文件中的信息。


创建过程主要是创建Java方法对应的Method,并将解析过程读取的信息填充到Method中。


链接过程主要是根据方法类型,获取并保存方法对应的入口例程的地址。


我的文章只是个引子,毕竟短短篇幅无法囊括JVM浩瀚如烟的源码。如果想对 HotSpot 如何处理 Java 方法的细节深入了解的话,想必最好的方式还是自己去阅读和调试 OpenJDK。






来源  |  阿里云开发者公众号

作者  |  勉成





相关文章
|
1天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
15天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
7天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
1月前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
3天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
|
3天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
3334 15
|
7天前
|
Cloud Native Apache 流计算
PPT合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
3242 10
PPT合集|Flink Forward Asia 2024 上海站
|
20天前
|
人工智能 自然语言处理 前端开发
100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP
打开手机,录制下你完成的代码效果,发布到你的社交媒体,前 100 个@玺哥超Carry、@通义灵码的粉丝,可以免费获得一个降噪蓝牙耳机。
5896 16
|
1月前
|
缓存 监控 Linux
Python 实时获取Linux服务器信息
Python 实时获取Linux服务器信息
|
2天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
349 34