解释器生成

解释器的机器代码片段都是在
TemplateInterpreterGenerator::generate_all()中生成的,下面将分小节详细展示该函数的具体细节,以及解释器某个组件的机器代码生成过程与逻辑。与第4章不同的是,本节中的各部分出现的顺序与它们在代码中的顺序不一致。

在研究解释器前了解调试手段是有必要的。由于运行时生成的机器代码是人类不可读的二进制形式,要想阅读它们,可以下载hsdis-amd64插件,并将该插件放到编译后的JDK中的/lib/server目录下面,然后开启虚拟机参数-XX:+PrintAssembly和-XX:+PrintInterpreter,然后便可输出解释器各个例程的机器代码的汇编表示形式了。也可以开启-XX:+TraceBytecodes跟踪解释器正在执行的字节码和对应方法。

普通方法入口

在第4章提到,JavaCalls::call_helper进入_call_stub_entry会创建Java栈帧(见图4-10),然后进入entry_point执行方法。entry_point即第2章中类链接阶段设置的解释器入口。实际上,解释器入口和_call_stub_entry一样,也是一段机器代码,也是在虚拟机初始化时动态生成的,只是生成它的是generate_normal_entry,如图5-4所示。

图5-4 entry_point解释器入口

entry_point的完整逻辑如图5-4左下角所示,它的详细过程比较复杂,但很值得一探究竟。代码清单5-7展示了解释器入口entry_point的(生成)代码。

代码清单5-7 普通方法入口

address TemplateInterpreterGenerator::generate_normal_entry(...) {
... // ebx存放了指向Method的指针
// 获取参数个数,放入rcx
__ movptr(rdx, constMethod);
__ load_unsigned_short(rcx, size_of_parameters);
// 获取局部变量个数
__ load_unsigned_short(rdx, size_of_locals);
__ subl(rdx, rcx);
// 检查栈上是否可以容纳即将分配的局部变量槽generate_stack_overflow_check();
// 获取返回地址
__ pop(rax);
// 计算第一个参数的地址
__ lea(rlocals, ...);
{// 分配局部变量槽,然后初始化这些槽
Label exit, loop;
__ testl(rdx, rdx);
__ jcc(Assembler::lessEqual, exit);
__ bind(loop);
__ push((int) NULL_WORD);// 用0初始化,局部变量有默认值便是因为它
__ decrementl(rdx);
__ jcc(Assembler::greater, loop);
__ bind(exit);
}
// 创建解释器栈帧(有别于Java栈帧)
generate_fixed_frame(false);
// 从当前位置到对方法加锁还有一段距离,万一中间发生异常且方法又是同步方法,则异常处理器
// 会unlock一个未lock的方法,所以这里需要告诉线程不能unlock
NOT_LP64(__ get_thread(thread));
const Address do_not_unlock_if_synchronized(..);
__ movbool(do_not_unlock_if_synchronized, true);
__ profile_parameters_type(rax, rcx, rdx);
// 执行字节码前增加方法调用计数,如果计数到达一定值即跳到后面通知编译器
...
if (inc_counter) {
generate_counter_incr(...);}
// 设置编译后继续执行的地方
Label continue_after_compile;
__ bind(continue_after_compile);
// 检查geneate_fixed_frame分配之后是否栈溢出
bang_stack_shadow_pages(false);
// 对方法加锁,并通知线程可以unlock方法了(因为可能抛出异常的区域已经没了)
NOT_LP64(__ get_thread(thread));
__ movbool(do_not_unlock_if_synchronized, false);
// 如果方法是同步方法,调用lock_method()锁住方法
if (synchronized) {
lock_method();
}
// 将字节码指针设置到第一个字节码,然后开始执行字节码(!)
__ dispatch_next(vtos);
if (inc_counter) {
...
// 如果之前增加调用计数达到一定值,则跳转到此处通知编译器判断是否编译
__ bind(invocation_counter_overflow);
generate_counter_overflow(continue_after_compile);
}
return entry_point;
}

HotSpot VM中有两个地方可能发生“解释器认为Java方法(循环)是热点并通知编译器判断是否编译”这一行为:字节码中的回边(Backedge)分支计数通知(本章后面讨论)与方法调用计数通知。

上面的代码并非指当某个方法/循环超过阈值时立刻进行编译,其中,generate_counter_inc()检查当前方法调用频率是否超过一个通知值(
-XX:TierXInvokeNotifyFreq-Log=<val>,X表示分层编译的层数,该值可根据情况进行调整),generate_counter_overflow通知编译器方法调用频率达到了一定的程度。两者检查在一定时间范围内某个方法调用是否到达了一定的频率,实际上是否编译是根据编译器策略(TieredThreshold Policy)进行抉择的。

方法加锁

普通方法入口中一个重要的内容是方法加锁,即代码清单5-8中所示的lock_method()。

代码清单5-8 方法加锁

void TemplateInterpreterGenerator::lock_method() {
...
{ Label done;
// 检查方法是否为static
__ movl(rax, access_flags);
__ testl(rax, JVM_ACC_STATIC);
// 获取局部变量槽的第一个元素(即receiver)
__ movptr(rax, ...);
__ jcc(Assembler::zero, done);
// 如果是static,加载方法所在类的Class对象__ load_mirror(rax, rbx);
__ bind(done);
// 保持使用receiver
__ resolve(IS_NOT_NULL, rax);
}
// 基本对象锁(BasicObjectLock)包含一个待锁对象和Displaced Header
// 下面会将receiver放入基本对象锁的待锁对象字段
__ subptr(rsp, entry_size);
__ movptr(monitor_block_top, rsp);
__ movptr(Address(rsp, BasicObjectLock),...);
const Register lockreg = NOT_LP64(rdx) LP64_ONLY(c_rarg1);
__ movptr(lockreg, rsp);
// 加锁
__ lock_object(lockreg);
}

HotSpot VM方法加锁的实现和Java语义一致:如果方法是static则对类的Class对象加锁,反之对this对象加锁。在执行lock_object()前需要找到位于解释器栈上的monitor区的基本对象锁,如图5-5所示。

图5-5 解释器栈的布局(左上)

monitor区存放了若干个基本对象锁(Basic Object Lock)。基本对象锁又叫轻量级锁、瘦锁,它包含一个加锁对象和Displaced Header。在获取到加锁对象后,会将加锁对象放入基本对象锁,然后调用lock_object()。lock_object()并不是简单锁住对象,它还会应用一些锁的优化措施:最开始尝试偏向锁,如果加锁失败则尝试基本对象锁,如果仍然失败则会使用重量级锁,具体过程将会在第6章讨论。

本地方法入口

本地方法入口由generate_native_entry()生成,它和普通方法最大的不同是普通方法通过dispatch_next()执行每一条字节码并达到解释的效果,而本地方法本身就是机器代码,可以直接执行。本地方法入口的实现如代码清单5-9所示。代码清单5-9 native方法调用片段

address TemplateInterpreterGenerator::generate_native_entry(...) {
...
// 获取native方法入口,第3章提到过native方法入口在Method后面的第一个槽
{
Label L;
// 获取native入口
__ movptr(rax, ...Method::native_function_offset());
__ cmpptr(rax, unsatisfied.addr());
// 获取失败会调用prepare_native_call以找到native入口和signature handler。
// 查找native入口的过程请参见第3章
__ jcc(Assembler::notEqual, L);
__ call_VM(...InterpreterRuntime::prepare_native_call);
__ get_method(method);
__ movptr(rax, ...Method::native_function_offset());
__ bind(L);
}
// 从线程上拿出JNIEnv并作为第一个参数传入
__ lea(c_rarg0, ...r15_thread);
__ set_last_Java_frame(rsp, rbp, (address) __ pc());
// 设置线程状态位_thread_in_native(即执行native方法)
__ movl(Address(thread, JavaThread::thread_state_offset()),
_thread_in_native);
// 调用native方法__ call(rax);
...
}

generate_native_entry()会先找native方法入口。第3章曾提到该入口位于Method后面的第一个槽,如果获取失败,则调用
InterpreterRuntime::prepare_native_call查找并设置native方法入口和signature handler。查找成功后传递必要参数,然后就可以跳转到本地方法执行了。

本地方法和普通方法的另一个不同之处是对同步方法的处理。

generate_normal_entry()中只使用lock_method()对方法加锁而没有对应的解锁代码,因为dispatch_next()执行字节码时(见5.5.4节),一些字节码(如return、athrow)在移除栈帧的时候会解锁同步方法,所以无须在generate_normal_entry()中解锁。但是generate_native_entry()没有执行字节码,它必须在执行完native方法之后检查是否需要解锁同步方法。

标准字节码

entry_point在准备好解释器栈帧和加锁事项后,会调用dispatch_next()解释执行字节码。作为字节码执行的“发动机”,dispatch_next(),其具体实现如代码清单5-10所示。

代码清单5-10 字节码派发实现

void InterpreterMacroAssembler::dispatch_next(...) {// 加载一条字节码
load_unsigned_byte(rbx, Address(_bcp_register, step));
// 字节码指针(bcp)前进
increment(_bcp_register, step);
// dispatch_next的详细实现
dispatch_base(...);
}

dispatch_next()的实现和前面描述的行为基本一致:读取字节码→前推字节码指针→执行每一条字节码。具体的执行由dispatch_base完成,如代码清单5-11所示。

代码清单5-11 dispatch_base

void InterpreterMacroAssembler::dispatch_base(...) {
// 获取安全点表
address* const safepoint_table = Interpreter::safept_table(state);
Label no_safepoint, dispatch;
// 如果需要生成安全点
if (SafepointMechanism::uses_thread_local_poll() && ...) {
testb(Address(r15_thread,Thread::polling_page_offset()),
SafepointMechanism::poll_bit());
jccb(Assembler::zero, no_safepoint);
lea(rscratch1, ExternalAddress((address)safepoint_table));
jmpb(dispatch);}
// 如果不需要安全点
bind(no_safepoint);
// 获取模板表
lea(rscratch1, ExternalAddress((address)table));
bind(dispatch);
// 跳转到模板表中指定字节码处的机器代码,然后执行
jmp(Address(rscratch1, rbx, Address::times_8));
}

总结来说,dispatch_next相当于一个“发动机”,它能推进字节码指针,从一个模板表(即字节码表)中找到与当前字节码对应的机器代码片段,并跳到该片段执行字节码片段。字节码执行完后还有一段“结尾曲”代码,会再次调用dispatch_next()。整个解释过程就像由dispatch_next()串联起来的链,只要调用并启动dispatch_next()一次,就能执行方法中的所有字节码。详细的过程如图5-6所示。

HotSpot VM的解释器名为模板解释器,它为所有字节码生成对应的机器代码片段,然后全部放入一个表。当VMThread收到一些请求(如垃圾回收任务)并需要安全点时,它会调用
TemplateInterpreter::notice_safepoints()通知模板解释器将普通的模板表切换为安全点表,由HotSpot VM在安全点表中寻找与当前字节码对应的逻辑然后跳到该位置执行字节码指令。如果不需要安全点,VMThread会关闭安全点,并调用TemplateInterpreter::ignore_safepoints(),再从安全点表切换回模板表,然后正常执行相关指令。实际上安全点表相比于模板表只多了一次InterpreterRuntime::at_safepoint调用,这个调用用于处理安全点的逻辑。在了解了字节码执行的方式后,接下来将讨论一些字节码的具体实现。

1. iconst

iconst向栈压入一个整型常量值,如代码清单5-12所示。

代码清单5-12 字节码iconst

void TemplateTable::iconst(int value) {
transition(vtos, itos);
if (value == 0) {
__ xorl(rax, rax);
} else {
__ movl(rax, value);
}
}

iconst的代码并没有压栈操作,它把值放入了rax寄存器,因为相比于寄存器,压栈操作开销较大,模板解释器把rax和xmm0当作栈顶缓存(Top of Stack,ToS),凡是能用ToS解决的绝不用栈。

那么什么是ToS呢?假设两条指令是iconst_1和istore,基于栈的模板解释器首先压入1,然后弹出1,最后保存到局部变量表。这个过程出现了两次栈操作,即内存读写。有了ToS后,模板解释器会将1放入rax寄存器,用istore读取rax寄存器,完全消除了内存读写过程。

为了确保操作的类型正确(如istore要求栈顶为int类型,iconst无须进行栈顶缓存),代码最开始有一个transition(vtos,itos)约束条件,它表示iconst字节码执行前栈顶无缓存,执行后栈顶缓存是int类型。约束条件除了vtos(无缓存)和itos(int),还有btos(byte)、ztos(bool)、ctos(char)、stos(short)、ltos(long)、ftos(float,使用xmm0寄存器)、dtos(double,使用xmm0寄存器)和atos(object)。

2. add、sub、mul、and、or、shl、shr、ushr

由于整数的四则运算和位运算几乎一样,模板解释器统一把它们放到了lop2中进行处理,如代码清单5-13所示。

代码清单5-13 字节码addsubmulandorxorshlshrushr

void TemplateTable::iop2(Operation op) {
transition(itos, itos);
switch (op) {
case add : __ pop_i(rdx); __ addl (rax, rdx); break;
case sub :__ movl(rdx, rax);__ pop_i(rax);__ subl (rax, rdx); break;
case mul : __ pop_i(rdx); __ imull(rax, rdx); break;
case _and: __ pop_i(rdx); __ andl (rax, rdx); break;
case _or : __ pop_i(rdx); __ orl (rax, rdx); break;case _xor: __ pop_i(rdx); __ xorl (rax, rdx); break;
case shl :__ movl(rcx, rax); __ pop_i(rax); __ shll (rax);break;
case shr :__ movl(rcx, rax); __ pop_i(rax); __ sarl (rax);break;
case ushr:__ movl(rcx, rax);__ pop_i(rax); __ shrl (rax);break;
default : ShouldNotReachHere();
}
}

ToS只能缓存一个数据,而加法需要两个操作数,所以第二个参数必须使用栈操作弹出。

3. new

new会真实地在堆上分配对象,然后返回对象引用并压入栈中,如代码清单5-14所示。

代码清单5-14 字节码new

void TemplateTable::_new() {
// new会在创建对象后向栈顶压入对象引用,所以ToS从无变成atos
transition(vtos, atos);
...
// 如果开启-XX:+UseTLAB,则在TLAB中分配对象,否则在Eden区中分配对象,
// 如果分配成功则将对象引用放入rax
if (UseTLAB) {__ tlab_allocate(thread, rax, rdx, 0, rcx, rbx, slow_case);
// 如果开启-XX:+ZeroTLAB
if (ZeroTLAB) {
// 初始化对象头
__ jmp(initialize_header);
} else {
// 初始化对象头和字段
__ jmp(initialize_object);
}
} else {
__ eden_allocate(thread, rax, rdx, 0, rbx, slow_case);
}
// 用0填充对象头和对象字段
...
// 慢速分配路径:调用InterpreterRuntime::_new
__ bind(slow_case);
...
call_VM(rax, ...InterpreterRuntime::_new);
// 分配完成
__ bind(done);
}

new和方法锁类似,在分配前要尽可能使用轻量级操作。它不会直接在堆中分配对象,如果启用-XX:+UseTLAB,它会尝试在线程的TLAB区中分配对象。TLAB(Thread Local Allocation Buffer)是Thread数据结构的一部分。由于TLAB是每个线程私有的存储区域,因此对象分配无须加锁同步。

TLAB有三个指针,分别记录TLAB开始位置、结束位置、当前使用位置。分配新对象时只需根据对象大小移动当前使用的指针,然后调用
ThreadLocalAllocBuffer::allocate()判断是否到达区域结束位置。这种分配方式也叫碰撞指针(Bump the Pointer)。TLAB结合碰撞指针可以快速地进行对象分配。仅当TLAB分配失败时才调用InterpreterRuntime::_new在堆上进行分配,具体分配方式和代码清单3-3所示代码调用MemAllocator::allocate一样,new最终调用特定垃圾回收器的mem_allocate()方法,如Shenandoah GC的ShenandoahHeap::mem_allocate、ZGC的ZCollectedHeap::mem_allocate、Parallel GC的ParallelScavengeHeap::mem_allocate,具体分配细节涉及垃圾回收模块,会在第三部分(第10~11章)详细讨论。

4. iinc

iinc会将某个局部变量的值增加到指定大小。它的字节码占用三个字节,分别对应iinc、index、const。第一个字节表示它是iinc,第二个字节表示局部变量在局部变量表中的索引,第三个字节表示递增大小,如代码清单5-15所示。

代码清单5-15 字节码iinc

void TemplateTable::iinc() {
transition(vtos, vtos); // iinc不改变栈的状态,无须ToS缓存
__ load_signed_byte(rdx, at_bcp(2)); // 获取constlocals_index(rbx); // 获取局部变量
__ addl(iaddress(rbx), rdx); // 局部变量 = 局部变量+const
}

5. arraylength

arraylength获取数组的长度,然后压栈,如代码清单5-16所示。

代码清单5-16 字节码arraylength

void TemplateTable::arraylength() {
transition(atos, itos); // 执行前栈顶是数组引用,执行后栈顶是数组大小
// 空值检查,确保数组有length字段
__null_check(rax, arrayOopDesc::length_offset_in_bytes());
// 获取数组(arrayOop)的length字段,然后放入ToS
__movl(rax,Address(rax,arrayOopDesc::length_offset_in_bytes()));
}

6. monitorenter

monitorenter是Java关键字synchronized的底层实现,它获取栈顶对象,然后对其加锁,如代码清单5-17所示。

代码清单5-17 字节码monitorenter

void TemplateTable::monitorenter() {
transition(atos, vtos);
// 在栈的monitor区中分配一个槽,用来存放基本对象锁
...
__ bind(allocated);
// 字节码指针递增
__ increment(rbcp);
// 将待加锁的对象放入基本对象锁中
__ movptr(Address(rmon, BasicObjectLock::obj_offset_in_bytes()), rax);
// 加锁
__ lock_object(rmon);
// 确保不会因为刚刚栈上分配的槽造成栈溢出
__ save_bcp();
__ generate_stack_overflow_check(0);
// 执行下一条字节码
__ dispatch_next(vtos);
}

monitorenter会在栈的monitor区中分配一个基本对象锁,具体的加锁工作是由代码清单5-12所示的lock_object()完成的。

7. athrow

Java语言的throw关键字反映到JVM上是一个athrow字节码。根据虚拟机规范描述,athrow弹出栈顶的对象引用作为异常对象,然后在当前方法寻找该匹配对象类型的异常处理器。如果找到它则清空当前栈,重新压入异常对象,最后跳转到异常处理器,就像没有发生异常一样;如果没有找到异常处理器,则弹出当前栈帧并恢复到调用者栈帧,然后在调用者栈帧上继续抛出异常,这样相当于将异常继续传播到调用者,如代码清单5-18所示。

代码清单5-18 字节码athrow

void TemplateTable::athrow() {
transition(atos, vtos);
__ null_check(rax);
__ jump(ExternalAddress(Interpreter::throw_exception_entry()));
}

可以看到athrow会跳到抛异常的机器代码片段,该片段由generate_throw_exception()生成,如代码清单5-19所示。

代码清单5-19 抛异常机器代码生成

void TemplateInterpreterGenerator::generate_throw_exception() {
...
// 抛异常入口
Interpreter::_throw_exception_entry = __ pc();
...
// 清空当前栈的一部分__ empty_expression_stack();
// 寻找异常处理器入口地址,如果找到则返回异常处理器入口,否则返回弹出当前栈帧的代码入口
__ call_VM(rdx,
CAST_FROM_FN_PTR(address,
InterpreterRuntime::exception_handler_for_exception),rarg);
// 不管是哪个入口,结果都会放到rax寄存器
__ push_ptr(rdx);
__ jmp(rax); // 跳到入口执行
...
}


exception_handler_for_exception会寻找异常处理器的入口地址,如果没有找到则返回remove_activation_entry入口,该入口和前面的描述一样,会弹出当前栈帧并恢复到调用者的栈帧,然后在调用者栈帧上继续抛出异常。

8. if_icmp<cond> & branch

if_icmp根据条件(大于/等于/小于等于)跳转到指定字节码处,如代码清单5-20所示。

代码清单5-20 字节码if_icmp<cond>

void TemplateTable::if_icmp(Condition cc) {
transition(itos, vtos);Label not_taken;
__ pop_i(rdx);
__ cmpl(rdx, rax); // 栈顶两个整数比较
__ jcc(j_not(cc), not_taken); // 根据条件进行跳转
branch(false, false); // 跳转
__ bind(not_taken); // 不需要跳转
__ profile_not_taken_branch(rax);
}

branch()实现了各种跳转行为,如if_icmp、goto等。前面提到的字节码的回边分支是两个可能触发编译行为的地方之一,而回边的实现就位于branch()。回边是指从当前字节码出发到前面的一条路径,典型的回边是一次循环结束到新条件判断的一条路径,如代码清单5-21所示。

代码清单5-21 回边字节码

0: iconst_0
1: istore_1
2: iload_1
3: iconst_2
4: if_icmpeq 13
7: iinc 1, 1
10: goto 2
13: return

代码清单5-21所示是一个循环语句,其中,goto字节码会向上跳转到第2条字节码,这样向上跳跃的字节码与目标字节码之间形成的就是一条回边。HotSpot VM不但会对热点方法进行性能计数,还会对回边进行性能计数。通过回边可以探测到热点循环。说到回边就不能不提栈上替换,如代码清单5-22所示。

代码清单5-22 JIT并非只是根据方法调用次数进行编译

public static void main(String... args){
int sum = 0;
for (int i=0;i<100000;i++){
int r = sum/2;
int k = r+3;
sum = 5+6/4*2-9 + k + sum;
}
System.out.println(sum);
}

即便解释器通过循环体的回边探测到这是一个热点方法,并对该方法进行了编译,但是main函数只执行一次,被编译后的main方法根本没有执行的机会。为了解决这个问题,需要一种机制:在运行main函数的过程中(而非运行main函数后)使用编译后的方法替代当前执行,这样的机制被称为OSR。OSR用于方法从低层次(解释器)执行向高层次(JIT编译器)执行变换。发生OSR的时机是遇到回边字节码,而回边又是在branch中体现的,如代码清单5-23所示。代码清单5-23 branch实现

void TemplateTable::branch(bool is_jsr, bool is_wide) {
...
if (UseLoopCounter) {
// rax: MethodData
// rbx: MDO bumped taken-count
// rcx: method
// rdx: target offset
// r13: target bcp
// r14: locals pointer
// 检查跳转相对于当前字节码是前向还是后向
__ testl(rdx, rdx);
// 如果是回边跳转就执行下面的计数操作,否则直接跳到dispatch处
__ jcc(Assembler::positive, dispatch);
...
if (TieredCompilation) {
...
// 增加回边计数
__movptr(rcx,Address(rcx, Method::method_counters_offset()));
// 检查回边计数是否到达一定的值,如果是就跳转
__ increment_mask_and_jump(Address(rcx, be_offset), increment,
mask,rax, false, Assembler::zero,
UseOnStackReplacement ? &backedge_counter_overflow : NULL);
} else {
...}
__ bind(dispatch); // [!]正常跳转到目标字节码,然后执行
}
// 加载跳转目标
__ load_unsigned_byte(rbx, Address(rbcp, 0));
// 从目标字节码处开始执行
__ dispatch_only(vtos, true);
if (UseLoopCounter) {
...
// 如果开启栈上替换机制
if (UseOnStackReplacement) {
// 回边计数到达一定值,调用frequency_counter_overflow以通知编译器
__ bind(backedge_counter_overflow);
__ negptr(rdx);
__ addptr(rdx, rbcp);
__ call_VM(..InterpreterRuntime::frequency_counter_overflow);
// rax存放编译结果,为NULL则表示没有编译,否则表示进行了编译,且这次编译的
// 方法即osr nmethod
// 如果进行了编译,则继续执行,否则跳转到dispatch处
__ testptr(rax, rax);
__ jcc(Assembler::zero, dispatch);
// 确保osr nmethod是执行的方法(in_use表示正常方法, not_used表示不可重
//入但可以复活的方法,not_installed表示还在编译,not_entrant表示即将退
// 优化,zombie表示可以被gc,unloaded表示即将转换为zomibe)
__cmpb(Address(rax,nmethod::state_offset()),nmethod::in_use);__ jcc(Assembler::notEqual, dispatch);
// 到这里说明osr nmethod存在且可以使用,即将进行栈上替换(OSR)
__ mov(rbx, rax);
NOT_LP64(__ get_thread(rcx));
// [!]调用OSR_migration_begin将当前解释器栈的数据打包成OSR buffer
call_VM(...SharedRuntime::OSR_migration_begin);
// 将OSR buffer地址放入rax寄存器
LP64_ONLY(__ mov(j_rarg0, rax));
const Register retaddr = LP64_ONLY(j_rarg2) NOT_LP64(rdi);
const Register sender_sp = LP64_ONLY(j_rarg1) NOT_LP64(rdx);
// [!]弹出当前解释器栈
__movptr(sender_sp,Address(rbp, frame::interpreter_frame_sender_sp_offset * wordSize));
__ leave();
__ pop(retaddr);
__ mov(rsp, sender_sp);
__ andptr(rsp, -(StackAlignmentInBytes));
__ push(retaddr);
// [!] 跳到OSR入口,执行编译后的方法。这样一来就成功完成了从低层次执行
// 到高层次执行的转换
__ jmp(Address(rbx, nmethod::osr_entry_point_offset()));
}
}
}

branch会检查目标字节码的位置。如果它位于当前字节码下面(前向跳转),那么不做任何处理,直接跳到dispatch处,加载目标字节码然后调用dispatch_only执行相应代码即可。

如果目标字节码位于当前字节码前面(回边跳转),情况就复杂很多了。假设main方法里面有一个执行10000次的循环,0~5000次时解释执行,5001~10000次时执行编译后的代码,每隔1000次会调用1次
InterpreterRuntime::frequency_counter_overflow。解释器每次执行循环(branch)时都会递增回边计数器,当执行4000次时,回边计数器到达阈值,调用frequency_counter_overflow方法,该方法通知编译器决定是否编译。假设编译器决定异步编译循环,则该函数返回NULL(因为编译过程不是一蹴而就,需要很长时间,frequency_counter_overflow默认不会阻塞,而是等待编译完成),从4001次开始解释器继续解释执行。

在执行到5000次时,回边计数器又到达阈值,


frequency_counter_overflow返回也不为NULL,说明编译完成。此时解释器调用InterpreterRuntime::OSR_migration_begin将解释器栈的局部变量表和基本对象锁打包成一个OSR buffer数组放到堆上并弹出当前的解释器方法栈,最后跳转到已编译的方法入口,从5001次开始执行编译后的机器代码。编译后的机器代码被称为OSR nmethod,它将OSR buffer中的数据拆包,将局部变量和基本对象锁放入合适的寄存器和栈上的槽,再跳转到与目标字节码对应的编译后的代码的位置执行。值得注意的是,解释器的状态除了局部变量和基本对象锁外还应该包括表达式栈,但是为了简单起见,OSR(栈上替换)不会处理非空表达式栈,当JIT编译器发现表达式栈非空时会“拒绝”编译。

9. returnreturn

终止方法执行并返回调用者栈帧,如代码清单5-24所示。

代码清单5-24 字节码return

void TemplateTable::_return(TosState state) {
transition(state, state);
// 如果方法重写了Object.finalize(),会额外调用register_finalizer
if (_desc->bytecode() == Bytecodes::_return_register_finalizer) {
...
__ call_VM(...InterpreterRuntime::register_finalizer);
__ bind(skip_register_finalizer);
}
// 检查是否需要进入安全点
if(SafepointMechanism::uses_thread_local_poll()&& _desc->bytecode() != Bytecodes::_return_register_finalizer) {
...
__ call_VM(...InterpreterRuntime::at_safepoint);
__ pop(state);
__ bind(no_safepoint);
}
...
// 弹出当前栈帧
__ remove_activation(state, rbcp);
__ jmp(rbcp);
}

第2章提到过,如果重写了Object.finalize()方法,return字节码也会重写成非标准字节码
_return_register_finalizer,当解释器发现它是重写版本后,会调用InterpreterRuntime::register_finalizer,将对象加入一个链表,等待后面垃圾回收器调用链表中的对象的finalize()方法。处理完finalize重写后,return还会检查是否允许线程局部轮询(区别于全局安全点轮询,详见第10章),并调用InterpreterRuntime::at_safepoint检查是否允许进入安全点。

10. putstatic/putfield

putfield将一个值存放到对象成员字段,putstatic将一个值存放到类的static字段。熟悉Java的读者都知道Java有一个volatile关键字,作为最弱“同步组件”,它具有如下三个特性。

  • 原子性:读写volatile修饰的变量都是原子性的。相对地,对于非volatile修饰的变量,如long、double类型,是否为原子性由实现定义(Implementation-specific)。
  • 可见性:多线程访问变量时,一个线程如果修改了它的值,其他线程能立刻看到最新值。
  • 有序性:volatile写操作不能和volatile写操作/读操作发生重排序,但是可以和普通变量读写发生重排序;volatile读操作不能与任何操作发生重排序。

这些特性都是经过HotSpot VM源码验证过的。putstatic/putfield源码如代码清单5-25所示。

代码清单5-25 字节码putstatic/putfield

void TemplateTable::putfield_or_static(...) {
...
// 检查成员是否有volatile关键字
__ movl(rdx, flags);
__ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift);
__ andl(rdx, 0x1);
__ testl(rdx, rdx);
__ jcc(Assembler::zero, notVolatile);
// 如果是volatile变量,先正常赋值给成员变量再插入内存屏障
putfield_or_static_helper(...);
volatile_barrier(...Assembler::StoreLoad|Assembler::StoreStore);
__ jmp(Done);
// 如果不是volatile成员变量,简单赋值即可
__ bind(notVolatile);
putfield_or_static_helper(...);
__ bind(Done);
}

原子性来自于putfield_or_static_helper(),该函数会判断成员变量类型,然后调用access_store_at(),将值写入成员变量。access_store_at会进一步调用
BarrierSetAssembler::store_at,如代码清单5-26所示。

代码清单5-26
BarrierSetAssembler::store_at

void BarrierSetAssembler::store_at(...) {
...switch (type) {
case T_LONG:
#ifdef _LP64
__ movq(dst, rax);
#else
if (atomic) {
__ push(rdx);
__ push(rax); // 必须用FIST进行原子性更新
__ fild_d(Address(rsp,0)); // 先加载进FPU寄存器
__ fistp_d(dst); // 放入内存
__ addptr(rsp, 2*wordSize);
} else {
__ movptr(dst, rax);
__ movptr(dst.plus_disp(wordSize), rdx);
}
#endif
break;
...
}
}

以上代码均为x64架构,如果BarrierSetAssembler发现变量类型为long且是64位CPU,它会直接使用原生具有原子性的mov操作,如果是32位CPU则使用fild将值放入FPU栈,然后使用fistp读取FPU栈顶元素(ST(0))并存放到目标位置。volatile的原子性是指写64位值的原子性(fistp),而不是赋值这一过程的原子性(五条指令)。volatile的可见性和一致性是通过volatile屏障实现的。普通变量和volatile变量写之间的唯一区别是volatile写完会插入一个volatile_barrier,由volatile_barrier实际执行membar内存屏障指令。该内存屏障可以保证volatile写完后,后续的读写指令都可以看到volatile的最新值。虽说是内存屏障,但是虚拟机未必会真的使用指令集中的内存屏障指令。一个典型例子是,x86上lock指令前缀具有内存屏障的效果同时又比内存屏障指令(mfence、sfence、lfence)速度更快,因此在x86上,HotSpot VM是使用代码清单5-27所示的lock addl $0, 0($rsp)指令实现内存屏障的。

代码清单5-27 内存屏障在x86的实现

void membar(Membar_mask_bits order_constraint) {
// x86只会发生StoreLoad重排序,因此只需要处理它
if (order_constraint & StoreLoad) {
...
// 用lock add 实现内存屏障
lock();addl(Address(rsp, offset), 0);
}
}

实际上HotSpot VM只在指令缓存刷新(ICache)组件时使用内存屏障指令mfence。

11. invokestatic

JVM有五条字节码用于方法调用:invokedynamic调用动态计算的调用点;invokeinterface调用接口方法;invokespecial调用实例方法和类构造函数;invokestatic调用静态方法;invokevirtual调用虚函数。以invokestatic为例,如代码清单5-28所示。

代码清单5-28 字节码invokestatic

void TemplateTable::invokestatic(int byte_no) {
transition(vtos, vtos);
prepare_invoke(byte_no, rbx); // 准备调用
__ profile_call(rax);
__ profile_arguments_type(rax, rbx, rbcp, false);
__ jump_from_interpreted(rbx, rax); // 进行调用
}

prepare_invoke从缓存中加载调用的方法,然后跳到方法入口进行执行。

非标准字节码

除了实现Java虚拟机规范规定的字节码外,模板解释器还实现了一些非标准字节码,它们都在
interpterer/templateTable.cpp中定义。这些非标准字节码多是对标准字节码的特化,以加速程序运行,例如第2章提到的根据switch语句的case个数将lookupswitch字节码重写为fast_linearswitch(快速线性搜索)或者fast_binaryswitch(快速二分搜索),这两个字节码就是非标准字节码;本章前面提到,如果类重写了Object.finalize()方法,方法的*return字节码会被重写为return_register_finalizer,该字节码也属于非标准字节码。本节将继续讨论一些非标准字节码的实现。

1. fast_iload2

有时候代码会连续使用iload从局部变量表将变量加载到操作数栈,最典型的例子为定义两个变量,然后相加,如代码清单5-29所示。

代码清单5-29 连续使用iload然后求和

int a = 23; // iload 23
int b = 34; // iload 34
a + b // iadd

上面的字节码将派发三次,每次都要执行前奏曲代码、字节码实现代码、尾曲代码,所以HotSpot VM根据这样的模式,使用非标准字节码fast_iload一次完成两个局部变量的相加,省去了派发流程,如代码清单5-30所示。

代码清单5-30 非标准字节码fast_iload2

void TemplateTable::fast_iload2() {
transition(vtos, itos);
locals_index(rbx);__ movl(rax, iaddress(rbx));
__ push(itos);
locals_index(rbx, 3);
__ movl(rax, iaddress(rbx));
}

2. fast_iputfield

前面讨论的putfield字节码的实现较为复杂,因为需要解析字段,检查字段的修饰符。当第一次完成上述操作后,虚拟机会将标准字节码putfield重写为fast版本,这样在第二次执行时将不需要任何额外的操作,如代码清单5-31所示。

代码清单5-31 非标准字节码fast_iputfield

void TemplateTable::fast_storefield_helper(...) {
switch (bytecode()) {
case Bytecodes::_fast_iputfield:
__ access_store_at(T_INT, IN_HEAP, field, rax, noreg, noreg);
break;
...
}

从代码中可以发现,非标准的putfield没有做任何多余的操作。

本文给大家讲解的内容是详解探讨模板解释器,解释器的生成

  1. 下篇文章给大家讲解的是讨论虚拟机在并发方面付出的努力;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 知识点总结

    ...

    2024/4/27 14:38:40
  2. GIS开发常用的开源地图数据库介绍

    通常GIS二次开发会用到很多开源的JavaScript数据库&#xff0c;本篇文章再给大家主要讲讲针对WebGIS开发的地图和可视化数据库。 Echarts ​ ECharts是一个使用 JavaScript 实现的开源可视化库。它可以流畅的运行在 PC 和移动设备上&#xff0c;兼容当前绝大部分浏览器&#xf…...

    2024/4/16 18:13:58
  3. 最简单的树莓派交叉编译环境搭建方法

    写在前面 对于小白&#xff0c;想编译树莓派内核时&#xff0c;会全网搜搭建树莓派交叉编译环境的方法&#xff0c;各种博客也讲的挺清楚的&#xff0c;不过都太麻烦。 最简单的树莓派交叉编译环境搭建方法 参考官方文档&#xff1a;https://www.raspberrypi.com/documentati…...

    2024/4/18 12:02:49
  4. 采购人电子化政采平台的操作权限可供代理机构使用吗?

    搭上互联网快车的政府采购&#xff0c;有关各方都得到了实惠&#xff0c;采购效率大大提高、工作量也大大减小。但电子化带来的操作模式的改变&#xff0c;也让一些从业人员不适应&#xff0c;近期就出现采购单位的从业人员因为不适应操作&#xff0c;而把自己的账户信息交给采…...

    2024/4/16 18:14:16
  5. VS Code 2022路线图:大量Spring Boot优化提上日程

    1月20日&#xff0c;一名微软开发者发布了一篇标题为《Java on Visual Studio Code Update》的文章。 文中介绍了VS Code 2021年的亮点&#xff0c;同时还透露了VS Code 2022年的最新路线图。 从上图中可以看到&#xff0c;2022年的路线规划主要有6个方面&#xff0c;包括&…...

    2024/4/24 16:44:56
  6. 使用ajax图片上传页面开发并回显

    1&#xff0c;需引入ajaxfileupload.js 2&#xff0c;HTML代码 <tr><td class"three">图片介绍</td><td> <br><div id"imgDiv" style"display:block; width: 40px; height: 50px;"></div><br>…...

    2024/4/27 14:59:42
  7. springboot上传文件并预览

    1&#xff0c;原始代码&#xff0c;创建临时路径&#xff0c;获取链接服务端访问。 弊端&#xff0c;占用服务器空间 import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframew…...

    2024/4/14 6:40:12
  8. Android性能优化–Systrace工具,kotlin参数默认值

    获取报告 无时间限制,需要enter键开始 ./systrace.py -o trace.html sched freq idle am wm gfx view binder_driver hal dalvik camera input res 加时间参数 ./systrace.py -o trace.html -t 12 sched freq idle am wm gfx view binder_driver hal dalvik camera input r…...

    2024/4/15 14:38:51
  9. c语言小游戏--三子棋

    一.问题描述 二.步骤 1.菜单界面 2.初始化棋盘 3.打印棋盘 4.玩家走 5.电脑走 6.判断输赢 三.结果演示 1.玩家赢 2.电脑赢 3.平局 四.完整代码 目录 一.问题描述 二.步骤 1.菜单界面 2.初始化棋盘 3.打印棋盘 4.玩家走 5.电脑走 6.判断输赢 三.结果演示…...

    2024/4/19 16:21:11
  10. 京东T9架构师到底整理出了什样的JAVA面试文档,拿到这么多offer

    前言 金九银十只剩银十&#xff0c;网上的JAVA面试文档更是层出不穷。 但是单单刷JAVA面试题就足够了吗&#xff1f;答案显然是不够的&#xff01;那么为什么呢&#xff1f; 因为现在的程序员就业环境早就和两年前不可同日而语了。 如果你在两年前面试&#xff1a; 就拿JVM…...

    2024/4/14 6:41:08
  11. 如果连Redis分布式锁的这些坑都不知道,我怀疑你是假的开发

    摘要&#xff1a; 用锁遇到过哪些问题&#xff1f; 一、白话分布式 什么是分布式&#xff0c;用最简单的话来说&#xff0c;就是为了降低单个服务器的压力&#xff0c;将功能分布在不同的机器上面&#xff1b;就比如&#xff1a; 本来一个程序员可以完成一个项目&#xff1a…...

    2024/4/14 6:46:28
  12. 男方瘫痪女方可不可有要求离婚

    夫妻双方有互相扶养的义务&#xff0c;一方有疾病另一方尽相应的照料义务&#xff0c;但双方感情破裂一方坚持离婚的&#xff0c;还是离得了的。男方瘫痪当然可以离婚&#xff0c;只不过是需要看另一方人是怎么想的&#xff0c;若是真的不管不顾要离婚&#xff0c;那想也是没有…...

    2024/4/14 6:40:22
  13. mongoDB之ObjectId

    mongoDB之ObjectId 一、ObjectId的介绍 我们再使用mongodb插入数据时&#xff0c;每个document中必须有一个_id字段&#xff0c;且可以是任意类型&#xff0c;但如果没有单独设置_id字段&#xff0c;mongo会自动生成一个_id字段&#xff0c;类型是ObjectId&#xff0c;查看数据…...

    2024/4/16 14:52:20
  14. OpenCV 计算fps(frames per second-fps)

    在OpenCV中&#xff0c;类videoccapture处理摄像头读取视频和抓取帧。通过使用videoccapture中的get(PROPERTY_NAME)方法&#xff0c;你可以找到很多关于你正在播放的视频文件的信息。 如何在OpenCV中找到一个摄像头/网络摄像头的帧率? 在OpenCV中&#xff0c;查找连接的摄像…...

    2024/4/19 17:40:24
  15. 怎么监控线程池,才能帮助开发者快速定位线上错误?

    大部分情况下&#xff0c;线程池的运行情况对于使用者来说是个黑盒。 运行情况不可知&#xff0c;会导致生产出现事故问题排查困难&#xff0c;以及线程池参数难以定义 文章围绕线程池监控展开&#xff0c;讨论线程池如何监控、监控的指标以及监控数据的存储展示。 1. 如何监…...

    2024/4/7 16:30:50
  16. java多线程之生命周期

    java多线程之生命周期1.线程的创建1.1继承Thread1.2实现runnable接口1.3 实现Callable接口2.线程就绪3.线程运行4.线程阻塞4.1 sleep&#xff08;&#xff09;4.2 join&#xff08;&#xff09;4.3 yield()4.4 wait()5.线程死亡1.线程的创建 1.1继承Thread public class Thre…...

    2024/4/14 6:39:57
  17. PHP直播源码支付回调的使用

    PHP直播源码在支付充值方面&#xff0c;对支付宝的依赖还蛮大的&#xff0c;伴随支付宝支付的使用&#xff0c;在PHP直播源码中加入支付宝支付和回调变得越来越平常&#xff0c;下面就给各位分析一下&#xff0c;如何实现PHP直播源码支付回调的使用。 一、支付宝的接入及配置 1…...

    2024/4/18 16:48:28
  18. 前端常用的跨域方案

    目录 前言 跨域 解决跨域方案 一.修改本地HOST: 二.JSONP 三.CORS 四 proxy 前言 发送请求XMLHttpRequest(ajax, $ajax, axois),fetch http://192.168.0.103:8080/ file:///F://vue阶段/vue工程化/_systerm/public/index.html:file协议不允许发送ajax请求 平时项目中…...

    2024/4/14 15:34:52
  19. 肠漏检测相关知识

    最先提出 肠漏这个概念&#xff0c;最早是美国著名自然医学专家乔希阿克斯博士&#xff08;Dr. Josh Axe&#xff09;提出的。 测试方法 一是用乳果糖/甘露醇尿液测试。这种测试会让患者先摄入乳果糖和甘露醇&#xff0c;然后再在之后的几小时里收集尿液&#xff1b;最后&…...

    2024/4/7 16:30:45
  20. 【es】es修改字段text类型为date类型

    一、概述 首先说明es中索引的字段类型是不可修改的&#xff0c;只能是重新创建一个索引并设置好映射mapping&#xff0c;然后再将老索引的数据复制过去。 二、原索引字段text类型 三、操作步骤 创建索引&#xff0c;并指定映射mapping PUT /audit2 {"mappings": {…...

    2024/4/27 1:23:51

最新文章

  1. 惊!文件夹突变文件,揭秘背后原因及数据恢复秘籍

    在使用电脑时&#xff0c;我们有时会遇到一些意想不到的问题。比如&#xff0c;你可能会突然发现&#xff0c;原本存储着大量重要资料的文件夹&#xff0c;竟然变成了一个无法打开的文件。这种情况听起来可能有些匪夷所思&#xff0c;但它确实存在&#xff0c;且给用户带来了巨…...

    2024/4/27 16:29:18
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. WPS二次开发专题:如何获取应用签名SHA256值

    作者持续关注WPS二次开发专题系列&#xff0c;持续为大家带来更多有价值的WPS开发技术细节&#xff0c;如果能够帮助到您&#xff0c;请帮忙来个一键三连&#xff0c;更多问题请联系我&#xff08;QQ:250325397&#xff09; 在申请WPS SDK授权版时候需要开发者提供应用包名和签…...

    2024/4/23 6:15:54
  4. 解决npm install安装node-sass包容易失败的问题

    具体问题如下&#xff1a; npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: XXX3.4.0 npm ERR! Found: webpack5.31.2 npm ERR! node_modules/webpack npm ERR! peer webpack”^4.0.0 || ^5.0.0″ from html-…...

    2024/4/26 14:55:57
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/26 18:09:39
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/26 20:12:18
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/26 23:05:52
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/27 4:00:35
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/25 18:39:22
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/26 21:56:58
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/27 9:01:45
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/26 16:00:35
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/25 18:39:16
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/26 22:01:59
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/25 18:39:14
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/26 23:04:58
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/25 2:10:52
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/25 18:39:00
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/26 19:46:12
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/27 11:43:08
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/27 8:32:30
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57