跳轉到內容

WebObjects/EOF/使用 EOF/記憶體管理

來自華夏公益教科書,開放的書籍,開放的世界

Java 中的記憶體管理有點像黑色藝術。當與 EOF 結合使用時,您需要注意某些問題。在 WebObjects 5.2 中,EOF 的記憶體管理系統發生了重大變化,為 EO 快照提供了對弱引用的支援,而不僅僅依賴於以前的引用計數系統。這在 5.2 及更高版本中,在相同條件下使用相同應用程式時,提供了明顯更好的記憶體效能,因為在許多情況下,引用將由 Java 的記憶體系統釋放,而不是由您的 EOEditingContext 保持。

關於 快取和新鮮度 的內容與記憶體管理的內容有很大的重疊。兩者都應該閱讀,以充分了解快取和記憶體管理如何在您的應用程式設計中發揮作用。

一般問題

[編輯 | 編輯原始碼]

在採取任何激進措施之前,要考慮的一個重要問題是,您的應用程式是否真的只需要比您提供的更多的記憶體。預設情況下,Java 虛擬機器為 Java 應用程式提供 64M 的記憶體。對於企業 Web 應用程式來說,這並不多。您可以在啟動時在 VM 上設定 “mx” 標誌來調整此值(-Xmx256M 將為您提供 256M 的堆空間,而不是預設的 64M)。除了在程式碼中漫無目的地尋找之外,分析您的應用程式是真正確定應用程式記憶體情況的唯一方法。

以下分析工具已被驗證與 WebObjects 相容

原始行

[編輯 | 編輯原始碼]

使用 原始行 可以顯著減少記憶體使用量(並提高應用程式效能),但代價是便利性。與普通 EO 相比,它使用更少的記憶體,更好地支援獲取限制,您仍然可以在需要時將其提升為完整的 EO。檢視 原始行 主題以瞭解更多資訊。

限定你的獲取

[編輯 | 編輯原始碼]

如果您在獲取時不小心,可能會引入龐大的資料集。當您使用 EOFetchSpecification 時,請注意指定最嚴格的 EOQualifier,它仍然可以返回您需要的資料。否則,您可能會引入比應用程式使用的更多資料,並且由於必須從資料庫傳輸的資料,您的整體應用程式效能會下降。

從列舉型別到其他 EO 的關係

[編輯 | 編輯原始碼]

使用代表 “列舉型別” 的 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)。這對於在儲存更改之前維護物件圖的一致性非常重要,但也帶來了很高的效能成本。

Dispose()ing your EOEditingContext

[編輯 | 編輯原始碼]

在您的 EOEditingContext 上呼叫 dispose() 並不是嚴格要求的。當 EOEditingContext 不再被引用時,它將自動被釋放。但是,問題是 GC 需要多長時間才能訪問您的物件。如果您手動呼叫 dispose(),您不會造成任何傷害,並且會立即減少快照引用計數。如果您將 EC 放出作用域或將其設定為 null,那麼誰先訪問它可能是一個問題——GC 最終化您的 EC 或您手動呼叫 dispose()。建議您在單個方法中執行長時間執行的程序時,在該方法中 EOEditingContext 不會在方法結束之前超出作用域,這時您應該釋放您的 EOEditingContext。

呼叫 System.gc()

[編輯 | 編輯原始碼]

呼叫 System.gc() 通常是一個不好的做法,會導致應用程式效能問題。此外,System.gc() 不是一個命令,而是一個請求,因此如果您的應用程式依賴於此命令執行,您應該重新評估應用程式的設計,因為您可能會遇到相關問題。

關於 editingContext.invalidateAllObjects() 的軼事

[編輯 | 編輯原始碼]

呼叫 editingContext.invalidateObject(..) 或 editingContext.invalidateAllObjects() 來解決記憶體問題可能很誘人。請避免這樣做。從長遠來看,這樣做只會傷害自己。失效操作非常激進,如果另一個 EOEditingContext 中的人正在修改你在你的 EOEditingContext 中失效的 EO,則會丟擲異常(共享快照將被銷燬,導致在另一個上下文中的 .saveChanges 丟擲異常)。它應該只在您瞭解該方法含義的非常受控的情況下使用。

Chuck Hill
[編輯 | 編輯原始碼]

關於失效物件,這是一件應該避免的事情。我承認,我有幾次陷入絕望,使用了這個方法,但只有悔恨和保留。

Art Isbell
[編輯 | 編輯原始碼]

我只是不喜歡使用 invalidateAllObjects(),因為對我來說,它帶來的問題比解決的問題更多。

Mike Schrag
[編輯 | 編輯原始碼]

我使用這個方法的幾次,後來都後悔了,因為它最終讓我陷入困境。最大的問題是你丟棄了人們可能正在編輯的快照,這會導致非常奇怪的問題。這是一個非常笨拙的方法。

NSUndoManager

[編輯 | 編輯原始碼]

人們在使用 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 後呼叫此方法。

WebObjects 5.2+

[編輯 | 編輯原始碼]

這是對 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 相同。

WebObjects 5.2 之前

[編輯 | 編輯原始碼]

在 5.2 之前,所有快照都只是由 EOEditingContext 引用計數。如果一個 EO 被錯誤地匯入到您的 EOEditingContext 中,它將不會消失,直到 EOEditingContext 本身被處置。因此,如果您在 5.2 之前的 WebObjects 上部署了一個應用程式,您將需要採用定期處置您的 EOEditingContext 並建立一個新的 EOEditingContext 的策略。

Chuck Hill

[編輯 | 編輯原始碼]

根據我們的經驗,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

Alan Ward

[編輯 | 編輯原始碼]

關於定期建立一個新的 EC,我會直接跳到這個選項(根據我的經驗)。我已經完成了一些類似的任務,發現確保您的應用程式不會膨脹的最佳方法是定期放棄 EC 並建立一個新的 EC。這似乎不應該有必要……但這確實有效。

Ken Anderson

[編輯 | 編輯原始碼]

我的猜測是記憶體增加是由於快照造成的,據我所知,快照永遠不會消失,而且在我看來不算是 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 都需要是同一個實體,才能使鍵路徑起作用!

華夏公益教科書