预定义变量 $_GLOBALS 与 global 关键字有何异同?
解读
面试官抛出此题,并非单纯考察“能不能写对”,而是想确认候选人是否真正理解 PHP 变量作用域的底层机制、符号表(symbol table)与内存模型。国内一线互联网、电商、SaaS 公司的高 P 面试中,这道题常被用作“作用域”环节的“门槛题”:答得浅,会被追问“ZEND 层面怎么实现”;答得深,可顺势展示扩展开发、性能调优、OPcache 优化经验。因此,回答必须兼顾“语法层面正确”与“Zend VM 层面可信”。
知识点
- 作用域(scope):PHP 只有函数/方法级局部作用域与全局作用域,没有块级作用域。
- 符号表:全局符号表保存在 EG(symbol_table) 哈希表,局部符号表保存在 EG(active_symbol_table) 指针。
- $_GLOBALS:PHP 超全局数组,内部通过 zend_builtin_module 注册,EG(symbol_table) 的“镜像”,每次访问都走 HASH 查找,生命周期与请求绑定。
- global:语法关键字,编译阶段生成 ZEND_FETCH_W/ZEND_FETCH_R 指令,运行期把 EG(symbol_table) 中对应变量“映射”到局部符号表,本质是“引用绑定”(IS_CV → IS_VAR 引用)。
- 性能差异:$_GLOBALS 每次访问都要 hash 查找,OPcache 无法优化;global 变量在编译期已确定偏移,JIT/GVN 可做引用传递优化,高并发场景差距可达 5%~8%。
- 安全差异:_GLOBALS['xxx']),导致全局符号表直接丢失;global 只影响当前作用域引用,不会销毁原变量。
- 闭包差异:Closure 用 use 或 global 都无法自动带入超全局;但 Closure 内部可通过 $_GLOBALS 直接读写,无需 use 声明。
- 扩展开发:扩展中如需暴露全局变量,推荐通过 ZEND_API 注册到 &EG(symbol_table),而不是手动操作 &GLOBALS;否则与 OPcache 共享内存冲突,CLI 模式正常,FPM 下出现 segfault。
答案
相同点
- 都能让局部作用域访问/修改全局变量。
- 都作用于 EG(symbol_table),最终指向同一块内存。
不同点
- 类型:$_GLOBALS 是语言内置的超全局数组;global 是语法关键字。
- 生命周期:$_GLOBALS 随请求创建/销毁,数组本身不可销毁;global 只是编译期建立的引用映射,函数返回后引用自动失效。
- 性能:$_GLOBALS 每次读写都触发 hash 查找,OPcache 无法缓存;global 在编译期已确定变量名,运行期直接引用,JIT 可优化。
- 可写性:可 unset(local) 仅断开引用,不会删除全局变量。
- 闭包捕获:Closure 内部无需 use 即可访问 $_GLOBALS;global 变量必须在闭包内重新 global 声明或使用 use 引入。
- 扩展与内核:扩展注册全局变量应操作 EG(symbol_table),而不是假设 $_GLOBALS 一定存在(某些嵌入式 SAPI 可能关闭 register_globals)。
一句话总结:$_GLOBALS 是“全局符号表的可读写镜像”,global 是“把全局变量映射为局部引用的快捷方式”;前者灵活但略慢,后者高效但只能按名映射。
拓展思考
- 在高并发电商秒杀接口中,若需要缓存“全局库存计数器”,用 stock?
答:两者都不合适;应使用 APCu/Opcache 共享内存或 Redis,避免 PHP 请求级内存无法跨进程共享。 - 开启 OPcache 后,为什么 _GLOBALS 属于可变数组,OPcache 无法将其键值对映射为静态内存;global 变量名在编译期已固化,可被 JIT/GVN 优化为直接内存偏移。
- 如果写扩展,想让 PHP 代码通过 $MYEXT->foo 访问 C 层全局变量,最佳实践是什么?
答:在 RINIT 阶段通过 zend_hash_str_update(&EG(symbol_table), "MYEXT", 5, &zv) 注册对象,而不是操作 &GLOBALS;否则在 ZTS 模式下会出现线程竞争。