unset() 不立即降低内存是因为 PHP 8.1 的引用计数无法处理循环引用,PhpSpreadsheet 中 Worksheet 与 Spreadsheet 互持引用,需先调用 disconnectWorksheets() 断开引用链、清除全局缓存 Settings::setCache(null),再 unset() 并 gc_collect_cycles() 才能有效回收内存。

为什么 unset() 之后内存还不降?
PHP 8.1 中调用 unset($spreadsheet) 或 unset($reader) 后,memory_get_usage() 显示内存没明显下降,不是因为没释放成功,而是 PhpSpreadsheet 的内部对象(如 Worksheet、Cell)存在循环引用,仅靠 unset() 不足以触发立即回收。尤其在启用缓存或存在全局静态引用时更明显。
必须配合显式断开工作表连接和清空缓存实例:
-
$spreadsheet->disconnectWorksheets();—— 断开所有工作表与工作簿的引用链 -
\PhpOffice\PhpSpreadsheet\Settings::setCache(null);—— 清除全局缓存配置(避免后续误用) - 再
unset($spreadsheet);,此时 GC 才能真正回收大部分内存
disconnectWorksheets() 必须在 save() 或 load() 后立刻调用
这个方法不是“可选优化”,而是内存安全的关键步骤。它会遍历所有 Worksheet 实例,把它们内部持有的 $this->parent 引用置为 null,从而打破循环引用结构。
常见错误写法:
立即学习“PHP免费学习笔记(深入)”;
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// ... 写入大量数据
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save('output.xlsx');
// ❌ 忘了 disconnectWorksheets(),$spreadsheet 仍持有所有 sheet 引用
unset($spreadsheet);
正确顺序:
$writer->save('output.xlsx');
$spreadsheet->disconnectWorksheets(); // ✅ 放在这里
unset($spreadsheet);
PHP 8.1 下缓存配置必须在创建任何对象前完成
一旦调用 IOFactory::load() 或 new Spreadsheet(),PhpSpreadsheet 就会初始化默认缓存(Memory 类型),后续再调用 Settings::setCache() 完全无效 —— 这是很多开发者踩坑的根本原因。
必须严格按顺序执行:
- 先
require 'vendor/autoload.php'; - 再创建并配置缓存实例(如 APCu、Redis 或磁盘缓存)
- 然后立即调用
\PhpOffice\PhpSpreadsheet\Settings::setCache($cache); - 最后才创建
Spreadsheet或调用IOFactory::load()
PHP 8.1 的 GC 更激进,但前提是缓存已提前接管单元格存储;否则所有单元格仍在内存中,disconnectWorksheets() 也救不了。
流式写入 + 磁盘缓存才是 PHP 8.1 处理大文件的底线配置
对于超过 5 万行的 Excel 导出,光靠 disconnectWorksheets() 和 unset() 仍可能爆内存。PHP 8.1 建议组合使用:
- 启用磁盘缓存:
$writer->setUseDiskCaching(true);并指定临时目录(需有写权限) - 关闭样式缓存:
$spreadsheet->getProperties()->setCreator('')->setLastModifiedBy('');避免元数据膨胀 - 禁用公式预计算:
$spreadsheet->setCalculateFormulae(false); - 写入后强制 GC:
gc_collect_cycles();(PHP 8.1 默认开启,但显式调用更稳妥)
注意:setUseDiskCaching() 只对 Xlsx 写入器生效,且必须在 save() 前设置;漏掉这一步,磁盘缓存形同虚设。
最易被忽略的一点:缓存实例本身(比如 ApcuCachePool)如果复用在多个请求间,它的生命周期独立于 PhpSpreadsheet,必须确保它不意外保留旧数据或 key 冲突 —— 否则看似释放了内存,实际只是把压力转嫁给了缓存服务。



















