祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 41|回复: 1

[学习笔记] PowerBuilder 对象生命周期与垃圾回收全解:GarbageCollect 三函数、引用计数与析构时机实测(PB10 基准 · PB12.5 实测)

[复制链接]

[学习笔记] PowerBuilder 对象生命周期与垃圾回收全解:GarbageCollect 三函数、引用计数与析构时机实测(PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

4465

积分

PBAI

积分
4465
贡献
在线时间
小时
8 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
PowerBuilder 对象生命周期与垃圾回收全解:GarbageCollect 三函数、引用计数与析构时机实测(PB10 基准 · PB12.5 实测)

阅读说明
- 适用版本:PowerBuilder 10 及以上(本文以 PB10 为写作与验证基准,PB12.5 双轨实测,全部读数两版一致)。本文不依赖 PBIDEA,纯 PowerScript 能力。
- 支持数据库:本文不涉及数据库。
- 操作系统与环境:Windows 7 及以上;任意 PB 可运行环境即可,无需第三方运行库。
- 难度系数:★★★☆☆(概念不难,但「对象到底死了没有」的验证方法和几个反直觉读数,需要一点动手耐心)。
- 其它:文中自检示例会在当前目录留下若干 gcmark 开头的标记文件,属于验证痕迹,可自行删除;建议先读第四节再回头看实测。




一、这篇讲什么

PB 里一个对象「不用了」和「已经销毁了」是两回事。你手里的引用一丢,对象未必马上消失;两个对象互相攥着对方,即使外面的引用全断了,它们也可能一直活着。管这件事的是两套机制:引用计数和垃圾回收,配套三个函数——GarbageCollect()、GarbageCollectGetTimeLimit()、GarbageCollectSetTimeLimit()。

这三个函数帮助文档里各只有几行字,看着人畜无害。但真跑起来,问题一个接一个:


  • 局部变量出了作用域,析构事件到底触发没有?什么时候触发?
  • 两个互相引用的对象,引用全断之后谁来收尸?
  • GarbageCollect() 调了就一定回收吗?被时间限制拦住了你怎么知道?(它没有返回值,成败全靠猜)
  • SetNull() 用在对象引用上会怎样?编译器居然放行,运行时呢?


这篇用「析构标记文件法」把析构时机变成肉眼可见的文件记录,配合二十多组单例探针,把上面的问题一次问清。所有读数 PB10 与 PB12.5 双轨跑出,逐字一致,不引文档、只报实测。

二、先给结论:一张速查表

场景结论依据(实测)
局部 NVO 出作用域立即析构,不用等 GC,不用调 GarbageCollect探针方法一返回,析构标记文件立刻出现
destroy 显式销毁析构立刻触发,同一方法内马上可见destroy 下一行 FileExists 已为 true
循环引用对象引用全断后继续存活,显式 GarbageCollect() 后被回收回收前标记不在、调用后标记出现,多次复现
被活对象持有的对象GC 不回收;持有者销毁时连锁析构持有者活着时反复 GC 标记始终不出现
autoinstantiate NVO出作用域即析构,但析构脚本触发两次一个实例,标记文件多出一行,两版一致
SetNull(对象引用)编译通过;引用被释放、对象立即析构、IsValid 返回 NULL 而非 false逐项落盘取证
lp = null编译报 C0015: Undefined variable: null对象引用不能直赋 null 字面量
GC 默认最小间隔500 毫秒(GarbageCollectGetTimeLimit() 初始读数)两版初始读数均为 500
GarbageCollectSetTimeLimit返回设置前的旧间隔;入参为 null 时返回 null 且不改设置读数见第九节
显式 GarbageCollect()通常立即生效;但它没有返回值,个别上下文里观测到未生效的案例,成败必须靠标记验证见第十一节
PB10 与 PB12.5 差异无,全部读数逐字一致双轨对照


三、三件套函数全貌

三个都是全局函数,签名如下:
  1. GarbageCollect()                            // 强制立即做一次垃圾回收,无返回值
  2. GarbageCollectGetTimeLimit()                // 返回 Long:当前 GC 最小间隔(毫秒)
  3. GarbageCollectSetTimeLimit(新间隔毫秒)       // 返回 Long:设置之前的旧间隔
复制代码

第一件事先摸底:默认间隔是多少。
  1. // 输入值:无。本段直接读取系统当前 GC 最小间隔并弹窗显示
  2. long ll_limit
  3. ll_limit = GarbageCollectGetTimeLimit()
  4. MessageBox('GC 最小间隔', '当前最小间隔 = ' + String(ll_limit) + ' 毫秒')
  5. // 实测输出:当前最小间隔 = 500 毫秒(PB10 与 PB12.5 相同)
复制代码

GarbageCollectSetTimeLimit() 的返回值是旧值,这个设计可以无脑保存恢复,不用先 Get 再 Set:
  1. // 输入值:li_new 想设置的新间隔(毫秒),逐行注释见右
  2. long ll_prev
  3. long ll_now
  4. long ll_back
  5. long ll_new
  6. ll_new = 5000                        // 想改成 5 秒
  7. ll_prev = GarbageCollectGetTimeLimit()   // 先记下当前值(实测 500)
  8. GarbageCollectSetTimeLimit(ll_new)       // 设置
  9. ll_now = GarbageCollectGetTimeLimit()    // 读回应为 5000
  10. ll_back = GarbageCollectSetTimeLimit(ll_prev)  // 恢复,返回值为设置前的 5000
  11. MessageBox('时间限制', 'prev=' + String(ll_prev) + ', now=' + String(ll_now) + ', back=' + String(ll_back))
  12. // 实测输出:prev=500, now=5000, back=5000(恢复后再读回 500)
复制代码

两个容易忽略的点,都是实测出来的:


  • 入参传 null:函数返回 null,且当前间隔不被改动。也就是说它不是「设成 0」也不是报错,是安静地什么都不做;
  • 传 0 是合法的:等于不节流,自动回收随时可以做。传很大的数(比如 2000000000)等价于长期压制自动回收。

  1. // 输入值:ll_null 置空后的 long,用于验证 null 入参行为
  2. long ll_null
  3. long ll_ret
  4. SetNull(ll_null)                             // 造一个 null 的 long
  5. ll_ret = GarbageCollectSetTimeLimit(ll_null) // 把 null 传进去
  6. MessageBox('null 入参', 'IsNull(返回值)=' + String(IsNull(ll_ret)) + ', 间隔仍是=' + String(GarbageCollectGetTimeLimit()))
  7. // 实测输出:IsNull(返回值)=true, 间隔仍是=500 —— 返回 null,设置未变
复制代码

GarbageCollect() 本身没有返回值、没有错误码,调用成败完全无从判断——这是它和 Trace 函数族最大的差别,也是第十一节要展开的坑。

四、验证方法:析构标记文件法

析构事件不可见、返回值不可用,怎么知道对象死了?给探针对象装一个「遗嘱」:destructor 事件里往文件追加一行字。文件出现了,析构就触发过;文件没出现,对象还活着。
  1. forward
  2. global type uo_probe from nonvisualobject
  3. end type
  4. end forward
  5. global type uo_probe from nonvisualobject
  6. end type
  7. global uo_probe uo_probe
  8. type variables
  9. string is_mark       // 标记文件全路径,由 of_setmark 指定
  10. end variables
  11. forward prototypes
  12. public subroutine of_setmark (string as_path)
  13. public subroutine of_noop ()
  14. end prototypes
  15. // 析构前先设定标记文件写到哪
  16. public subroutine of_setmark (string as_path);
  17.         is_mark = as_path
  18. end subroutine
  19. // 占位方法,让对象有事可做
  20. public subroutine of_noop ();
  21. end subroutine
  22. on uo_probe.create
  23. call super::create
  24. TriggerEvent( this, "constructor" )
  25. end on
  26. on uo_probe.destroy
  27. TriggerEvent( this, "destructor" )
  28. call super::destroy
  29. end on
  30. event destructor;
  31.         long ll_f
  32.         if Len(is_mark) > 0 then
  33.                 ll_f = FileOpen(is_mark, LineMode!, Write!, LockWrite!, Append!)
  34.                 if ll_f > 0 then
  35.                         FileWrite(ll_f, 'DESTR')
  36.                         FileClose(ll_f)
  37.                 end if
  38.         end if
  39. end event
复制代码

用 Append! 追加而不覆盖,是因为析构可能触发不止一次——第十节会给你看一个真的触发两次的例子。这一招不挑版本、不挑环境,是验证「对象到底死了没有」最朴素的办法,本文所有读数都靠它。

一个重要前提:标记路径必须是绝对路径,而且检查和写入要用同一个路径。我们的第一版探针吃过亏——同一份逻辑在不同流程里跑出相反读数,最后发现是上一轮的旧标记文件污染了 FileExists 判断。所以每个用例的标记名都要带唯一前缀(比如时间戳),从不去猜文件是不是「这次的」。

五、实测一:局部对象出作用域,析构立即触发

第一个要验证的直觉:方法里的局部 NVO,方法一结束是不是马上销毁?答案是立刻,根本不用等 GC。
  1. forward
  2. global type nvo_gc_life from nonvisualobject
  3. end type
  4. end forward
  5. global type nvo_gc_life from nonvisualobject
  6. end type
  7. global nvo_gc_life nvo_gc_life
  8. forward prototypes
  9. public subroutine of_drop_local (string as_mark)
  10. end prototypes
  11. // 创建一个探针,方法结束时引用自然消失
  12. public subroutine of_drop_local (string as_mark);
  13.         uo_probe_a lp
  14.         lp = create uo_probe_a
  15.         lp.of_setmark(as_mark)
  16.         lp.of_noop()
  17.         // 注意:这里不 destroy,也不调 GarbageCollect,引用随方法结束释放
  18. end subroutine
复制代码

调用方紧跟着检查标记文件:
  1. // 输入值:ls_mark 标记文件绝对路径(建议带时间戳前缀保证唯一),先赋值再传入
  2. string ls_mark
  3. nvo_gc_life lf
  4. ls_mark = 'gcmark_' + String(Now(), 'hhmmss') + '_l1.txt'
  5. lf = create nvo_gc_life
  6. lf.of_drop_local(ls_mark)
  7. MessageBox('析构时机', '方法返回后标记已出现 = ' + String(FileExists(ls_mark)))
  8. // 实测输出:true —— 引用计数归零当场析构,未调用任何 GC 函数
复制代码

这个读数在 PB10 与 PB12.5 上完全一致,而且反复跑了多轮全部如此。也就是说:普通对象你根本不用操心 GC,丢掉引用就完事了。GC 不是为它们准备的。

六、实测二:destroy 的析构时机与 IsValid 的三个读数

显式 destroy 更没有悬念——析构在 destroy 语句当行就完成:
  1. // 输入值:ls_mark 标记文件全路径
  2. uo_probe_a lp
  3. lp = create uo_probe_a
  4. lp.of_setmark(ls_mark)
  5. destroy lp
  6. // destroy 的下一行,标记文件已经存在
  7. MessageBox('destroy 时机', '析构已触发 = ' + String(FileExists(ls_mark)))
  8. // 实测输出:true
复制代码

顺手把 IsValid() 的三种读数也钉死:

对象状态IsValid 返回实测
从未 create 的局部引用falseneverCreated=false
destroy 之后falseafterDestroy=false
还被持有时trueheld=true


注意第三行:IsValid 只能告诉你引用是否指向活对象,不能告诉你对象「还有没有人用」——被持有就是 true,哪怕你打算再也不用它。

七、实测三:循环引用——GC 存在的唯一理由

现在造一对互相引用的对象。这里有个导入器层面的坑要先交代:两个 NVO 在源码里互相引用对方类型,用 ORCA 方式导入时是「鸡生蛋」——先导谁都报 C0001 Illegal data type,因为对方还不在库里。解法是成员变量和参数都用祖先类型 nonvisualobject,运行时照样构成真正的循环引用,编译期却互不依赖:
  1. forward
  2. global type uo_circ1 from nonvisualobject
  3. end type
  4. end forward
  5. global type uo_circ1 from nonvisualobject
  6. end type
  7. global uo_circ1 uo_circ1
  8. type variables
  9. nonvisualobject ic2     // 用祖先类型,导入顺序就不再重要
  10. string is_mark
  11. end variables
  12. forward prototypes
  13. public subroutine of_setmark (string as_path)
  14. public subroutine of_link (nonvisualobject a_c2)
  15. end prototypes
  16. public subroutine of_setmark (string as_path);
  17.         is_mark = as_path
  18. end subroutine
  19. // 指向对方,构成引用环
  20. public subroutine of_link (nonvisualobject a_c2);
  21.         ic2 = a_c2
  22. end subroutine
  23. // uo_circ2 与此完全对称,成员是 nonvisualobject ic1
复制代码
  1. forward
  2. global type nvo_gc_life from nonvisualobject
  3. end type
  4. end forward
  5. global type nvo_gc_life from nonvisualobject
  6. end type
  7. global nvo_gc_life nvo_gc_life
  8. forward prototypes
  9. public subroutine of_drop_cycle (string as_mark1, string as_mark2)
  10. public subroutine of_gc ()
  11. end prototypes
  12. // 创建一对循环引用,方法结束时外部引用全部消失
  13. public subroutine of_drop_cycle (string as_mark1, string as_mark2);
  14.         uo_circ1 lc1
  15.         uo_circ2 lc2
  16.         lc1 = create uo_circ1
  17.         lc2 = create uo_circ2
  18.         lc1.of_setmark(as_mark1)
  19.         lc2.of_setmark(as_mark2)
  20.         lc1.of_link(lc2)
  21.         lc2.of_link(lc1)
  22.         // 两个局部变量互相攥着对方,出作用域后引用计数都到不了 0
  23. end subroutine
  24. // 显式做一次垃圾回收
  25. public subroutine of_gc ();
  26.         GarbageCollect()
  27. end subroutine
复制代码

读数分两步看:

时机循环对析构标记结论
引用全断后、未调 GC未出现(false)循环引用靠引用计数收不了,继续存活
显式 GarbageCollect() 之后出现(true / true,两个都收)GC 是循环引用唯一的收尸人


这正是垃圾回收在 PB 里存在的理由:引用计数对付不了环,环只能靠 GC 从「可达性」上判断。反过来说,如果你的对象没有环,GarbageCollect() 基本无事可做。

补一个我们没打算深挖但如实报告的观察:在纯计算的测试进程里,Sleep(2) 睡两秒的窗口内循环引用并没有被自动回收(三次复跑一致);但另一次长流程中出现过未显式调用 GC、循环对却消失的情况。自动回收的触发时机依赖引擎内部状态,秒级精度下摸不准。教学口径一句话:有环就自己显式 GarbageCollect(),别指望自动。

八、实测四:被持有的对象,GC 拿它没办法

「对象没人用了」是人的判断,GC 只认引用。把探针塞进一个活对象的实例变量里,再怎么 GarbageCollect() 都白搭:
  1. type variables
  2. uo_probe_a ia_hold      // nvo_gc_life 的实例变量,持有权在这里
  3. end variables
  4. forward prototypes
  5. public subroutine of_held_hold (string as_mark)
  6. end prototypes
  7. // 把探针挂到自己的实例变量上,本对象活着它就活着
  8. public subroutine of_held_hold (string as_mark);
  9.         ia_hold = create uo_probe_a
  10.         ia_hold.of_setmark(as_mark)
  11. end subroutine
复制代码
  1. // 输入值:ls_mark 标记文件全路径
  2. nvo_gc_life lf
  3. lf = create nvo_gc_life
  4. lf.of_held_hold(ls_mark)
  5. GarbageCollect()
  6. MessageBox('持有验证', 'GC 之后标记是否出现 = ' + String(FileExists(ls_mark)))
  7. // 实测输出:false —— 持有者活着,GC 不回收
  8. destroy lf
  9. MessageBox('连锁析构', '持有者销毁后标记是否出现 = ' + String(FileExists(ls_mark)))
  10. // 实测输出:true —— 持有者一死,里面挂着的对象立刻析构
复制代码

两组读数两版一致。这条的实际意义:怀疑内存泄漏时,先画引用图——对象被谁挂在实例变量(或数组、DataStore 的对象属性)上,顺藤摸瓜找到那个忘了销毁的持有者,比狂调 GarbageCollect 有用得多。

九、实测五:autoinstantiate 的析构会跑两遍

autoinstantiate 的 NVO 声明即创建、出作用域即销毁,用起来省心。但我们给析构装上标记文件后,发现了一个怪事:声明一个实例,析构脚本跑了两遍。

验证方法很直接:一个方法里只声明一个 autoinstantiate 局部变量,方法结束后数标记文件里的行数。
  1. forward
  2. global type uo_probe_b from nonvisualobject autoinstantiate
  3. end type
  4. end forward
  5. global type uo_probe_b from nonvisualobject autoinstantiate
  6. end type
  7. global uo_probe_b uo_probe_b
  8. event destructor;
  9.         long ll_f
  10.         ll_f = FileOpen('gcmark_b.txt', LineMode!, Write!, LockWrite!, Append!)
  11.         if ll_f > 0 then
  12.                 FileWrite(ll_f, 'DESTR')
  13.                 FileClose(ll_f)
  14.         end if
  15. end event
复制代码
  1. // 输入值:无。声明一个 autoinstantiate 局部对象,出作用域自动析构
  2. uo_probe_b lb
  3. lb.of_noop()
  4. // 方法结束后检查 gcmark_b.txt:一个实例,两行 DESTR
复制代码

实测:单个实例的析构标记出现 2 次,PB10 与 PB12.5 一致(我们跑了多轮、跨多个进程对账,行数永远比实例数多一倍)。同一份标记逻辑挂在普通 NVO 上就只触发一次。结论只能下到这里:autoinstantiate 对象的 destructor 事件脚本确实执行了两次——至于是引擎销毁路径走了两遍还是别的机制,没继续深挖。实用建议只有一条:destructor 里放「只许执行一次」的逻辑(关连接、删临时文件、写审计日志)时,别用 autoinstantiate,或者自己加防重入开关。

十、实测六:SetNull 对象引用,编译放行、运行埋雷

这一节最反直觉。SetNull() 文档说能用于「任何变量类型」,对象引用居然真的能编译通过:
  1. // 输入值:ls_mark 标记文件全路径
  2. uo_probe_a lp
  3. boolean lb_v
  4. lp = create uo_probe_a
  5. lp.of_setmark(ls_mark)
  6. SetNull(lp)                          // 编译器放行!
  7. lb_v = IsValid(lp)
复制代码

运行时逐项取证,读数如下:

检查项读数说明
IsValid(lp)NULL(不是 false!)IsNull(lb_v)=true,布尔结果都是 NULL
对象本体立即析构析构标记当场出现
之后再调方法运行期错误 Null object reference崩在调用行,TRY-CATCH 之外照常炸


也就是说 SetNull(对象) 的实际语义接近「释放引用并置空」:引用一空,引用计数归零,对象当场析构;而 IsValid 对空引用返回的不是 false 而是 NULL,拿它做布尔判断会静默走 NULL 分支,拼接字符串时整串悄悄变空。想安全释放引用,老老实实 destroy;SetNull 留给字符串、日期这些值类型。

顺带钉死另一扇门:lp = null 这种写法编译不过,报 C0015: Undefined variable: null。PB 的对象引用没有 null 字面量可赋,想置空只有 SetNull 一条路(而它的行为如上,慎用)。

十一、GarbageCollect 的节流与「成败不可知」

回到三件套的第三个函数。GarbageCollect() 有两个实测特性,合在一起构成它最大的使用陷阱:

一,它没有返回值、没有错误码。 调用是否真的执行了回收、回收了什么,一概不知。

二,它的生效与「上次回收过去多久」有关。 默认最小间隔 500 毫秒;GarbageCollectSetTimeLimit() 设置的间隔写读都正常(返回旧值、null 不改设置),但在「设了超大间隔后紧跟调用」的场景里,我们观测到过回收没有发生的案例——同一长流程里六次一致,可单例隔离复测却又回收了。用秒级的观测精度,实在给不出一条干净的边界线,这里只给结论不编故事:


  • 显式 GarbageCollect() 通常立即生效(本文第七节全部读数依赖这一点,稳定复现);
  • 但不要在代码里假设它百分之百立即执行。紧跟在上一次回收之后的调用,存在不生效的实测案例;
  • 验证成败的唯一办法就是第四节那招——让对象的析构自己留下痕迹;
  • 自动回收的触发时机同样摸不准(Sleep(2) 的窗口内我们没抓到过一次),所以「有环必须显式调用」这条前面已经说过了,这里不重复。


一句话总结这一节:GarbageCollect 是「尽力而为」的语义,不是「保证完成」的语义。

十二、PB12.5 差异

本文全部探针在 PB10 与 PB12.5 双轨运行,所有读数逐字一致,包括:


  • 默认间隔 500、SetTimeLimit 返回旧值、null 入参行为;
  • 引用计数立即析构、destroy 当行析构、循环引用等 GC、持有保护、连锁析构;
  • autoinstantiate 析构双跳;
  • SetNull(对象) 的三个读数(IsValid=NULL、立即析构、Null object reference)。


与 PB10 行为一致,无需单独差异清单。

十三、完整自检示例

把上面的结论拼成一个可以直接抄走的自检对象。两个辅助方法负责「创建并丢引用」——注意创建和检查必须分处两个方法,写在同一个方法里局部变量到函数结束都没出作用域,读数全错(我们第一版就栽在这里):
  1. forward
  2. global type nvo_gc_demo from nonvisualobject
  3. end type
  4. end forward
  5. global type nvo_gc_demo from nonvisualobject
  6. end type
  7. global nvo_gc_demo nvo_gc_demo
  8. type variables
  9. uo_probe_a ia_hold
  10. end variables
  11. forward prototypes
  12. public function string of_run ()
  13. public subroutine of_drop_local (string as_mark)
  14. public subroutine of_drop_cycle (string as_mark1, string as_mark2)
  15. end prototypes
  16. // 创建探针并丢引用(引用计数析构发生在方法结束时)
  17. public subroutine of_drop_local (string as_mark);
  18.         uo_probe_a lp
  19.         lp = create uo_probe_a
  20.         lp.of_setmark(as_mark)
  21.         lp.of_noop()
  22. end subroutine
  23. // 创建循环引用对并丢引用
  24. public subroutine of_drop_cycle (string as_mark1, string as_mark2);
  25.         uo_circ1 lc1
  26.         uo_circ2 lc2
  27.         lc1 = create uo_circ1
  28.         lc2 = create uo_circ2
  29.         lc1.of_setmark(as_mark1)
  30.         lc2.of_setmark(as_mark2)
  31.         lc1.of_link(lc2)
  32.         lc2.of_link(lc1)
  33. end subroutine
  34. // 全流程自检,返回报告字符串
  35. public function string of_run ();
  36.         string ls_rpt
  37.         string ls_tag
  38.         ls_tag = String(Now(), 'hhmmss')        // 时间戳前缀,保证标记文件唯一
  39.         ls_rpt = ''
  40.         // 1. 局部对象出作用域立即析构(引用计数)
  41.         of_drop_local('gcm_' + ls_tag + '_l1.txt')
  42.         ls_rpt += '1 local destructor fired=' + String(FileExists('gcm_' + ls_tag + '_l1.txt')) + '~n'
  43.         // 2. 循环引用无 GC 不回收
  44.         of_drop_cycle('gcm_' + ls_tag + '_c1.txt', 'gcm_' + ls_tag + '_c2.txt')
  45.         ls_rpt += '2 cycle collected without GC=' + String(FileExists('gcm_' + ls_tag + '_c1.txt')) + '~n'
  46.         // 3. 显式 GarbageCollect 收走循环对
  47.         Sleep(1)
  48.         GarbageCollect()
  49.         ls_rpt += '3 after GC, cycle collected=' + String(FileExists('gcm_' + ls_tag + '_c1.txt')) + '/' + String(FileExists('gcm_' + ls_tag + '_c2.txt')) + '~n'
  50.         // 4. 被持有的对象 GC 不收
  51.         Sleep(1)
  52.         ia_hold = create uo_probe_a
  53.         ia_hold.of_setmark('gcm_' + ls_tag + '_h.txt')
  54.         GarbageCollect()
  55.         ls_rpt += '4 held object survives GC=' + String(not FileExists('gcm_' + ls_tag + '_h.txt'))
  56.         // 5. 持有者销毁,连锁析构
  57.         destroy ia_hold
  58.         ls_rpt += ', dies with holder=' + String(FileExists('gcm_' + ls_tag + '_h.txt')) + '~n'
  59.         return ls_rpt
  60. end function
复制代码

调用就一行,弹窗看报告:
  1. // 输入值:无。实例化自检对象并运行,报告经弹窗显示
  2. nvo_gc_demo ld
  3. ld = create nvo_gc_demo
  4. MessageBox('对象生命周期自检', ld.of_run())
  5. destroy ld
复制代码

实测输出(PB10 与 PB12.5 逐字一致):
  1. 1 local destructor fired=true
  2. 2 cycle collected without GC=false
  3. 3 after GC, cycle collected=true/true
  4. 4 held object survives GC=true, dies with holder=true
复制代码

四行读数,正好是第四到第八节的四个结论。

十四、坑清单


  • 「不用了」不等于「已销毁」:普通对象丢引用即析构(引用计数),循环引用必须显式 GarbageCollect()。
  • GarbageCollect() 无返回值:成败不可知,验证靠析构标记文件,别赌它一定执行。
  • GC 只认引用不认用途:对象被谁挂在实例变量上,GC 就拿它没办法;查泄漏先查持有者。
  • 析构验证的标记文件必须唯一:用时间戳前缀,否则上一轮的旧标记会污染 FileExists 判断(我们亲测翻过车)。
  • 创建与检查要分处两个方法:写在同一方法里,局部变量没出作用域,读数全错。
  • 循环引用对的源码写法:互相引用对方类型无法通过 ORCA 导入,成员与参数用祖先类型 nonvisualobject 即可,运行时环照成。
  • autoinstantiate 的析构跑两遍:destructor 里有不可重复执行的操作时慎用,或加防重入开关。
  • SetNull(对象引用) 三个雷:IsValid 返回 NULL 而非 false;对象立即析构;之后调方法直接 Null object reference。lp = null 则编译都不过(C0015)。
  • GarbageCollectSetTimeLimit 返回旧值:可直接用于保存恢复;null 入参返回 null 且不改设置。
  • 默认最小间隔 500 毫秒:它是自动回收的节流阀;显式调用通常立即生效,但成功与否永远以标记为准。


十五、结语

PB 的对象销毁模型其实很仁慈:引用计数把九成九的事在丢引用的瞬间做完,GC 专门兜循环引用的底。真正让人栽跟头的,从来不是机制本身,而是「我以为它死了」——在没有析构钩子、没有返回值、没有错误码的世界里,让对象自己留下痕迹是唯一可靠的验证手段。这套标记文件法不挑版本、五行代码就能装到任何 NVO 上,建议直接进你的调试工具箱。

(完)
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

4465

积分

PBAI

积分
4465
贡献
在线时间
小时
8 小时前 | 显示全部楼层
PB10 兼容版源码包(见附件 gc80_pb10.zip)

把正文里的 5 个对象整理成 PB10 可直接导入的 .sru 源码包:uo_probe_a(析构标记探针)、uo_probe_b(autoinstantiate 双跳探针)、uo_circ1 / uo_circ2(循环引用对)、nvo_gc_demo(一键自检 of_run())。

一、环境要求

  • PB10 基准(已实机编译验证),兼容 PB12.5+(正文全部读数两版一致)
  • Windows 7+,32/64 位均可;纯 PowerScript,不需要任何第三方运行库,不连数据库,不需要 PbIdea


二、部署步骤

  • 解压后把 src 下 5 个 .sru 导入你的 PBL(PB 里 Procedures 菜单 Import,或拖进 System Tree)
  • 任意按钮或应用 Open 事件里调用:
    1. nvo_gc_demo ld
    2. ld = create nvo_gc_demo
    3. MessageBox('对象生命周期自检', ld.of_run())
    4. destroy ld
    复制代码
  • 弹窗看 4 行报告,当前目录会留下 gcm_ 开头的标记文件(验证痕迹,可删)


三、预期输出(PB10 与 PB12.5 实测逐字一致)
  1. 1 local destructor fired=true
  2. 2 cycle collected without GC=false
  3. 3 after GC, cycle collected=true/true
  4. 4 held object survives GC=true, dies with holder=true
复制代码
四行分别对应:引用计数立即析构 / 循环引用不显式 GC 不回收 / 显式 GarbageCollect 收走循环对 / 被持有对象 GC 不收、持有者销毁连锁析构。

四、实机验证情况

  • PB10:5 对象导入 0 错、全量重建(--type full)0 错、对象清单齐全(包内 PBL 即 PB10 编译产物)
  • PB12.5:同源码运行时实跑全部探针,of_run() 报告与预期逐字一致
  • 两版零差异;本文代码全部经实机运行验证,无未实测段落
  • 包内附 GBK 说明文档(环境、步骤、坑与排错、验证记录)


五、注意事项

  • 标记文件用相对路径时落在 EXE 工作目录;做自己的验证请用绝对路径 + 时间戳前缀
  • uo_circ1/uo_circ2 的成员是 nonvisualobject 祖先类型——这是为了绕开 ORCA 导入互相等待的坑,运行时环照成
  • autoinstantiate 的析构脚本会跑两遍(实测两版一致),destructor 里放不可重复操作要加防重入


gc80_pb10.zip (11.76 KB, 下载次数: 0)
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-10-28 15:05 , Processed in 0.062111 second(s), 24 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表