WebObjects/EOF/使用 EOF/内存管理
Java 中的内存管理有点像魔术。当与 EOF 结合使用时,您需要注意一些特定问题。在 WebObjects 5.2 中,对 EOF 中的内存管理系统进行了重大更改,为 EO 快照提供了对弱引用的支持,而不仅仅依赖于以前的引用计数系统。这为 5.2 及更高版本中相同条件下具有相同应用程序的应用程序提供了大大优化的内存性能,因为在许多情况下,引用将由 Java 的内存系统释放,而不是由您的 EOEditingContext 保持。
关于 缓存和新鲜度 的内容与内存管理的内容有很大重叠。您应该阅读这两部分内容,以充分了解缓存和内存管理应如何在您的应用程序设计中发挥作用。
在采取任何措施之前,需要考虑的一个重要问题是您的应用程序是否真的需要比您提供的更多内存。默认情况下,Java 虚拟机为 Java 应用程序提供 64M 的内存。对于企业 Web 应用程序来说,这并不是很多内存。您可以在启动时设置 VM 的“mx”标志来调整它(-Xmx256M 会为 VM 提供 256M 的堆,而不是默认的 64M)。除了在代码中漫无目的地寻找之外,分析您的应用程序是真正确定应用程序内存状况的唯一方法。
以下分析工具已被验证可与 WebObjects 一起使用
- JProfiler(http://www.ej-technologies.com/products/jprofiler/overview.html)对 WebObjects 有明确的支持。它可以自动更新您的 WebObjects 应用程序启动脚本,以便在运行时插入自身,允许您远程连接到应用程序并对其进行分析。
使用 原始行 可以显着减少您的内存使用量(并提高应用程序的性能),但以便利性为代价。与普通 EO 相比,这种方法使用的内存更少,对提取限制的支持更好,并且您仍然可以在需要时将它们提升为完整的 EO。查看 原始行 主题以了解更多信息。
如果您在提取时不小心,可能会引入大量数据集。当使用 EOFetchSpecification 时,请务必指定限制最严格的 EOQualifier,该限定符仍能返回您需要的數據。否则,您可能会提取比应用程序使用的更多数据,由于必须从数据库传输数据,从而降低整体应用程序性能。
通常会有代表“枚举类型”的 EO。例如,您可能有一个错误跟踪系统,该系统具有严重性实体(具有“严重错误”、“轻微错误”等各个实例),所有错误都引用该实体。很容易创建反向引用的关系(即 Bug=>严重性,严重性=>>Bug)。但是,请注意,如果您从严重性创建到 Bug 的引用,那么在添加 Bug 并正确使用 addObjectToBothSidesOfRelationshipWithKey 方法时,EOF 会在将新 Bug 添加到列表末尾之前,将 Severity 上的整个“错误”关系错误引入内存。这可能会对您的应用程序造成破坏。随着错误数量的增加,添加新错误将花费越来越长时间。建议的最佳做法是只创建 Bug=>严重性,而不是创建反向关系。如果您需要执行该提取,可以手动构建 EOFetchSpecification,而不是使用自动方法。
请注意,这是针对 EOF 的已知问题。EOF 可以使用的一种方法是,只有在错误关系已经错误引入内存时,才将对象添加到 Severity 的错误关系中。如果数组尚未错误引入,那么在插入时导致错误可能会非常昂贵。当前行为的原因是为了保持在保存更改之前访问新提交的 Bug 的能力,通过 Severity 错误关系(即从数据库引入关系不会立即返回新插入的 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 开始,如果你使用会话 defaultEditingContext,WOSession 会将默认撤销堆栈大小设置为 10,如果使用 WODEFAULTUNDOSTACKLIMIT 参数没有指定默认大小。
最后,你可以在完成操作后通过调用以下方法手动清除你的 NSUndoManager:
editingContext.undoManager().removeAllActions();
如果你使用 NSUndoManager,建议你在执行涉及大量数据的非常大的 saveChanges 操作后调用它。
这是对 5.3 中当前行为的描述,一个人可能会通过反编译和审查整个过程来收集,当然,我永远不会这样做或纵容它 - 但是如果你真的做了,你可能会收集到确切的信息,这些信息在 5.2 版本说明中得到了很好的记录(并且似乎符合规范)
从 5.2 开始,它的工作方式是:EODatabase 中的快照具有引用计数。每个获取 EO 的编辑上下文都会增加引用计数。EC 通过弱引用持有该 EO。当弱引用被回收时,快照引用计数可能会减少(注意是可以,而不是立即就会 - 编辑上下文保留引用队列,该队列仅定期处理)。当计数降至零时,数据库会忘记快照。如果启用了实体缓存,则 EODatabase 会忽略引用计数(或将其保持在“1”作为最小值),并且它不会在只读场景中消失。如果你修改了该类型的任何实体并在你的 EditingContext 中调用 saveChanges,则缓存实体的缓存将被完全刷新。(注意:请记住这一点,因为如果你正在缓存大量可写数据,它不会很聪明地更新该缓存 - 它会在每次更新时被清除,然后它会在下一次访问时立即重新加载该实体的整个对象集)
如果在你的编辑上下文中启用了 retainsAllRegisteredObjects,它不会使用弱引用。在这种情况下,EO 引用计数仅在以下情况下减少:1) 你处置了编辑上下文,或 2) 你忘记或使对象无效。
当你在编辑上下文中修改一个对象时,编辑上下文会保留对该对象的强引用,直到你调用 saveChanges(或还原、重置等),此时强引用会被清除,唯一剩下的引用就是之前那样的弱引用。
如果启用了撤销管理器,它会保留对受影响 EO 的强引用,只要撤销存在。
我确实想知道 EC 是否应该使用软引用而不是弱引用...... 似乎对那些 EO 的用户更友好。
如果你使用的是 5.2 之前的 WO,那么所有弱引用相关内容都不适用,所有内容都是纯粹使用快照引用计数完成的 - 它应该表现得像 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。这似乎是不必要的...... 但它有效。
我的猜测是内存增加是由于快照造成的,据我所知,快照永远不会消失,而且在我看来,这不算是一个错误。
为了处理我遇到的一个永远导入问题,我不想使用笨拙的“新 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必须是同一个实体,keypaths才能工作!