百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

大牛巧用一文带你彻底搞懂解释器的内部构造和解释执行过程

liebian365 2024-10-22 15:40 5 浏览 0 评论

模板解释器

最简单的Java虚拟机可以只包括类加载器和解释器:类加载器加载字节码iconst_1、iconst_1、iadd并传给虚拟机,解释器按照字节码计算并得到结果。在没有JIT编译器的情况下,解释器从某种程度上来说就是虚拟机本体,有关虚拟机的绝大部分问题都能在解释器中找到答案。

本章将详细讨论解释器的内部构造和解释执行过程。

解释器体系

众所周知,HotSpot VM默认使用解释和编译混合(-Xmixed)的方式执行代码。首先它使用模板解释器对字节码进行解释,当发现一段代码是热点时,就使用C1或C2即时编译器优化编译后再执行,这也是它的名字——“热点”的由来。解释器的代码位于hotspot/share/interpreter,它的总体架构如图5-1所示。

HotSpot VM有一个C++字节码解释器,还有一个模板解释器(Template Interpreter),它们有很大的区别。

C++解释器行为

对于Java字节码istore_0和iadd来说,如果是C++字节码解释器(见图5-1右侧部分所示),那么它的工作流程如代码清单5-1所示。

代码清单5-1 C++字节码解释器伪代码

void cppInterpreter::work(){
for(int i=0;i<bytecode.length();i++){
switch(bytecode[i]){
case ISTORE_0:
int value = operandStack.pop();
localVar[0] = value;
break;
case IADD:
int v1 = operandStack.pop();
int v2 = operandStack.pop();
int res = v1+v2;
operandStack.push(res);
break;
....
}
}
}

C++解释器使用C++语言模拟字节码的执行:iadd是两个数相加,字节码解释器从栈上pop两个数据然后求和,再push到栈上。如果是模板解释器就完全不一样了。

模板解释器

行为模板解释器是一堆机器代码的例程,会在虚拟机创建时初始化好,换句话说,模板解释器在虚拟机初始化的时候为iadd和istore_0申请两片内存,并设置为可读、可写、可执行,然后向内存写入模拟iadd和istore_0执行的机器代码。在解释执行时遇到iadd,跳转到相应内存,并将该片内存的数据视作代码直接执行。

通常,JIT暗指即时编译器,但是JIT(Just-In-Time)这个词本身并没有编译器的含义,它只是表示“即时”,如果按照这个定义,JIT指运行时机器代码生成技术。在这个定义下,模板解释器也属于JIT范畴,因为根据上面的描述,它的各个组件如同各种字节码,异常处理、安全点处理等都是在虚拟机启动的时候动态生成机器代码,然后组成一个整体的。如果上面的描述太过抽象,可以参见代码清单5-2,它直观地说明了模板解释器是什么。

代码清单5-2 模板解释器

class TemplateInterpreter: public AbstractInterpreter {
protected:
static address _throw_ArrayIndexOutOfBoundsException_entry;
static address _throw_ArrayStoreException_entry;
static address _throw_ArithmeticException_entry;
static address _throw_ClassCastException_entry;
static address _throw_NullPointerException_entry;
static address _throw_exception_entry;
static address _throw_StackOverflowError_entry;
static address _remove_activation_entry;
static address _remove_activation_preserving_args_entry;static EntryPoint _return_entry[number_of_return_entries];
static EntryPoint _earlyret_entry;
static EntryPoint _deopt_entry[number_of_deopt_entries];
static address _deopt_reexecute_return_entry;
static EntryPoint _safept_entry;
static DispatchTable _active_table;
static DispatchTable _normal_table;
static DispatchTable _safept_table;
static address _wentry_point[DispatchTable::length];
...
};

TemplateInterpreter包含各种机器代码入口,例如字节码对应的机器代码模板(_normal_table)、退优化的机器代码(_deopt_entry)、常见异常发生时的机器代码(_throw_X)。除此之外,TemplateInterpreter继承自AbstractInterpreter,也包含一些机器代码入口,如代码清单5-3所示。

代码清单5-3 抽象解释器

class AbstractInterpreter: AllStatic {
protected:
static StubQueue* _code;
static bool _notice_safepoints;
static address _native_entry_begin;
static address _native_entry_end;// 方法入口点
static address _entry_table[number_of_method_entries];
static address _cds_entry_table[number_of_method_entries];
static address _slow_signature_handler;
static address _rethrow_exception_entry;
...
};

如代码清单5-3所示,抽象解释器中包含普通方法入口的机器代码(_entry_table)、CDS方法入口的机器代码(_cds_entry_table)、第4章提到的处理解释器与JNI调用约定的机器代码(_slow_signature_handler)等。_entry_table等价于代码清单5-1中的for-switch,也就是说,模板解释器把“遍历方法字节码然后逐个执行”这一过程也写成了机器代码。

机器代码片段

上面的TemplateInterpreter和AbstractInterpreter包含各种机器代码片段,它们构成解释器本体。机器代码片段的生成是由TemplateInterpreterGenerator完成的,它是解释器本体的生成器。关于重要入口机器代码的生成过程将在本章后面详细描述,这里我们关心的是生成的机器代码片段,它们都会放入桩代码队列(_code),如代码清单5-4所示。

代码清单5-4 桩代码队列

class StubQueue: public CHeapObj<mtCode> {
private:
StubInterface* _stub_interface; // 沟通Stub和StubQueue的接口
address _stub_buffer; // 存放机器的地方(buffer)
int _buffer_size; // buffer大小
int _buffer_limit; // buffer大小限制
int _queue_begin; // 队列开始
int _queue_end; // 队列结束
int _number_of_stubs; // 机器代码片段个数
Mutex* const _mutex;
public:
StubQueue::StubQueue(...) : _mutex(lock) {
intptr_t size = align_up(buffer_size, 2*BytesPerWord);
BufferBlob* blob = BufferBlob::create(name, size);if( blob == NULL) {
vm_exit_out_of_memory(...);
}
_stub_interface = stub_interface;
_buffer_size = blob->content_size();
_buffer_limit = blob->content_size();
_stub_buffer = blob->content_begin();
_queue_begin = 0;
_queue_end = 0;
_number_of_stubs = 0;
}
};

StubQueue是code/stubs中的一个结构。它抽象出一个存放机器代码片段的队列,当模板解释器的生成器生成机器代码时会将代码片段放入该队列。StubQueue只是一个队列抽象,真正存放机器代码的片段是_stub_buffer,它由BufferBlob::create()创建。

CodeCache

在HotSpot VM中,除了模板解释器外,有很多地方也会用到运行时机器代码生成技术,如广为人知的C1编译器产出、C2编译器产出、C2I/I2C适配器代码片段、解释器到JNI适配器的代码片段等。为了统一管理这些运行时生成的机器代码,HotSpot VM抽象出一个CodeBlob体系,由CodeBlob作为基类表示所有运行时生成的机器代码,并衍生出五花八门的子类:

1)CompiledMethod:编译后的Java方法。

a)nmethod:JIT编译后的Java方法。

b)AOTCompiledMethod:AOT编译的方法。

2)RuntimeBlob:非编译后的代码片段。

a)BufferBlob:解释器等使用的代码片段。

AdapterBlob:C2I/I2C适配器代码片段。

VtableBlob:虚表代码片段。

MethodHandleAdapterBlob:MethodHandle代码片段。

b)RuntimeStub:调用运行时方法的代码片段。

c)SingletonBlob:单例代码片段。

DeoptimizationBlob:退优化代码片段。

ExceptionBlob:异常处理代码片段。SafepointBlob:错误指令异常处理代码片段。

UncommonTrapBlob:打破编译器假设的稀有情况代码片段。

前面提到过C2I/I2C适配器代码片段,它们就存放在AdapterBlob中。解释器到JNI的调用约定适配器代码片段和模板解释器一样,都存放在BufferBlob中。前面进行分类是为了区分代码片段的类型,而统一管理这些即时生成的机器代码片段的区域是CodeCache,由虚拟机将所有CodeBlob都放入CodeCache。

第4章曾提到Threads::create_vm会初始化线程和组件,CodeCache便是这里所说的组件之一,它在Threads::create_vm初始化主线程后初始化,如代码清单5-5所示。

代码清单5-5 CodeCache初始化

void CodeCache::initialize() {
... // 开启分段CodeCache,将运行时生成的代码片段按类别放到三个区域
if (SegmentedCodeCache) {
initialize_heaps();
} else {
... // 不开启分段CodeCache,所有运行时生成的代码片段都放到一个区域
add_heap(rs, "CodeCache", CodeBlobType::All);
}
// 初始化指令缓存刷新模块(ICache Flush)
icache_init();
// * Windows上为CodeCache中的运行时生成的代码注册结构化异常处理(SEH)
os::register_code_area((char*)low_bound(), (char*)high_bound());
}

CodeCache区域的最大空间可以用-XX:ReservedCodeCacheSize=<val>指定。

Java 9在JEP 197中引入了CodeCache分段。如果没有开启CodeCache分段,JVM会用一个区域存放所有运行时生成的代码片段。

如果使用-XX:+SegmentedCodeCache开启分段,JVM会将CodeCache内

部拆分为三个区域,分别用于存放非nmethod代码片段(如解释器、C2I/I2C适配器等)、处于分层编译的2和3级别带Profiling信息的nmethod、处于分层编译的1和4级别不带Profiling信息的nmethod。

CodeCache分段有很多好处,比如:

分隔非nmethod方法,例如带Profiling的nmethod与不带Profiling的nmethod,可以根据需要访问不同的区域,无须每次遍历整个CodeCache。

提升程序运行时尤其是GC的性能。在开启分段堆后GC扫描根只需要遍历一个区域。

提升代码局部性,因为相同类型的代码很有可能在最近一段时间被频繁访问。

指令缓存刷新

模板解释器和JIT编译器都重度依赖运行时代码生成技术,它们在运行时向内存写入数据,这些数据可以被当作指令执行。CPU和主存间一般有L1、L2、L3三级高速缓存,L1级高速缓存又可以分为指令缓存(Instruction Cache)和数据缓存(Data Cache),这样划分后CPU可以同时获取指令和数据,进而提升性能,但是也带来了一致性问题。

处理器只能执行位于指令缓存中的指令,不能直接将数据缓存中的数据视作指令来执行。同时处理器只能看到位于数据缓存中的数据,不能直接访问内存。因为不能直接修改指令缓存和内存,所以会出现如图5-2所示的情况。

处理器未来会自动将数据缓存的数据写回内存,然后指令缓存重新读取位于内存的指令,但是没有办法知道处理器何时这样做。举个例子,如果虚拟机运行时生成了新的代码想要立即执行它们,处理器可能会忽略它们执行旧的代码,因为旧的代码仍然位于指令缓存中。观察图片的箭头不难知道,要解决这个问题需要强制将数据缓存中的新数据先写回内存,然后载入指令缓存,如图5-3所示。

要想执行新的指令,可以强制刷新指令缓存的数据,使缓存的指令无效化,这时处理器会主动将数据缓存中的数据写入内存,然后读取内存的新指令到指令缓存。HotSpot VM中无效化指令缓存的操作由runtime/icache模块完成,CodeCache区域初始化后会调用icache_init()初始化指令缓存刷新模块,如代码清单5-6所示。

代码清单5-6 指令缓存清理的实现

void ICacheStubGenerator::generate_icache_flush(...) {
...
// 如果待清理的内存地址为0,则跳过清理
__ testl(lines, lines);
__ jcc(Assembler::zero, done);
// 禁止CPU指令重排序(只能使用mfence屏障)
__ mfence();
// 否则清理[0,ICache::line_size]内存地址范围内的缓存行
__ bind(flush_line);
__ clflush(Address(addr, 0)); // 底层是x86的clflush实现
__ addptr(addr, ICache::line_size);
__ decrementl(lines);
__ jcc(Assembler::notZero, flush_line);
// 禁止CPU指令重排序
__ mfence();
// 清理完成
__ bind(done);
__ ret(0);
*flush_icache_stub = (ICache::flush_icache_stub_t)start;
}

x86上指令缓存刷新是由clflush指令实现的,该指令是唯一一个必须配合使用mfence的指令。

本文给大家讲解的内容是详细讨论解释器的内部构造和解释执行过程

  1. 下篇文章给大家讲解的是详解探讨模板解释器,解释器的生成;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: