Java的模板解析执行需要模板表与转发表的支持,而这2个表中的数据在HotSpot虚拟机启动时就会初始化。这一篇首先介绍模板表。

在启动虚拟机阶段会调用init_globals()方法初始化全局模块,在这个方法中通过调用interpreter_init()方法初始化模板解释器,调用栈如下:

TemplateInterpreter::initialize()    templateInterpreter.cpp
interpreter_init() interpreter.cpp
init_globals() init.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c
start_thread() pthread_create.c

interpreter_init()方法主要是通过调用TemplateInterpreter::initialize()方法来完成逻辑,initialize()方法的实现如下:

源代码位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateInterpreter::initialize() {
if (_code != NULL)
return; // 抽象解释器AbstractInterpreter的初始化,AbstractInterpreter是基于汇编模型的解释器的共同基类,
// 定义了解释器和解释器生成器的抽象接口
AbstractInterpreter::initialize(); // 模板表TemplateTable的初始化,模板表TemplateTable保存了各个字节码的模板
TemplateTable::initialize(); // generate interpreter
{
ResourceMark rm;
int code_size = InterpreterCodeSize;
// CodeCache的Stub队列StubQueue的初始化
_code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,"Interpreter");
// 实例化模板解释器生成器对象TemplateInterpreterGenerator
InterpreterGenerator g(_code);
} // initialize dispatch table
_active_table = _normal_table;
}

模板解释器的初始化包括如下几个方面:

(1)抽象解释器AbstractInterpreter的初始化,AbstractInterpreter是基于汇编模型的解释器的共同基类,定义了解释器和解释器生成器的抽象接口。

(2)模板表TemplateTable的初始化,模板表TemplateTable保存了各个字节码的模板(目标代码生成函数和参数);

(3)CodeCache的Stub队列StubQueue的初始化;

(4)解释器生成器InterpreterGenerator的初始化。

在之前介绍过,在TemplateInterpreter::initialize() 中通过调用语句来间接调用generate_method_entry()和generate_normal_entry()创建方法执行的栈帧:

InterpreterGenerator g(_code);

不过在如上语句调用之前,首先需要调用TemplateInterpreter类中的initialize()方法初始化模板表,如下:

TemplateTable::initialize();

模板表TemplateTable保存了各个字节码的模板(目标代码生成函数和参数),initialize()方法的实现如下:

源代码位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateTable::initialize() {
if (_is_initialized) return; _bs = Universe::heap()->barrier_set(); // For better readability
const char _ = ' ';
const int ____ = 0;
const int ubcp = 1 << Template::uses_bcp_bit;
const int disp = 1 << Template::does_dispatch_bit;
const int clvm = 1 << Template::calls_vm_bit;
const int iswd = 1 << Template::wide_bit;
// interpr. templates
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument
def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ );
def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ );
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 );
def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0 );
// ...
def(Bytecodes::_tableswitch , ubcp|disp|____|____, itos, vtos, tableswitch , _ );
def(Bytecodes::_lookupswitch , ubcp|disp|____|____, itos, itos, lookupswitch , _ );
def(Bytecodes::_ireturn , ____|disp|clvm|____, itos, itos, _return , itos );
def(Bytecodes::_lreturn , ____|disp|clvm|____, ltos, ltos, _return , ltos );
def(Bytecodes::_freturn , ____|disp|clvm|____, ftos, ftos, _return , ftos );
def(Bytecodes::_dreturn , ____|disp|clvm|____, dtos, dtos, _return , dtos );
def(Bytecodes::_areturn , ____|disp|clvm|____, atos, atos, _return , atos );
def(Bytecodes::_return , ____|disp|clvm|____, vtos, vtos, _return , vtos );
def(Bytecodes::_getstatic , ubcp|____|clvm|____, vtos, vtos, getstatic , f1_byte );
def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte );
def(Bytecodes::_getfield , ubcp|____|clvm|____, vtos, vtos, getfield , f1_byte );
def(Bytecodes::_putfield , ubcp|____|clvm|____, vtos, vtos, putfield , f2_byte );
def(Bytecodes::_invokevirtual , ubcp|disp|clvm|____, vtos, vtos, invokevirtual , f2_byte );
def(Bytecodes::_invokespecial , ubcp|disp|clvm|____, vtos, vtos, invokespecial , f1_byte );
def(Bytecodes::_invokestatic , ubcp|disp|clvm|____, vtos, vtos, invokestatic , f1_byte );
def(Bytecodes::_invokeinterface , ubcp|disp|clvm|____, vtos, vtos, invokeinterface , f1_byte );
def(Bytecodes::_invokedynamic , ubcp|disp|clvm|____, vtos, vtos, invokedynamic , f1_byte );
def(Bytecodes::_new , ubcp|____|clvm|____, vtos, atos, _new , _ );
def(Bytecodes::_newarray , ubcp|____|clvm|____, itos, atos, newarray , _ );
def(Bytecodes::_anewarray , ubcp|____|clvm|____, itos, atos, anewarray , _ );
def(Bytecodes::_arraylength , ____|____|____|____, atos, itos, arraylength , _ );
def(Bytecodes::_athrow , ____|disp|____|____, atos, vtos, athrow , _ );
def(Bytecodes::_checkcast , ubcp|____|clvm|____, atos, atos, checkcast , _ );
def(Bytecodes::_instanceof , ubcp|____|clvm|____, atos, itos, instanceof , _ );
def(Bytecodes::_monitorenter , ____|disp|clvm|____, atos, vtos, monitorenter , _ );
def(Bytecodes::_monitorexit , ____|____|clvm|____, atos, vtos, monitorexit , _ );
def(Bytecodes::_wide , ubcp|disp|____|____, vtos, vtos, wide , _ );
def(Bytecodes::_multianewarray , ubcp|____|clvm|____, vtos, atos, multianewarray , _ );
def(Bytecodes::_ifnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , equal );
def(Bytecodes::_ifnonnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , not_equal );
def(Bytecodes::_goto_w , ubcp|____|clvm|____, vtos, vtos, goto_w , _ );
def(Bytecodes::_jsr_w , ubcp|____|____|____, vtos, vtos, jsr_w , _ ); // wide Java spec bytecodes
def(Bytecodes::_iload , ubcp|____|____|iswd, vtos, itos, wide_iload , _ );
def(Bytecodes::_lload , ubcp|____|____|iswd, vtos, ltos, wide_lload , _ );
// ... // JVM bytecodes
def(Bytecodes::_fast_agetfield , ubcp|____|____|____, atos, atos, fast_accessfield , atos );
def(Bytecodes::_fast_bgetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );
def(Bytecodes::_fast_cgetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );
def(Bytecodes::_fast_dgetfield , ubcp|____|____|____, atos, dtos, fast_accessfield , dtos );
def(Bytecodes::_fast_fgetfield , ubcp|____|____|____, atos, ftos, fast_accessfield , ftos );
def(Bytecodes::_fast_igetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );
def(Bytecodes::_fast_lgetfield , ubcp|____|____|____, atos, ltos, fast_accessfield , ltos );
def(Bytecodes::_fast_sgetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos ); def(Bytecodes::_fast_aputfield , ubcp|____|____|____, atos, vtos, fast_storefield , atos );
def(Bytecodes::_fast_bputfield , ubcp|____|____|____, itos, vtos, fast_storefield , itos );
def(Bytecodes::_fast_cputfield , ubcp|____|____|____, itos, vtos, fast_storefield , itos );
def(Bytecodes::_fast_dputfield , ubcp|____|____|____, dtos, vtos, fast_storefield , dtos );
def(Bytecodes::_fast_fputfield , ubcp|____|____|____, ftos, vtos, fast_storefield , ftos );
def(Bytecodes::_fast_iputfield , ubcp|____|____|____, itos, vtos, fast_storefield , itos );
def(Bytecodes::_fast_lputfield , ubcp|____|____|____, ltos, vtos, fast_storefield , ltos );
def(Bytecodes::_fast_sputfield , ubcp|____|____|____, itos, vtos, fast_storefield , itos ); def(Bytecodes::_fast_aload_0 , ____|____|____|____, vtos, atos, aload , 0 );
def(Bytecodes::_fast_iaccess_0 , ubcp|____|____|____, vtos, itos, fast_xaccess , itos );
def(Bytecodes::_fast_aaccess_0 , ubcp|____|____|____, vtos, atos, fast_xaccess , atos );
def(Bytecodes::_fast_faccess_0 , ubcp|____|____|____, vtos, ftos, fast_xaccess , ftos ); def(Bytecodes::_fast_iload , ubcp|____|____|____, vtos, itos, fast_iload , _ );
def(Bytecodes::_fast_iload2 , ubcp|____|____|____, vtos, itos, fast_iload2 , _ );
def(Bytecodes::_fast_icaload , ubcp|____|____|____, vtos, itos, fast_icaload , _ ); def(Bytecodes::_fast_invokevfinal , ubcp|disp|clvm|____, vtos, vtos, fast_invokevfinal , f2_byte ); def(Bytecodes::_fast_linearswitch , ubcp|disp|____|____, itos, vtos, fast_linearswitch , _ );
def(Bytecodes::_fast_binaryswitch , ubcp|disp|____|____, itos, vtos, fast_binaryswitch , _ ); def(Bytecodes::_fast_aldc , ubcp|____|clvm|____, vtos, atos, fast_aldc , false );
def(Bytecodes::_fast_aldc_w , ubcp|____|clvm|____, vtos, atos, fast_aldc , true ); def(Bytecodes::_return_register_finalizer , ____|disp|clvm|____, vtos, vtos, _return , vtos ); def(Bytecodes::_invokehandle , ubcp|disp|clvm|____, vtos, vtos, invokehandle , f1_byte ); def(Bytecodes::_shouldnotreachhere , ____|____|____|____, vtos, vtos, shouldnotreachhere , _ );
// platform specific bytecodes
pd_initialize(); _is_initialized = true;
}

TemplateTable的初始化调用def()将所有字节码的目标代码生成函数和参数保存在_template_table或_template_table_wide(wide指令)模板数组中。除了虚拟机规范本身定义的字节码指令外,HotSpot虚拟机也定义了一些字节码指令,这些指令为了辅助虚拟机进行更好、理简单的功能实现,例如Bytecodes::_return_register_finalizer等在之前已经介绍过,可以更好的实现finalizer类型对象的注册功能。

对于调用def()函数时传递的一些参数在后面会解释。def()函数有2个,接收的参数不同,实现如下:

void TemplateTable::def(
Bytecodes::Code code, // 字节码指令
int flags, // 标志位
TosState in, // 模板执行前TosState
TosState out, // 模板执行后TosState
void (*gen)(), // 模板生成器,是模板的核心组件
char filler
) {
assert(filler == ' ', "just checkin'");
def(code, flags, in, out, (Template::generator)gen, 0); // 调用下面的def()函数
} void TemplateTable::def(
Bytecodes::Code code, // 字节码指令
int flags, // 标志位
TosState in, // 模板执行前TosState
TosState out, // 模板执行后TosState
void (*gen)(int arg), // 模板生成器,是模板的核心组件
int arg
) {
// should factor out these constants
const int ubcp = 1 << Template::uses_bcp_bit; // 表示是否需要bcp指针
const int disp = 1 << Template::does_dispatch_bit; // 表示是否在模板范围内进行转发
const int clvm = 1 << Template::calls_vm_bit; // 表示是否需要调用JVM函数
const int iswd = 1 << Template::wide_bit; // 表示是否为wild指令 // determine which table to use
bool is_wide = (flags & iswd) != 0; // make sure that wide instructions have a vtos entry point
// (since they are executed extremely rarely, it doesn't pay out to have an
// extra set of 5 dispatch tables for the wide instructions - for simplicity
// they all go with one table)
assert(in == vtos || !is_wide, "wide instructions have vtos entry point only");
Template* t = is_wide ? template_for_wide(code) : template_for(code); // setup entry
t->initialize(flags, in, out, gen, arg); // 调用模板表t的initialize()方法初始化模板表
assert(t->bytecode() == code, "just checkin'");
}

模板表由模板表数组与一组生成器组成:

(1)模板表数组有_template_table与_template_table_wild,数组的下标为bytecode,值为Template,按照字节码指令的操作码递增顺序排列。

(2)一组生成器,所有与bytecode配套的生成器,在初始化模板表时作为gen参数传给相应的Template。

Template类的定义如下:

源代码位置:hotspot/src/share/vm/interpreter/templateTable.hpp
// A Template describes the properties of a code template for a given bytecode
// and provides a generator to generate the code template. class Template VALUE_OBJ_CLASS_SPEC {
private:
enum Flags {
// 字节码指令指的是该字节码的操作数是否存在于字节码里面
uses_bcp_bit, // set if template needs the bcp pointing to bytecode
does_dispatch_bit,// set if template dispatches on its own
calls_vm_bit, // set if template calls the vm
wide_bit // set if template belongs to a wide instruction
}; typedef void (*generator)(int arg); int _flags; // describes interpreter template properties (bcp unknown)
TosState _tos_in; // tos cache state before template execution
TosState _tos_out; // tos cache state after template execution
generator _gen; // template code generator
int _arg; // argument for template code generator
... // Templates
static Template* template_for(Bytecodes::Code code) {
Bytecodes::check(code);
return &_template_table[code];
}
static Template* template_for_wide(Bytecodes::Code code) {
Bytecodes::wide_check(code);
return &_template_table_wide[code];
}
};

调用的template_for()与template_for_wild()方法从_template_table或_template_for_wild数组中取值。这2个变量定义在TemplateTable类中,如下:

static Template        _template_table     [Bytecodes::number_of_codes];
static Template _template_table_wide[Bytecodes::number_of_codes];

继续看TemplateTable::def(0函数的各个参数,解释如下:

(1)_flags:是一个标志,低四位分别表示:

  • uses_bcp_bit,标志需要使用字节码指针(byte code pointer,数值为字节码基址+字节码偏移量)
  • does_dispatch_bit,标志是否在模板范围内进行转发,如跳转类指令会设置该位
  • calls_vm_bit,标志是否需要调用JVM函数
  • wide_bit,标志是否是wide指令(使用附加字节扩展全局变量索引)

(2)_tos_in:表示模板执行前的TosState(操作数栈栈顶元素的数据类型,TopOfStack,用来检查模板所声明的输出输入类型是否和该函数一致,以确保栈顶元素被正确使用)

(3)_tos_out:表示模板执行后的TosState

(4)_gen:表示模板生成器(函数指针)

(5)_arg:表示模板生成器参数

再来看一下TemplateTable::initialize()方法中对def()函数的调用,以_iinc(将局部变量增加1)为例,调用如下:

def(
Bytecodes::_iinc, // 字节码指令
ubcp|____|clvm|____, // 标志
vtos, // 模板执行前的TosState
vtos, // 模板执行后的TosState
iinc , // 模板生成器,是一个iinc()函数的指针
_ // 不需要模板生成器参数
); 

设置标志位uses_bcp_bit和calls_vm_bit,表示iinc指令的生成器需要使用bcp指针函数at_bcp(),且需要调用JVM函数,下面给出了生成器的定义:

源代码位置:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp

void TemplateTable::iinc() {
transition(vtos, vtos);
__ load_signed_byte(rdx, at_bcp(2)); // get constant
locals_index(rbx);
__ addl(iaddress(rbx), rdx);
}

iinc指令的格式如下:

iinc
index
const

操作码iinc占用一个字节,而index与const分别占用一个字节。使用at_bcp()函数获取iinc指令的操作数,2表示偏移2字节,所以会将const取出来存储到rdx中。调用locals_index()函数取出index,locals_index()就是JVM函数。最终生成的汇编如下:

// %r13存储的是指向字节码的指针,偏移2字节后取出const存储到%edx
0x00007fffe101a210: movsbl 0x2(%r13),%edx
// 取出index存储到%ebx
0x00007fffe101a215: movzbl 0x1(%r13),%ebx
0x00007fffe101a21a: neg %rbx
// %r14指向本地变量表的首地址,将%edx加到%r14+%rbx*8指向的内存所存储的值上
// 之所以要对%rbx执行neg进行符号反转,是因为在Linux内核的操作系统上,栈是向低地址方向生长的
0x00007fffe101a21d: add %edx,(%r14,%rbx,8)

不过这里并不会调用iinc()函数生成对应的汇编代码,只是将传递给def()函数的各种信息保存到Template对象中,在TemplateTable::def()方法中,通过template_for()或template_for_wild()方法获取到数组中对应的Template对象后,就会调用Template::initialize()方法,实现如下:

void Template::initialize(int flags, TosState tos_in, TosState tos_out, generator gen, int arg) {
_flags = flags;
_tos_in = tos_in;
_tos_out = tos_out;
_gen = gen;
_arg = arg;
}

可以看到,只是将信息保存到对应的Template对象中,这样就可以根据字节码索引从数组中获取对应的Template对象,进而获取相关信息。下一篇我们将会看到对这些信息的使用。  

相关文章的链接如下:

1、在Ubuntu 16.04上编译OpenJDK8的源代码

2、调试HotSpot源代码

3、HotSpot项目结构 

4、HotSpot的启动过程

5、HotSpot二分模型(1)

6、HotSpot的类模型(2)

7、HotSpot的类模型(3)

8、HotSpot的类模型(4)

9、HotSpot的对象模型(5)

10、HotSpot的对象模型(6)

11、操作句柄Handle(7)

12、句柄Handle的释放(8)

13、类加载器

14、类的双亲委派机制

15、核心类的预装载

16、Java主类的装载

17、触发类的装载

18、类文件介绍

19、文件流

20、解析Class文件

21、常量池解析(1)

22、常量池解析(2)

23、字段解析(1)

24、字段解析之伪共享(2)

25、字段解析(3)

26、字段解析之OopMapBlock(4)

27、方法解析之Method与ConstMethod介绍

28、方法解析

29、klassVtable与klassItable类的介绍

30、计算vtable的大小

31、计算itable的大小

32、解析Class文件之创建InstanceKlass对象

33、字段解析之字段注入

34、类的连接

35、类的连接之验证

36、类的连接之重写(1)

37、类的连接之重写(2)

38、方法的连接

39、初始化vtable

40、初始化itable

41、类的初始化

42、对象的创建

43、Java引用类型

44、Java引用类型之软引用(1)

45、Java引用类型之软引用(2)

46、Java引用类型之弱引用与幻像引用

47、Java引用类型之最终引用

48、HotSpot的垃圾回收算法

49、HotSpot的垃圾回收器

50、CallStub栈帧

51、entry point栈帧

52、generate_fixed_frame()方法生成Java方法栈帧

53、dispatch_next()方法的实现

54、虚拟机执行模式

作者持续维护的个人博客  classloading.com

关注公众号,有HotSpot源码剖析系列文章!

  

JVM的方法执行引擎-模板表的更多相关文章

  1. JVM的方法执行引擎-entry point栈帧

    接着上一篇去讲,回到JavaCalls::call_helper()中: address entry_point = method->from_interpreted_entry(); entr ...

  2. JVM总结(五):JVM字节码执行引擎

    JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...

  3. 一夜搞懂 | JVM 字节码执行引擎

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习字节码执行引擎? 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一 ...

  4. 深入理解JVM—字节码执行引擎

    原文地址:http://yhjhappy234.blog.163.com/blog/static/3163283220122204355694/ 前面我们不止一次的提到,Java是一种跨平台的语言,为 ...

  5. JVM字节码执行引擎和动态绑定原理

    1.执行引擎 所有Java虚拟机的执行引擎都是一致的: 输入的是字节码文件,处理过程就是解析过程,最后输出执行结果. 在整个过程不同的数据在不同的结构中进行处理. 2.栈帧 jvm进行方法调用和方法执 ...

  6. 【JVM】JVM系列之执行引擎(五)

    一.前言 在了解了类加载的相关信息后,有必要进行更深入的学习,了解执行引擎的细节,如字节码是如何被虚拟机执行从而完成指定功能的呢.下面,我们将进行深入的分析. 二.栈帧 我们知道,在虚拟机中与执行方法 ...

  7. 图解JVM字节码执行引擎之栈帧结构

    一.执行引擎      “虚拟机”的概念是相对于“物理机”而言的,这两种“机器”都有执行代码的能力.物理机的执行引擎是直接建立在硬件处理器.物理寄存器.指令集和操作系统层面的:而“虚拟机”的执行引擎是 ...

  8. JVM字节码执行引擎

    一.概述 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择,所有的Java虚拟机的执行引擎都是一致的:输 ...

  9. JVM 专题十五:执行引擎

    1. 执行引擎概述 1.1 执行引擎 1.2 概述 执行引擎是Java虚拟机的核心组成部分之一. 虚拟机是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处 ...

随机推荐

  1. DJANGO-天天生鲜项目从0到1-014-订单-订单评论

    本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...

  2. Vue脚手架创建项目出现 (Failed to download repo vuejs-templates/webpack: Response code 404)

    搭建好(脚手架2.X版本)环境像往常一样使用vue init webpack xxxx 创建项目可以是没多久就开始报错了 报错结果就是:vue-cli · Failed to download rep ...

  3. 如何利用tox打造自动自动化测试框架,看完就懂

    什么是toxtox官方文档的第一句话 standardize testing in Python,意思就是说标准化python中的测试,那是不是很适合测试人员来使用呢,我们来看看他究竟是什么? 根据官 ...

  4. Object#wait()与Object#wait(long)的区别

    例子 例1 最基础的等待-通知 下面一个例子,一个线程"waiting"在同步代码块调用了Object#wait()方法,另一个线程"timedWaiting" ...

  5. pandas处理excel文件和csv文件

    一.csv文件 csv以纯文本形式存储表格数据 pd.read_csv('文件名'),可添加参数engine='python',encoding='gbk' 一般来说,windows系统的默认编码为g ...

  6. 手写 promies

    简单的 Promies 封装 function Promiss(fn) { this.state = 'pending' //当前状态 this.value = null // 成功执行时得到的数据 ...

  7. 星屑幻想 optimal mark

    LINK :SP839 星屑幻想 取自 OJ 的名称 小事情...题目大意还是要说的这道题比较有意思,想了一段时间. 给你一张图 这张图给答案带来的贡献是每条边上两个点值得异或 一些点的值已经被确定 ...

  8. AutoWired注解和Lazy延迟加载

    一.代码截图: @Lazy是延迟加载的意思, 容器启动时不创建对象, 当从容器中需要获取此对象时才创建. @Lazy//@Lazy注解可以用在类上, 还可以用在普通方法上,还可以用在构造方法上,还可以 ...

  9. TF签名是什么?比企业签名好在哪里?

      现在苹果企业签名的服务大致分为三类,苹果企业签名.超级签名和TF签名,而TF签名TF签名又称 TestFlight 签名,是目前最稳定的签名方式. ​   「优势」   关键词:零风险;限制少;安 ...

  10. 找工作的你不容错过的45个PHP面试题附答案(下篇)

    找工作的你不容错过的45个PHP面试题附答案(上篇) Q28:你将如何使用PHP创建Singleton类? /** * Singleton class * */ final class UserFac ...