WebObjects/EOF/使用 EOF/内存管理
Java 中的内存管理有点像黑色艺术。当与 EOF 结合使用时,您需要注意某些问题。在 WebObjects 5.2 中,EOF 的内存管理系统发生了重大变化,为 EO 快照提供了对弱引用的支持,而不仅仅依赖于以前的引用计数系统。这在 5.2 及更高版本中,在相同条件下使用相同应用程序时,提供了明显更好的内存性能,因为在许多情况下,引用将由 Java 的内存系统释放,而不是由您的 EOEditingContext 保持。
关于 缓存和新鲜度 的内容与内存管理的内容有很大的重叠。两者都应该阅读,以充分了解缓存和内存管理如何在您的应用程序设计中发挥作用。
在采取任何激进措施之前,要考虑的一个重要问题是,您的应用程序是否真的只需要比您提供的更多的内存。默认情况下,Java 虚拟机为 Java 应用程序提供 64M 的内存。对于企业 Web 应用程序来说,这并不多。您可以在启动时在 VM 上设置 “mx” 标志来调整此值(-Xmx256M 将为您提供 256M 的堆空间,而不是默认的 64M)。除了在代码中漫无目的地寻找之外,分析您的应用程序是真正确定应用程序内存情况的唯一方法。
以下分析工具已被验证与 WebObjects 兼容
- JProfiler ( http://www.ej-technologies.com/products/jprofiler/overview.html ) 对 WebObjects 有明确的支持。它可以自动更新您的 WebObjects 应用程序启动脚本,以便在运行时插入自身,以便您远程连接到应用程序并进行分析。
使用 原始行 可以显著减少内存使用量(并提高应用程序性能),但代价是便利性。与普通 EO 相比,它使用更少的内存,更好地支持获取限制,您仍然可以在需要时将其提升为完整的 EO。查看 原始行 主题以了解更多信息。
如果您在获取时不小心,可能会引入庞大的数据集。当您使用 EOFetchSpecification 时,请注意指定最严格的 EOQualifier,它仍然可以返回您需要的数据。否则,您可能会引入比应用程序使用的更多数据,并且由于必须从数据库传输的数据,您的整体应用程序性能会下降。
使用代表 “枚举类型” 的 EO 非常常见。例如,您可能有一个错误跟踪系统,它具有一个 Severity 实体(具有 “Crasher”、 “Minor” 等的单个实例),系统中的所有错误都会引用该实体。始终创建关系反向引用(即 Bug=>Severity、Severity=>>Bug)很诱人。但是,请注意,如果您从 Severity 创建对 Bug 的引用,那么每当添加一个 Bug 时,并且您正确使用了 addObjectToBothSidesOfRelationshipWithKey 方法,EOF 都会在将新错误添加到列表末尾之前,在 Severity 上引入整个 “bugs” 关系。这可能会对您的应用程序造成灾难性的影响。随着错误数量的增加,添加新的错误将花费越来越长的时间。建议的最佳做法是只创建 Bug=>Severity,而不是创建反向关系。如果您需要执行该获取,可以手动构建 EOFetchSpecification,而不是使用自动方法。
请注意,这是一个针对 EOF 的已知 bug。EOF 可以使用的一种方法是,只有在 bugs 关系已被引入内存时才将对象添加到 Severity 的 bugs 关系中。如果数组尚未被引入,那么在插入时导致引入可能会非常昂贵。当前行为的原因是为了保持在保存更改之前访问新提交的 Bug 的能力(即,从数据库引入关系还不会返回新插入的 Bug)。这对于在保存更改之前维护对象图的一致性非常重要,但也带来了很高的性能成本。
在您的 EOEditingContext 上调用 dispose() 并不是严格要求的。当 EOEditingContext 不再被引用时,它将自动被释放。但是,问题是 GC 需要多长时间才能访问您的对象。如果您手动调用 dispose(),您不会造成任何伤害,并且会立即减少快照引用计数。如果您将 EC 放出作用域或将其设置为 null,那么谁先访问它可能是一个问题——GC 最终化您的 EC 或您手动调用 dispose()。建议您在单个方法中执行长时间运行的进程时,在该方法中 EOEditingContext 不会在方法结束之前超出作用域,这时您应该释放您的 EOEditingContext。
调用 System.gc() 通常是一个不好的做法,会导致应用程序性能问题。此外,System.gc() 不是一个命令,而是一个请求,因此如果您的应用程序依赖于此命令执行,您应该重新评估应用程序的设计,因为您可能会遇到相关问题。
调用 editingContext.invalidateObject(..) 或 editingContext.invalidateAllObjects() 来解决内存问题可能很诱人。请避免这样做。从长远来看,这样做只会伤害自己。失效操作非常激进,如果另一个 EOEditingContext 中的人正在修改你在你的 EOEditingContext 中失效的 EO,则会抛出异常(共享快照将被销毁,导致在另一个上下文中的 .saveChanges 抛出异常)。它应该只在您了解该方法含义的非常受控的情况下使用。
关于失效对象,这是一件应该避免的事情。我承认,我有几次陷入绝望,使用了这个方法,但只有悔恨和保留。
我只是不喜欢使用 invalidateAllObjects(),因为对我来说,它带来的问题比解决的问题更多。
我使用这个方法的几次,后来都后悔了,因为它最终让我陷入困境。最大的问题是你丢弃了人们可能正在编辑的快照,这会导致非常奇怪的问题。这是一个非常笨拙的方法。
人们在使用 WebObjects 时遇到的最常见的内存问题之一是 NSUndoManager。WebObjects 有一个特别酷的功能,它支持 EO 事务的撤销堆栈。每次你调用 saveChanges 时,你都会将一组可撤销的更改推送到堆栈中。这种行为的缺点是,你最终会对旧的 EO 状态产生强引用,这在大型 EOF 操作中可能是致命的。
对此有几种解决方法。一种是简单地通过调用以下方法在您的编辑上下文中禁用 NSUndoManager:
editingContext.setUndoManager(null);
有资料表明,在某些情况下,将撤销管理器设置为 null 可能会导致删除问题。在使用 WebObjects 5.3 测试时,无法重现这种情况,但如果您使用 WebObjects 5.2 或更低版本,则应注意您可能会遇到问题。
在这种情况下,推荐的替代方法是通过调用以下方法禁用撤销事件的注册:
editingContext.undoManager().disableUndoRegistration();
请注意,使用 setLevelsOfUndo() 将撤销级别设置为 0 不会起作用,因为 0 表示无限制!
在 5.2 之前,NSUndoManager 默认情况下实际上具有无限撤销级别。WO 5.3 的 javadoc 仍然提到以下内容:
EOEditingContext 的撤销支持是任意深度的;您可以反复撤销一个对象,直到您将其恢复到它首次创建或提取到其编辑上下文中的状态。即使在保存之后,您也可以撤销更改。为了支持此功能,NSUndoManager 可以在内存中保存大量数据。
但是,从 5.2 开始,如果您使用的是会话默认的 editingContext,WOSession 会将默认的撤销堆栈大小设置为 10(如果 WODEFAULTUNDOSTACKLIMIT 参数没有指定默认大小)。
最后,您可以通过调用以下方法在完成操作后手动清除您的 NSUndoManager:
editingContext.undoManager().removeAllActions();
如果您使用 NSUndoManager,建议您在执行涉及大量数据的非常大的 saveChanges 后调用此方法。
这是对 5.3 中当前行为的描述,如果有人要(比如)反编译并审查整个过程,他们可能能够收集到这些信息——当然,我绝不会这样做或纵容它——但如果你这样做,你可能会准确地收集到这些信息,这些信息在 5.2 版本说明中已经记录得很好(并且似乎按规格运行)。
从 5.2 开始,它的工作原理是:EODatabase 中的快照有一个引用计数。每个提取 EO 的编辑上下文都会增加引用计数。EC 通过 WeakReference 保持对该 EO 的引用。当 WeakReference 被回收时,快照引用计数可能会减少(注意 CAN,而不是 IMMEDIATELY WILL——编辑上下文保留引用队列,该队列只定期处理)。当计数降至零时,数据库会忘记快照。如果您启用了实体缓存,则 EODatabase 会忽略引用计数(或将其保留为“1”作为最小值),并且在只读场景中不会消失。如果您修改了该类型的任何实体并在您的 EditingContext 中调用 saveChanges,则缓存的实体的缓存将被完全刷新。(注意:请记住这一点,因为如果您正在缓存大量可写的数据,它在更新该缓存方面不会非常智能——它会在每次更新时被清除,然后它会在下一次访问时立即重新加载该实体的整个对象集。)
如果您在编辑上下文中启用了 retainsAllRegisteredObjects,它将不会使用 WeakReferences。在这种情况下,EO 引用计数只会在以下两种情况下减少:1)您处置编辑上下文或 2)您忘记或失效了该对象。
当您在编辑上下文中修改一个对象时,编辑上下文会一直保持对该对象的强引用,直到您调用 saveChanges(或 revert、reset 等),此时强引用将被清除,而唯一剩下的引用就是之前的 weakreference。
如果您启用了撤销管理器,它会一直保持对受影响的 EO 的强引用,只要撤销操作存在。
我确实想知道 EC 是否应该使用 SoftReferences 而不是 WeakReferences……这似乎对那些 EO 的用户更友好。
如果您使用的是 WO 5.2 之前的版本,那么所有 WeakReference 内容都不适用,所有操作都是纯粹通过快照引用计数完成的——它的行为应该与 5.2 中的 retainsAllRegisteredObjects = true 相同。
在 5.2 之前,所有快照都只是由 EOEditingContext 引用计数。如果一个 EO 被错误地导入到您的 EOEditingContext 中,它将不会消失,直到 EOEditingContext 本身被处置。因此,如果您在 5.2 之前的 WebObjects 上部署了一个应用程序,您将需要采用定期处置您的 EOEditingContext 并创建一个新的 EOEditingContext 的策略。
根据我们的经验,EOF 在这类批量操作方面不太好。如果我们有很多事情要做,而且不需要 EO 的逻辑,我们会进入 EOAccess 级别,并编写一些批处理插入语句。您甚至可能想考虑直接使用 JDBC。
好吧,这就是说,
- 定期保存
- 不要保留对已插入对象的引用
- 如果这样做不起作用,请尝试定期创建一个新的 EC
- 有点笨拙,但 ec.invalidateAllObjects() 应该也会转储所有快照。
一些有用的评论:http://lists.apple.com/archives/webobjects-dev/2004/Sep/msg00225.html http://lists.apple.com/archives/webobjects-dev/2004/Sep/msg00228.html
阅读 EOF 文档总是有趣的,JavaDocs 只针对 API,而 EOF 文档包含很多有价值的信息。http://developer.apple.com/documentation/WebObjects/Enterprise_Objects/index.html
“EOEditingContexts 使用弱引用来引用注册到它们身上的 EOEnterpriseObjects。… EOEditingContexts 通过强引用来保存所有插入、删除或更新的对象。这些强引用被 EOEditingContext 方法 saveChanges… 清除——http://developer.apple.com/documentation/WebObjects/Enterprise_Objects/Managing/chapter_7_section_9.html
关于定期创建一个新的 EC,我会直接跳到这个选项(根据我的经验)。我已经完成了一些类似的任务,发现确保您的应用程序不会膨胀的最佳方法是定期放弃 EC 并创建一个新的 EC。这似乎不应该有必要……但这确实有效。
我的猜测是内存增加是由于快照造成的,据我所知,快照永远不会消失,而且在我看来不算是 bug。
为了处理一个永远导入的问题(我没有使用强硬的“新 EC”计划),我编写了这个方法,它可以减少快照的数量。
public static void forgetObjectsAndKeypaths(EOEditingContext ec, NSArray eos, NSArray keypaths) { Enumeration eoEnum = eos.objectEnumerator(); while (eoEnum.hasMoreElements()) { EOGenericRecord eo = (EOGenericRecord) eoEnum.nextElement(); Enumeration keypathEnum = keypaths.objectEnumerator(); while (keypathEnum.hasMoreElements()) { String keypath = (String) keypathEnum.nextElement(); EOFaulting faulting = (EOFaulting) eo.valueForKeyPath(keypath); if (faulting != null && !faulting.isFault()) { if (eo.isToManyKey(keypath)) { NSArray relEos = (NSArray) eo.valueForKeyPath(keypath); EOHelper.forgetObjects(ec, relEos); } else { ec.forgetObject((EOCustomObject) eo.valueForKeyPath(keypath)); } } } ec.forgetObject(eo); } }
您可以像这样使用它。
EOHelper.forgetObjectsAndKeypaths(ec, arrayOfEOsToForget, new NSArray(new String[]{"rel1", "rel2"});
当然,数组中的所有 EO 都需要是同一个实体,才能使键路径起作用!