記憶體回收
Xamarin.Android 使用 Mono 的 簡單世代垃圾收集行程。 這是具有兩代代和 大型物件空間的標記和掃掠垃圾收集行程,具有兩種收集:
- 次要集合 (收集 Gen0 堆積)
- 主要集合(收集 Gen1 和大型物件空間堆積)。
注意
如果沒有透過 GC 的明確集合 。Collect() 集合是 隨選的,根據堆積配置。 這不是參考計數系統;一旦沒有未完成的參考,或範圍已結束時,就不會收集物件。 當次要堆積因新配置而記憶體不足時,GC 將會執行。 如果沒有配置,則不會執行。
次要集合便宜且頻繁,用來收集最近配置和無效的物件。 次要集合會在每幾 MB 的已配置對象之後執行。 呼叫 GC 可以手動執行 次要集合。收集 (0)
主要集合昂貴且較不頻繁,可用來回收所有無效的物件。 一旦記憶體耗盡目前的堆積大小,就會執行主要集合(重設大小堆積之前)。 呼叫 GC 可以手動執行主要集合。收集 () 或呼叫 GC。使用自變數 GC 收集 (int)。MaxGeneration。
跨 VM 物件集合
物件類型有三種類別。
Managed 物件:不會繼承自 Java.Lang.Object 的類型,例如 System.String。 這些通常是由 GC 收集。
Java 物件:Android 運行時間 VM 記憶體在 Java 類型,但不會公開至 Mono VM。 這些都是無聊的,不會進一步討論。 這些通常是由Android運行時間 VM 收集。
對等對象:實 作 IJavaObject 的類型,例如所有 Java.Lang.Object 和 Java.Lang.Throwable 子類別。 這些類型的實例有兩個「半」受控 對等 和 原生對等。 受控對等是 C# 類別的實例。 原生對等是 Android 運行時間 VM 內 Java 類別的實例,而 C# IJavaObject.Handle 屬性包含原生對等的 JNI 全域參考。
原生對等有兩種類型:
架構對等 :不知道 Xamarin.Android 的「一般」Java 類型,例如 android.content.Context。
使用者對等 : 應用程式內每個 Java.Lang.Object 子類別的建置時間所產生的 Android 可呼叫包裝函 式。
由於 Xamarin.Android 程式中有兩部 VM,因此有兩種類型的垃圾收集:
- Android 運行時間集合
- Mono 集合
Android 運行時間集合的運作正常,但請注意:JNI 全域參考會被視為 GC 根目錄。 因此,如果 Android 運行時間 VM 物件上有 JNI 全域參考,即使該物件符合收集資格,也無法收集物件。
Mono 集合是樂趣發生的地方。 Managed 物件通常會收集。 執行下列程式會收集對等物件:
符合Mono集合資格的所有Peer對象都會將其 JNI 全域參考取代為 JNI 弱式全域參考。
會叫用 Android 執行時間 VM GC。 可以收集任何原生對等實例。
檢查在 (1) 中建立的 JNI 弱式全域參考。 如果收集弱式參考,則會收集Peer物件。 如果尚未收集弱式參考,則弱式參考會取代為 JNI 全域參考,而且不會收集 Peer 物件。 注意:在 API 14+ 上,這表示從
IJavaObject.Handle
傳回的值可能會在 GC 之後變更。
這一切的最終結果是,只要 Managed 程式代碼(例如儲存在變數中 static
)或 Java 程式代碼所參考,Peer 對象的實例就會存留。 此外,原生對等的存留期將會延伸至其存留時間之外,因為原生對等在原生對等和 Managed 對等皆可收集之前,將無法收集。
物件週期
對等物件會以邏輯方式存在於 Android 運行時間和 Mono VM 內。 例如, Android.App.Activity 受控對等實例會有對應的 android.app.Activity 架構對等 Java 實例。 繼承自 Java.Lang.Object 的所有物件都預期在這兩個 VM 中都有表示法。
在這兩個 VM 中具有表示法的所有物件,都會有與只存在於單一 VM 內的物件相較之下延伸的存留期(例如 , System.Collections.Generic.List<int>
。
呼叫 GC。Collect 不一定會收集這些對象,因為 Xamarin.Android GC 必須在收集該物件之前,確保該物件不會由任一個 VM 參考。
若要縮短物件存留期,應該叫用 Java.Lang.Object.Dispose()。 這樣會藉由釋放全域參考,手動「將」兩個 VM 之間的物件連線「斷斷」,讓對象能夠更快速地收集。
自動集合
從 4.1.0 版開始,Xamarin.Android 會在超過 gref 閾值時自動執行完整的 GC。 此閾值是平臺已知最大 gref 的 90%:模擬器上 1800 個 grefs (2000 max), 硬體上 46800 個 grefs (最大值 52000)。 注意:Xamarin.Android 只會計算 Android.Runtime.JNIEnv 所建立的 gref,而且不會知道進程中建立的任何其他 gref。 這隻是啟發學習法。
執行自動收集時,會將類似下列的訊息列印至偵錯記錄檔:
I/monodroid-gc(PID): 46800 outstanding GREFs. Performing a full GC!
這種情況的發生不具決定性,而且可能會在不合時宜的時間發生(例如在圖形轉譯的中間)。 如果您看到此訊息,您可能會想要在其他地方執行明確的集合,或想要嘗試 減少對等物件的存留期。
GC 網橋選項
Xamarin.Android 提供使用 Android 和 Android 執行時間的透明記憶體管理。 它會實作為稱為 GC Bridge 之 Mono 垃圾收集行程的延伸模組。
GC Bridge 會在 Mono 垃圾收集期間運作,並找出哪些對等物件需要其使用 Android 運行時間堆積驗證其「活躍度」。 GC 網橋會執行下列步驟來進行此判斷(依序):
將無法連線之對等物件的單一參考圖表引發至它們所代表的 Java 物件。
執行 Java GC。
確認哪些物件真的已失效。
這個複雜的程式可讓 的子類別 Java.Lang.Object
自由參考任何物件;它會移除Java物件可以繫結至 C# 的任何限制。 由於這種複雜性,網橋程式可能非常昂貴,而且可能會導致應用程式中明顯的暫停。 如果應用程式發生重大暫停,值得調查下列三個 GC 網橋實作之一:
Tarjan - 以 Robert Tarjan 演算法和向後參考傳播為基礎的 GC 橋全新設計。 它在我們的模擬工作負載下具有最佳效能,但也具有實驗性程序代碼的較大份額。
新增 - 原始程序代碼的重大改革,修正兩個二次行為實例,但保留核心演算法(根據 Kosaraju 的演算法 尋找強連接元件)。
舊 - 原始實作(視為三者中最穩定)。 這是應用程式在可接受暫停時
GC_BRIDGE
應該使用的網橋。
找出哪一個 GC 網橋最適合的方法,是在應用程式中進行實驗和分析輸出。 有兩種方式可用來收集數據以進行效能評定:
啟用記錄 - 針對每個 GC Bridge 選項啟用記錄(如組態一節所述),然後擷取並比較每個設定的記錄輸出。
GC
檢查每個選項的訊息;特別是GC_BRIDGE
訊息。 非互動式應用程式的暫停最多 150 毫秒是可容忍的,但對於非常互動式的應用程式(例如遊戲)暫停超過 60 毫秒是個問題。啟用網橋會計 - 網橋會計 會顯示橋接流程中每個物件所指向之物件的平均成本。 依大小排序這項資訊將提供保留最大額外物件的提示。
默認設定為 Tarjan。 如果您發現回歸,可能會發現必須將此選項設定為 Old。 此外,如果 Tarjan 未產生效能改善,您可以選擇使用更穩定的 Old 選項。
若要指定應用程式應該使用的選項 GC_BRIDGE
、傳遞 bridge-implementation=old
或 bridge-implementation=new
bridge-implementation=tarjan
至 MONO_GC_PARAMS
環境變數。 這可藉由使用 的建置動作AndroidEnvironment
,將新檔案新增至專案來完成。 例如:
MONO_GC_PARAMS=bridge-implementation=tarjan
如需詳細資訊,請參閱組態。
協助 GC
有多種方式可協助 GC 減少記憶體使用量和收集時間。
處置對等實例
GC 有進程不完整的檢視,而且記憶體不足時可能無法執行,因為 GC 不知道記憶體不足。
例如,Java.Lang.Object 型別或衍生型別的實例大小至少為 20 個字節(可能會變更而不通知等等)。 Managed 可呼叫包裝函 式不會新增其他實例成員,因此當您有一個 參照記憶體 10 MB Blob 的 Android.Graphics.Bitmap 實例時,Xamarin.Android 的 GC 將不知道 - GC 會看到 20 字組的物件,而且無法判斷它已連結到 Android 運行時間配置的物件,讓記憶體保持運作 10 MB。
經常需要協助 GC。 不幸的是, GC。AddMemoryPressure() 和 GC。不支援 RemoveMemoryPressure() ,因此如果您 知道 您剛釋放大型 Java 配置的物件圖形,您可能需要手動呼叫 GC。Collect() 提示 GC 釋放 Java 端記憶體,或者您可以明確處置 Java.Lang.Object 子類別,打破 Managed 可呼叫包裝函式與 Java 實例之間的對應。
注意
處置子類別實例時Java.Lang.Object
,您必須非常小心。
若要將記憶體損毀的可能性降到最低,請在呼叫 Dispose()
時觀察下列指導方針。
在多個線程之間共用
如果 Java 或受控實例可以在多個線程之間共用,則不應該Dispose()
永遠共用它。 例如,Typeface.Create()
可能會傳回快 取的實例。 如果多個線程提供相同的自變數,它們將取得 相同的 實例。 因此, Dispose()
從一個線程的 Typeface
實例可能會使其他線程失效,這可能會導致 ArgumentException
來自 JNIEnv.CallVoidMethod()
(以及其他線程),因為實例已從另一個線程處置。
處置系結的Java類型
如果實例是系結的 Java 類型,只要實例不會從 Managed 程式代碼重複使用,而且 Java 實例無法在線程之間共用,就可以處置實例。Typeface.Create()
(作出這一決定可能很困難。下次 Java 實例進入 Managed 程式代碼時, 將會為其建立新的 包裝函式。
當涉及到 Drawables 和其他大量資源實例時,這通常很有用:
using (var d = Drawable.CreateFromPath ("path/to/filename"))
imageView.SetImageDrawable (d);
上述是安全的,因為 Drawable.CreateFromPath() 傳回的對等會參考 Framework 對等,而不是使用者對等。 區塊Dispose()
結尾的using
呼叫會中斷 Managed Drawable 和架構 Drawable 實例之間的關聯性,讓 Java 實例在 Android 運行時間需要時立即收集。 如果對等實例參照至使用者對等,則這不會是安全的;在此我們使用「外部」資訊來知道Drawable
無法參考使用者對等,因此Dispose()
呼叫是安全的。
處置其他類型
如果實例參考的類型不是 Java 類型的系結(例如自定義 Activity
),除非您知道該實例上不會呼叫覆寫的方法,否則請勿呼叫 Dispose()
。
無法這麼做會導致 NotSupportedException
s。
例如,如果您有自定義的點選接聽程式:
partial class MyClickListener : Java.Lang.Object, View.IOnClickListener {
// ...
}
您 不應該 處置此實體,因為 Java 未來會嘗試在它上叫用方法:
// BAD CODE; DO NOT USE
Button b = FindViewById<Button> (Resource.Id.myButton);
using (var listener = new MyClickListener ())
b.SetOnClickListener (listener);
使用明確檢查以避免例外狀況
如果您已實 作 Java.Lang.Object.Dispose 多載方法,請避免接觸涉及 JNI 的物件。 這樣做可能會建立雙重 處置 的情況,讓您的程式代碼能夠(嚴重地)嘗試存取已垃圾收集的基礎 Java 物件。 這樣做會產生類似下列的例外狀況:
System.ArgumentException: 'jobject' must not be IntPtr.Zero.
Parameter name: jobject
at Android.Runtime.JNIEnv.CallVoidMethod
當第一次處置物件時,通常會發生此情況,導致成員變成 Null,然後在此 Null 成員上進行後續的存取嘗試會導致擲回例外狀況。 具體來說,物件的 Handle
(將受控實例連結到其基礎 Java 實例)在第一次處置時失效,但 Managed 程式代碼仍會嘗試存取此基礎 Java 實例,即使它已無法使用(如需 Java 實例與受控實例之間對應的詳細資訊,請參閱 受控可呼叫包裝函 式)。
若要防止此例外狀況,最好在方法中 Dispose
明確確認受控實例與基礎 Java 實例之間的對應仍然有效;也就是說,在存取其成員之前,請檢查物件 Handle
是否為 Null (IntPtr.Zero
)。 例如,下列 Dispose
方法會 childViews
存取物件:
class MyClass : Java.Lang.Object, ISomeInterface
{
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
for (int i = 0; i < this.childViews.Count; ++i)
{
// ...
}
}
}
如果初始處置傳遞造成childViews
無效,迴圈for
存取將會擲回 ArgumentException
Handle
。 藉由在第一次childViews
存取之前新增明確的 Handle
Null 檢查,下列Dispose
方法可防止發生例外狀況:
class MyClass : Java.Lang.Object, ISomeInterface
{
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
// Check for a null handle:
if (this.childViews.Handle == IntPtr.Zero)
return;
for (int i = 0; i < this.childViews.Count; ++i)
{
// ...
}
}
}
減少參考的實例
每當 GC 期間掃描類型或子類別的 Java.Lang.Object
實例時,也必須掃描實例所參考的整個 物件圖形 。 物件圖形是「根實例」所參考的物件實例集合, 加上 根實例所參考的所有專案,以遞歸方式參考。
請考慮下列 類別:
class BadActivity : Activity {
private List<string> strings;
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
strings.Value = new List<string> (
Enumerable.Range (0, 10000)
.Select(v => new string ('x', v % 1000)));
}
}
建構時BadActivity
,物件圖形會包含10004個實例(1xBadActivity
、1x、1x、1x、strings
1x string[]
strings
、1000x字串實例),這一切在掃描實例時BadActivity
都需要掃描。
這可能會對您的收集時間造成有害影響,導致 GC 暫停時間增加。
您可以藉由 減少 使用者對等實例所根目錄的物件圖形大小,來協助 GC。 在上述範例中,您可以移至 BadActivity.strings
不繼承自 Java.Lang.Object 的個別類別來完成:
class HiddenReference<T> {
static Dictionary<int, T> table = new Dictionary<int, T> ();
static int idgen = 0;
int id;
public HiddenReference ()
{
lock (table) {
id = idgen ++;
}
}
~HiddenReference ()
{
lock (table) {
table.Remove (id);
}
}
public T Value {
get { lock (table) { return table [id]; } }
set { lock (table) { table [id] = value; } }
}
}
class BetterActivity : Activity {
HiddenReference<List<string>> strings = new HiddenReference<List<string>>();
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
strings.Value = new List<string> (
Enumerable.Range (0, 10000)
.Select(v => new string ('x', v % 1000)));
}
}
次要集合
呼叫 GC 可以手動執行 次要集合。收集(0). 次要集合很便宜(相較於主要集合),但確實有顯著的固定成本,因此您不想太常觸髮它們,而且應該有幾毫秒的暫停時間。
如果您的應用程式有一個「工作週期」,其中相同動作會一遍又一遍地完成,則建議在工作周期結束後手動執行次要集合。 範例值班週期包括:
- 單一遊戲畫面的轉譯週期。
- 與指定應用程式對話框的整個互動(開啟、填滿、關閉)
- 重新整理/同步處理應用程式數據的一組網路要求。
主要集合
呼叫 GC 可以手動執行 主要集合。Collect() 或 GC.Collect(GC.MaxGeneration)
。
它們應該很少執行,而且收集 512MB 堆積時,在 Android 樣式裝置上可能會暫停一秒的時間。
只有在有這樣的情況下,才應該手動叫用主要集合:
在冗長的值班周期結束時,當長時間暫停不會對使用者提出問題時。
在覆寫的 Android.App.Activity.OnLowMemory() 方法內。
診斷
若要追蹤何時建立和終結全域參考,您可以將debug.mono.log系統屬性設定為包含 gref 和/或 gc。
組態
您可以藉由設定環境變數來設定 MONO_GC_PARAMS
Xamarin.Android 垃圾收集行程。 環境變數可以使用AndroidEnvironment的 建置動作來設定。
MONO_GC_PARAMS
環境變數是下列參數的逗號分隔清單:
nursery-size
= size :設定託兒所的大小。 大小是以位元組為單位指定,而且必須是兩個乘冪。 後綴k
和m
g
可用來分別指定千位元組、MB 和 GB。 託兒所是第一代(兩代人)。 較大的托兒所通常會加快程式的速度,但顯然會使用更多的記憶體。 默認託兒所大小 512 kb。soft-heap-limit
= size :應用程式的目標最大受控記憶體耗用量。 當記憶體使用低於指定的值時,GC 會針對運行時間優化(集合較少)。 高於此限制,GC 已針對記憶體使用量優化(更多集合)。evacuation-threshold
= threshold :設定疏散閾值百分比。 值必須是範圍 0 到 100 中的整數。 預設值為 66。 如果集合的掃掠階段發現特定堆積區塊類型的佔用量小於這個百分比,它會針對下一個主要集合中的該區塊類型執行複製集合,進而將佔用量還原到接近 100%。 值為 0 會關閉疏散。bridge-implementation
= 網橋實作 :這會設定 GC 網橋選項,以協助解決 GC 效能問題。 有三個可能的值: 舊 、 新 、 tarjan。bridge-require-precise-merge
:Tarjan 網橋包含優化,在少數情況下,可能會導致物件在第一次變成垃圾之後收集一個 GC。 包含此選項會停用該優化,讓 GCS 更具可預測性,但可能會變慢。
例如,若要將 GC 設定為具有 128MB 的堆積大小限制,請使用 內容的 [建置] 動作AndroidEnvironment
,將新的檔案新增至您的 Project:
MONO_GC_PARAMS=soft-heap-limit=128m