[交流] FlintHub 插件增多后内存爆掉的原因分析

🌴Lv.5 高级 🌏 正式会员
2026-10-16 10:15:25
FlintHub 插件增多后内存爆掉的原因分析

我把导出代码里跟「插件加载 / 内存占用」相关的路径全部过了一遍,结论是:这不是某个插件的 bug,而是 PHP 语言特性 + 本项目的插件加载架构 + 钩子调用方式三者叠加的必然结果。下面按「根因 → 放大因素 → 验证方法 → 解决方向」四层说明。


一、根因:PHP 类/函数定义不可卸载,FPM worker 会累积

PHP 的核心特性:class_exists() / function_exists() 一旦为真,类与函数的定义就永久驻留进程内存,没有 unload_class() 这种东西。

FlintHub 的插件加载走的是 app/Core/Autoloader.php 的 Plugin\ 命名空间分支 + app/Helpers/Plugin.php 的钩子 include:

php
// app/Helpers/Plugin.php :: hook()
(static function (string $__hookFile, array $params, bool $__once): void {
    extract($params, EXTR_SKIP | EXTR_REFS);
    if ($__once) {
        include_once $__hookFile;   // 声明了符号的钩子文件
    } else {
        include $__hookFile;        // 纯逻辑钩子文件
    }
})($hookFile, $params, $once);

关键点:PHP-FPM 是进程复用模型。一个 worker 处理完请求后不会重启,之前 include_once 过的所有插件类、钩子文件里定义的函数/常量,全部留在该 worker 的内存里。

于是出现这样的累积曲线:

时间该 worker 已加载的插件代码
第 1 个请求触发 init_after加载了 5 个插件的钩子文件
第 2 个请求触发 route_register又加载了另外 8 个插件
……
第 N 个请求所有已启用插件的所有被调用过的钩子文件全部驻留

插件越多、每个插件的钩子文件越多(导出里能看到一个插件动辄 10~20 个钩子),单个 worker 的内存就越大。再乘以 FPM 的 pm.max_children(宝塔默认 50~100),总内存瞬间爆掉。

用一句话概括:插件 = 类定义 = 不可卸载 = 每个 FPM worker 都各自背一份全量插件代码。


二、放大因素

① 每请求都重新 include 钩子文件 → 运行时数据反复构建

Plugin::hook() 在每个钩子点都会遍历全部已激活插件。导出里能看到 layout_head_end / nav_plugin_links / right_sidebar_after / footer_links 等钩子每页都会触发。虽然 include_once 防止了重复定义类,但:

  • 未声明符号的钩子文件(纯逻辑)每次都重新执行;

  • 执行时构建的临时数组、常量、$GLOBALS 数据每请求重新分配;

  • 多个钩子点共享的插件代码被反复触发。

② 语言包全量合并 → 瞬时内存峰值陡增

app/Helpers/I18n.php::mergePluginLangs():

php
foreach (glob($pluginsRoot . '/*') ?: [] as $dir) {
    ...
    $extra = (array)require $langFile;
    if ($extra) {
        $dict = $dict + $extra;   // 核心优先
    }
}

每个已启用插件的 lang/{zh,en,zh_tw}.php 都会被 merge 进主字典。核心自身三语各约 1000 key,插件每个再叠几百到上千。插件一多,一次 I18n::load() 就要构建一个几万键的巨型数组。请求结束虽释放,但并发高峰时多个请求同时在建这个数组,瞬时峰值足以打爆 FPM。

③ 插件间用 $GLOBALS 通信 → 数据不自动清理

导出里可见多处约定:

php
$GLOBALS['auth_register_error'] = ...
$GLOBALS['attachment_download_error'] = ...
$GLOBALS['mod_system_login_blocked'] = ...

$GLOBALS 是超全局,请求结束才释放。如果某个插件把大对象挂上去(比如整帖列表、整页评论),内存会一直占到请求结束。

④ 插件独立 SQLite 连接 + DBFactory 池只有 8 个

php
// app/SplitDB/DBFactory.php
private const MAX_HANDLES = 8;
private static array $connections = [];

Plugin::db($name) 每个插件一个独立 .sqlite。插件多了之后,8 个句柄的 LRU 池频繁淘汰/重建。虽然不直接爆内存,但:

  • 每个 PDO SQLite 连接本身占 1~2MB;

  • 每次重建会触发 PRAGMA journal_mode 等初始化,间接抬高单请求峰值;

  • 配合上面三条,整体把内存推向极限。

⑤ plugin.json 里的 hooks 声明是全量的

导出里看到 Plugin::hookIndex() 会一次性把所有已激活插件的 hooks 展开成索引。插件一多,这份索引在每个请求都会重建一次(虽然是请求级,但每次分配的内存不小),且它决定了后续要 include 多少文件。


三、为什么"插件少的时候没事,多了就爆"

这是非线性的:

  • 插件少时,单个 worker 加载的代码量小,FPM 池子里每个 worker 都还撑得住;

  • 插件多时,单 worker 内存线性增长,但 FPM 不会因此少开 worker,总内存 = 单 worker 内存 × worker 数,乘性放大;

  • 语言包合并、$GLOBALS 残留、SQLite 池抖动都是并发时才暴露的放大项。

所以现象是:本地/低并发测试正常,上线后随着并发上来 + 插件越装越多,内存监控曲线呈阶梯式上涨,最终 OOM 杀进程。


四、如何验证

在 index.php 或某个钩子点打点:

php
error_log(sprintf('[mem] peak=%.1fMB current=%.1fMB',
    memory_get_peak_usage(true)/1048576,
    memory_get_usage(true)/1048576));

对照实验:

  1. 关闭所有插件 → 记录峰值

  2. 依次启用 5 / 10 / 20 / 30 个插件 → 每次记录峰值

  3. 看 opcache_get_status() 的 opcache_enabled、used_memory、hit_rate

同时看 FPM 的:

  • pm.max_children

  • pm.max_requests(默认 0 = 永不重启,这是内存累积的关键)

  • request_terminate_timeout

如果 pm.max_requests = 0,worker 永不重启,插件代码永久驻留,内存只增不减。


五、解决方向(按性价比排序)

🔴 P0:立刻能做的

  1. 开启并调大 OPcache(PHP 8 自带):

    ini
    opcache.enable=1
    opcache.memory_consumption=256
    opcache.max_accelerated_files=20000
    opcache.validate_timestamps=0

    OPcache 把编译后的 opcode 放进共享内存,避免每个 worker 各背一份。

  2. 设置 pm.max_requests(宝塔 PHP-FPM 配置里):

    ini
    pm.max_requests = 500   ; 每个 worker 处理 500 个请求后自动重启,强制释放累积内存

    这是最立竿见影的一招。

  3. 限制 pm.max_children:

    text
    pm.max_children = 可用内存 / 单 worker 峰值内存

    宁可排队也不要 OOM。

🟡 P1:架构层优化

  1. 语言包按需加载:不要把所有插件的 lang 文件全量 merge,改成按 key 前缀路由 + 懒加载。

  2. 钩子文件分类:

    • 声明了类/函数的 → include_once(现状已做)

    • 纯逻辑的 → 合并成一个「插件引导文件」,减少文件数

  3. $GLOBALS 改局部约定:用返回值 / 参数引用替代 $GLOBALS['xxx_error'] 这种约定。

  4. DBFactory 池容量按插件数动态调整(或者对插件库用短连接,避免池抖动)。

🟢 P2:长期方向

  1. PHP 7.4+ 的 opcache.preload:把插件类预编译进 OPcache,减少运行时重复加载开销。

  2. 插件代码体积审计:给每个插件设一个「单文件行数 / 类数量 / 全局符号数量」上限,超出拒绝激活。

  3. CLI 常驻进程(worker.php / worker2.php)的静态缓存加内存上限(作者已对 Thread::DECORATE_CACHE_MAX=500 等做了处理,但 Plugin::hookDeclaresSymbols、Plugin::$callerPlugin 等仍无上限)。

知识,奉行,知行合一
| 浏览 20 次 | 回复 3 次

全部回复 (3)

👑Lv.11 元老 🌞 骨干会员
2026-10-16 12:34:45

我核实了一下你提出的问题,结论如下,我建议你检查一下自己做的插件,严格按照规范中的红线约定彻底检查一下:

FlintHub「插件增多 → 内存爆掉」论断核实报告
  • 核实日期:2026-10-16
  • 核实对象:用户提供的《FlintHub 插件增多后内存爆掉的原因分析》
  • 核实方式:全部结论均来自本机实测(真实 HTTP 请求 + 进程 RSS + 反射探针),不采用推测
  • 环境:本机 phpstudy PHP 8.2.9 nts(Windows),站点根 D:/phpstudy_pro/WWW,插件 59 个目录 / 37 个已启用
  • 探针与原始日志:.workbuddy-ai/tmp/memprobe/(mem.log 为 184 条真实请求打点原始数据)

一、结论摘要

# 原文论断 核实结果 判定
1 PHP 类/函数定义不可卸载,FPM worker 会跨请求累积插件代码,形成「阶梯上涨曲线」 同一进程连续 184 次请求,class_exists() 前置判定恒为 false,memory_get_peak_usage(true) 183/184 恒为 8 MB,无任何累积 ❌ 不成立
2 OPcache 的作用是「避免每个 worker 各背一份编译后的类定义」 FPM 请求模型下类表本就是按请求建立/销毁的;opcache 的真实作用是避免每请求重复编译 ⚠️ 方向对、理由错
3 ①「每请求都重新 include 钩子文件」是内存放大因素 现象属实(实测每请求 include 248 个文件),但内存成本极低;且代码已有 hookIndex 一次性构建 + 符号判定磁盘缓存两项优化 ⚠️ 部分成立
4 ② 语言包全量合并 → 「几万键的巨型数组」,瞬时峰值打爆 FPM 实测单语言 4470 keys / 0.83 MB / 87.7 ms;三语合计 2.42 MB。不存在「几万键」,且每请求只构建当前语言一个字典 ❌ 数字夸大 ~5–10 倍
5 ③ 插件用 $GLOBALS 通信,若挂大对象(整帖列表/整页评论)内存占到请求结束 约定确实存在(3 个官方约定 + 若干插件内部标记),但实测全部是标量/小数组(bool、字符串、ID→计数映射),无大对象 ⚠️ 存在但未发生
6 ④ 每个 PDO SQLite 连接占 1~2 MB,8 个句柄池抖动抬高峰值 实测 8 个句柄 PHP 侧合计仅 +0.06 MB(约 60 KB/连接),夸大 20–30 倍。真正的 page cache 走 C malloc(不计入 memory_get_usage),但全站 SQLite 仅 5.18 MB,受库大小封顶 ❌ 数字夸大
7 ⑤ hookIndex() 每请求重建,「每次分配的内存不小」 实测 0.54 ms / 134 KB ❌ 不成立
8 插件越多,单 worker 内存线性增长,× worker 数 → OOM 实测 1 个插件 → 37 个插件,钩子源码仅从 15 KB 增至 371 KB(36 个插件合计只增加 356 KB) ❌ 因果链不成立
9 P0:开 OPcache、设 pm.max_requests、限制 pm.max_children 方向正确,是全文唯一真正有效的部分 ✅ 成立
10 P2:Plugin::hookDeclaresSymbols、Plugin::$callerPlugin 仍无上限 $hookDeclaresSymbols 是请求级数组(上限=被 include 的钩子文件数)+ 磁盘缓存(17 KB);$callerPlugin 是 ?string,上限为 1 ❌ 不成立

一句话结论:原文的根因模型(跨请求累积)是错的,因此它推导出的「插件架构必然导致 OOM」这个结论站不住脚;但它在部署层给出的 P0 建议(opcache + FPM 参数)方向是对的——只是理由需要修正。真正的 OOM 风险在 FPM 进程池配置,不在插件架构。


二、逐条核实(证据)

论断 1:跨请求累积 ❌

原文主张:include_once 过的插件类、钩子文件里定义的函数/常量,全部留在该 worker 的内存里,形成累积曲线。

实测(.workbuddy-ai/tmp/memprobe/probe_cumul.php,同一 PID=21128 连续 5 次请求):

pid=21128 beforeClass=0 beforeFn=0 afterClass=1   ← 第 1 次:类不存在 → include 后存在
pid=21128 beforeClass=0 beforeFn=0 afterClass=1   ← 第 2 次:类**依然不存在**(未累积)
pid=21128 beforeClass=0 beforeFn=0 afterClass=1   ← 第 3 次:同上
pid=21128 beforeClass=0 beforeFn=0 afterClass=1
pid=21128 beforeClass=0 beforeFn=0 afterClass=1
classes=177 included_files=2    (每次完全相同)
mem=395560 mem_real=2097152     (每次完全相同,2 MB 不增长)

184 次真实请求的峰值分布(mem.log):

peak_real 取值分布: {"8388608":183, "6291456":1}     ← 8 MB × 183 次
用户态 peak 最小/最大: 5.44 / 7.52 MB
included_files 最小/最大: 132 / 249

机理:PHP 的 included_files 去重表、类表(class table)、用户态静态变量都是按请求(per-request)建立并在 php_request_shutdown 时销毁的,与 FPM / cli-server / mod_php 无关。include_once 的去重只在单次请求内有效,跨请求必然重新 include。

补充:唯一「类定义真的常驻」的场景是 CLI 常驻进程(cli/worker.php / worker2.php)。但实测 grep Plugin::hook cli/ cron_trigger.php 无任何命中 —— 这两个队列消费者不触发任何插件钩子,与插件数量无关。

进程 RSS 长跑测试(PID 13592,每轮 60 个 /login 请求):

阶段 RSS
起始 36,320 K
60 请求后 39,492 K(+3,172 K)
120 请求后 40,236 K(+744 K)
180 请求后 40,552 K(+316 K)

增量 3,172 → 744 → 316 K,明显收敛(典型 allocator 高水位 + SQLite page cache 按需分配),不是线性泄漏。

论断 3:每请求重新 include 钩子文件 ⚠️

现象属实。实测一次完整前台请求(/login,探针 router_probe.php):

分类 文件数 源码字节
核心 38 731,230
插件钩子文件 123 178,080
插件其它(Plugin.php / FrontController / views) 87 1,174,889
合计 248 2,084,199

→ 插件相关共 210 个文件 / 1.35 MB,占单请求代码加载量的约 65% —— 这是原文唯一站得住的量化直觉。

但:

  1. 绝对量只有 1.35 MB 源码,编译后 opcode 约 2–4 MB;
  2. 代码里已经有两项针对性优化(原文未提及):
    • Plugin::hookIndex()(app/Helpers/Plugin.php:738-779):请求级一次性构建「钩子名 → 文件列表」,实测 0.54 ms / 134 KB;
    • Plugin::hookDeclaresSymbols() 的磁盘缓存(Plugin.php:790-825,落盘 data/runtime/hook_symbols.json):实测 214 个文件判定,缓存命中 10.14 ms,无缓存 186.14 ms → 节省 95%。

论断 4:语言包「几万键」 ❌

实测(probe_i18n_db.php):

核心 lang/zh.php: 1641 keys
插件 lang(36 个文件): 2829 keys
合并后 load('zh'): 4470 keys, 内存增量 0.83 MB, 耗时 87.7 ms
三语合计常驻: 2.42 MB

app/Helpers/I18n.php:58-75 的 mergePluginLangs() 确实每请求遍历 plugins/*(含每个插件一次 plugin.json 读取 + json_decode),但每请求只构建当前语言的一个字典(self::$loaded 请求内缓存),成本 0.83 MB。

论断 6:SQLite 连接 1~2 MB ❌

实测(probe_i18n_db.php):

连接 data/adslot.sqlite → 增量 0.058 MB
连接 data/ai_assistant.sqlite → 增量 0.058 MB
...(8 个句柄)
8 个句柄后 PHP 侧内存增量: 0.06 MB

app/SplitDB/DBFactory.php:18 MAX_HANDLES = 8 属实,LRU 淘汰逻辑属实,但 PDO 对象本身只有 ~60 KB。

原文漏掉的一点:DBFactory.php:43 PRAGMA cache_size = -20000(负值单位 KB)= 单连接 page cache 上限 20 MB,这部分走 SQLite 的 C malloc,不计入 PHP 的 memory_get_usage / memory_limit,只体现在进程 RSS。这是原文完全没提、但真实存在的内存项。不过它是懒分配且受库大小封顶的——实测全站 SQLite 共 5.18 MB(插件 50 个库合计 5.09 MB),所以实际占用上限就是几 MB 量级。

论断 8:插件数量 → 内存 ❌

实测(probe_hookindex.php,反射替换 Plugin::$plugins 后调私有 hookIndex()):

启用插件数 钩子名数 唯一钩子文件 源码字节
0 0 0 0
1 6 6 15,162
5 16 31 59,855
10 22 53 94,964
20 39 109 173,830
37 42 214 379,874

36 个插件合计只增加 356 KB 源码(平均 ~10 KB/插件)。相对单请求 8 MB 峰值,插件数量的边际影响在几百 KB 量级,不构成「越多越爆」的因果链。

论断 10:静态缓存「无上限」 ❌

  • Plugin::$hookDeclaresSymbols(Plugin.php:21):请求级数组,键为文件绝对路径,上限 = 被 include 的钩子文件数(实测 ~123);配套磁盘缓存 hook_symbols.json 实测 17 KB,按文件数封顶。
  • Plugin::$callerPlugin(Plugin.php:18):类型 ?string,上限就是 1。
  • Thread::DECORATE_CACHE_MAX = 500(app/Models/Thread.php:43)、Post::THREAD_INDEX_CACHE_MAX = 500(Post.php:621)、User::BATCH_CACHE_MAX = 500(User.php:25)确实都已存在——原文这点是对的。

三、重新归因:内存真正的账在哪

把实测数字代进 FPM 的真实模型:

单请求 PHP 用户态峰值          ≈ 8 MB   (实测)
单 worker 进程 RSS             ≈ 35 MB  (实测 13592 = 35.5 MB;Linux php-fpm 通常 20–30 MB)
   └ 其中大头是 PHP 二进制/扩展映射 + allocator 保留 + SQLite page cache,
     而不是「插件代码累积」(后者实测 0 累积)

于是 OOM 的算式是:

PHP-FPM 总内存 = 单 worker RSS × pm.max_children

关键:pm.max_children 才是乘数。 若面板默认给了 50(宝塔部分版本默认值偏高,需在面板实际确认):

50 × 35 MB = 1.75 GB   ← 2 核 2G 机器上,加上系统 + Nginx + 数据库必然 OOM

而插件数量对这个乘积的贡献是:每个插件约 +10 KB 源码。

次要但真实的放大项(与插件数量无关,与「有没有配好」有关):

  1. opcache 未启用:本机 php -m 完全没有 opcache 扩展。无 opcache 时每请求要重新编译 248 个文件,实测单请求 264–980 ms(本机 Windows)。生产若同样未开,响应慢 → 并发堆积 → 触发更多 worker → 内存上升。注意:Plugin::saveCache()(Plugin.php:593-595)与 flushSymbolDiskCache()(Plugin.php:889-891)都主动调用了 opcache_invalidate() —— 说明作者本就预期 opcache 是开启的,当前未开属于部署缺失。
  2. pm.max_requests 未设:若为 0(unlimited),worker 永不重启。实测 RSS 虽收敛,但设一个上限仍是低成本的保险。
  3. PageCache 覆盖率:index.php:112-156 的游客前置静态缓存能完全跳过框架引导(含全部插件钩子),是降低单请求成本最有效的手段;覆盖率越高,FPM 压力越小。

四、改善方案(按性价比排序)

⚠️ 以下均为方案,未改任何代码。涉及生产配置的项请确认后再动。

🔴 P0 —— 只改部署配置,不动一行代码(这才是治 OOM 的)

  1. 开启 OPcache

    opcache.enable=1
    opcache.memory_consumption=128        ; 本站 opcode 量不大,128 MB 足够
    opcache.max_accelerated_files=10000   ; 本站 PHP 文件约 1500+,10000 有余量
    opcache.validate_timestamps=1         ; ★ 见下方注意
    opcache.revalidate_freq=60
    

    ⚠️ 本项目特有约束:FlintHub 的在线升级、插件启停都依赖「文件改动后立即生效」。Plugin::refresh() / Plugin::saveCache() / clearSymbolCache() 已显式调用 opcache_invalidate(),因此即使 validate_timestamps=0,代码里这些路径也能正确失效;但手工上传/覆盖文件(不经上述入口)在 validate_timestamps=0 下不会自动生效。建议先用 validate_timestamps=1 + revalidate_freq=60,稳妥后再考虑收紧。

  2. 按实测 RSS 反算并限制 pm.max_children

    pm.max_children = 可用内存 / 单 worker RSS
    

    2G 机器扣掉系统 + Nginx + 数据库后按 1.2 G 可用、单 worker 30–35 MB 计 → 建议 20–25,先取 20。宁可排队,不要 OOM。

  3. 设置 pm.max_requests = 500(worker 处理 500 请求后自动重启,兜住任何潜在慢泄漏)。

  4. 确认 memory_limit:本站 env_check.php:141 会打印该项,建议显式设为 128M,不要留空或设成 unlimited。

🟡 P1 —— 值得做,收益中等

  1. 在 cli/env_check.php 里补一条 OPcache 检查:目前 env_check.php:141 检查了 allow_url_fopen / memory_limit / max_execution_time / upload_max_filesize / post_max_size / max_file_uploads / open_basedir / date.timezone,唯独没有 opcache。而代码里已在依赖它。加一行 opcache.enable + opcache_get_status() 输出,能让「没开 opcache」这件事在部署时就被发现,而不是等 OOM。

  2. 把「FPM 参数 + opcache」写进 INSTALL.md 的部署章节:目前全站文档(docs/、INSTALL.md、README.md)grep 不到任何 opcache / pm.max_children / pm.max_requests 的部署建议(只有 docs/性能与可靠性修复_交付说明_2026-10-05.md:68 一句提及测试脚本「带 opcache」)。这是当前文档的真实空白。

  3. 提高 PageCache 覆盖率:确认首页 / /forum / /forum/category/N 之外,还有哪些游客可缓存的列表页(如 /tags、/tag/{id})可以纳入。每多覆盖一类页面,就少一类「加载 210 个插件文件」的请求。

🟢 P2 —— 收益很小,可不做

  1. 语言包按需加载:实测单语言 0.83 MB,优化收益上限就是这 0.83 MB。投入产出比很低。
  2. 钩子文件合并 / 减少文件数:实测 214 个钩子文件共 371 KB,合并省不下什么。
  3. $GLOBALS 改返回值/参数引用:实测当前全是标量,无内存问题。若要改,动机应是契约清晰度(审计报告.md:194 已记录该弱契约),而非内存。
  4. opcache.preload:本站 opcode 量小,preload 收益有限,且与在线升级的文件替换流程有冲突风险,不建议现在上。

⚪ 需要进一步确认的(本机数据量太小,测不出来)

  1. 单请求内存的「数据相关」峰值:本机 SQLite 数据量小(业务库 2.36 MB),无法复现「大数据量下某个页面/插件内存暴涨」。建议在生产(或数据完整的环境)用同样的探针复测一次:把 .workbuddy-ai/tmp/memprobe/router_probe.php 挂成 php-fpm 的 auto_prepend_file(FPM 支持该指令),跑几天收集 mem.log,看是否存在某个 URI 的 peak 显著高于 8 MB。若存在,那才是需要针对具体插件优化的点——而它与「插件数量」无关。

五、复现方法

# 1. 跨请求不累积(决定性实验)
php -S 127.0.0.1:8899 -t D:/phpstudy_pro/WWW
curl http://127.0.0.1:8899/.workbuddy-ai/tmp/memprobe/probe_cumul.php   # 连打多次,看 beforeClass 恒为 0

# 2. 真实请求打点(分类统计)
php -S 127.0.0.1:8904 -t D:/phpstudy_pro/WWW \
    D:/phpstudy_pro/WWW/.workbuddy-ai/tmp/memprobe/router_probe.php
for i in $(seq 1 60); do curl -s -o /dev/null http://127.0.0.1:8904/login; done
tasklist //FI "PID eq <pid>" //FO CSV        # 对比 RSS

# 3. 成本探针(CLI,只读)
php .workbuddy-ai/tmp/memprobe/probe_i18n_db.php      # I18n + DB 句柄
php .workbuddy-ai/tmp/memprobe/probe_hookindex.php    # 插件数 → 钩子文件
php .workbuddy-ai/tmp/memprobe/probe_hooksymbol.php   # 索引 + 符号判定

副作用声明:探针全程零写入(唯一例外是打真实路由 /login 等,等价于用浏览器访问几次,会正常写 session/在线状态)。mem.log 与全部探针脚本保留在 .workbuddy-ai/tmp/memprobe/,需要清理时按文件名精确删除即可。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
#1 楼
👑Lv.11 元老 🌞 骨干会员
2026-10-16 13:16:36


ahfanxin 发表于:FlintHub 插件增多后内存爆掉的原因分析我把导出代码里跟「插件加载 / 内存占用」相关的路径全部过了一遍,结论是:这不是某个插件的 bug,而是 PHP 语言特性 + 本项目的插件加载架构 + 钩子调用方式三者叠加的必然结果。下面按「根因 → 放大因素 → 验证方法 → 解决方向」四层说明。一、根因:PHP 类/函数定义不可卸载,FPM worker 会累积PHP 的核心特性:class_exists() / function_exists() 一旦为真,类与函数的定义就永久驻留进程内存,没有 unload_class() 这种东西。FlintHub 的插件加载走的是 app/Core/Autoloader.php 的 Plugin\ 命名空间分支 + app/Helpers/Plugin.php 的钩子 include:php// app/Helpers/Plugin.php :: hook() (static function (string $__hookFile, array $params, bool $__once): void { extract($para

你看一下上一个帖子,程序代码和官方插件我都查了,接下来你要做的是服务器的配置和你自制插件的核查。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
#2 楼
🌴Lv.5 高级 🌏 正式会员
2026-10-16 14:34:30
我正在一个个排查
知识,奉行,知行合一
#3 楼

请 登录