Share via


가비지 수집

Xamarin.Android는 Mono의 단순 세대 가비지 수집기를 사용합니다. 다음 두 종류의 컬렉션이 있는 2세대와 큰 개체 공간이 있는 마크 앤 스윕 가비지 수집기입니다.

  • 부 컬렉션(Gen0 힙 수집)
  • 주 컬렉션(Gen1 및 큰 개체 공간 힙 수집).

참고 항목

GC를 통한 명시적 컬렉션이 없는 경우 Collect() 컬렉션은 힙 할당에 따라 요청이 필요합니다. 이것은 참조 계산 시스템이 아닙니다. 개체는 미해결 참조가 없거나 범위가 종료되는 즉시 수집되지 않습니다. 새 할당을 위해 부 힙의 메모리가 부족하면 GC가 실행됩니다. 할당이 없으면 실행되지 않습니다.

부 컬렉션은 저렴하고 자주 사용되며 최근에 할당된 개체와 데드 개체를 수집하는 데 사용됩니다. 부 컬렉션은 할당된 개체의 몇 MB마다 수행됩니다. 부 컬렉션은 GC를 호출 하여 수동으로 수행할 수 있습니다. Collect (0)

주 컬렉션은 비용이 많이 들고 빈도가 낮으며 모든 데드 개체를 회수하는 데 사용됩니다. 현재 힙 크기(힙 크기 조정 전)에 대해 메모리가 소진되면 주 컬렉션이 수행됩니다. GC를 호출하여 주 컬렉션을 수동으로 수행할 수 있습니다. 수집() 또는 GC를 호출합니다. 인수 GC를 사용하여 (int) 수집합니다. MaxGeneration.

VM 간 개체 컬렉션

개체 형식에는 세 가지 범주가 있습니다.

  • 관리되는 개체: Java.Lang.Object(예: System.String)에서 상속되지 않는 형식입니다. 이러한 항목은 GC에서 일반적으로 수집됩니다.

  • Java 개체: Android 런타임 VM 내에 있지만 Mono VM에 노출되지 않는 Java 형식입니다. 이들은 지루하고, 더 이상 논의되지 않습니다. 이러한 항목은 일반적으로 Android 런타임 VM에서 수집됩니다.

  • 피어 개체: IJavaObject를 구현하는 형식(예: 모든 Java.Lang.ObjectJava.Lang.Throw 가능 서브클래스). 이러한 형식의 인스턴스에는 관리되는 피어와 네이티브 피어라는 두 개의 "절반"이 있습니다. 관리되는 피어는 C# 클래스의 인스턴스입니다. 네이티브 피어는 Android 런타임 VM 내에서 Java 클래스의 인스턴스이며 C# IJavaObject.Handle 속성에는 네이티브 피어에 대한 JNI 전역 참조가 포함됩니다.

네이티브 피어에는 다음 두 가지 유형이 있습니다.

Xamarin.Android 프로세스 내에 두 개의 VM이 있으므로 다음 두 가지 유형의 가비지 수집이 있습니다.

  • Android 런타임 컬렉션
  • Mono 컬렉션

Android 런타임 컬렉션은 정상적으로 작동하지만 주의해야 합니다. JNI 전역 참조는 GC 루트로 처리됩니다. 따라서 Android 런타임 VM 개체에 유지되는 JNI 전역 참조가 있는 경우 수집에 적합하더라도 개체 를 수집할 수 없습니다 .

모노 컬렉션은 재미가 일어나는 곳입니다. 관리되는 개체는 일반적으로 수집됩니다. 피어 개체는 다음 프로세스를 수행하여 수집됩니다.

  1. Mono 컬렉션에 적합한 모든 Peer 개체에는 JNI 전역 참조가 JNI 약한 전역 참조로 대체됩니다.

  2. Android 런타임 VM GC가 호출됩니다. 모든 네이티브 피어 인스턴스를 수집할 수 있습니다.

  3. (1)에서 만든 JNI 약한 전역 참조는 검사. 약한 참조가 수집된 경우 Peer 개체가 수집됩니다. 약한 참조가 수집되지 않은 경우 약한 참조가 JNI 전역 참조로 대체되고 Peer 개체가 수집되지 않습니다. 참고: API 14 이상에서는 반환된 IJavaObject.Handle 값이 GC 이후에 변경되었을 수 있음을 의미합니다.

이 모든 것의 최종 결과는 피어 개체의 인스턴스가 관리 코드(예: 변수에 저장됨)에서 참조되거나 Java 코드에서 참조되는 한 static 라이브가 됩니다. 또한 네이티브 피어와 관리되는 피어를 모두 수집할 수 있을 때까지 네이티브 피어는 수집할 수 없으므로 네이티브 피어의 수명은 라이브 상태 이상으로 확장됩니다.

개체 주기

피어 개체는 Android 런타임과 Mono VM 둘 다에 논리적으로 존재합니다. 예를 들어 Android.App.Activity 관리형 피어 인스턴스에는 해당 android.app.Activity 프레임워크 피어 Java 인스턴스가 있습니다. Java.Lang.Object에서 상속되는 모든 개체 는 두 VM 내에서 표현을 가질 것으로 예상할 수 있습니다.

두 VM에서 표현된 모든 개체는 단일 VM(예 System.Collections.Generic.List<int>: )에만 있는 개체에 비해 확장된 수명을 갖습니다. GC를 호출 합니다. Xamarin.Android GC는 개체를 수집하기 전에 VM에서 참조하지 않도록 해야 하므로 수집이 반드시 이러한 개체를 수집하지는 않습니다.

개체 수명을 단축하려면 Java.Lang.Object.Dispose() 를 호출해야 합니다. 이렇게 하면 전역 참조를 해제하여 두 VM 간의 개체 연결을 수동으로 "끊기"하므로 개체를 더 빠르게 수집할 수 있습니다.

자동 컬렉션

릴리스 4.1.0부터는 gref 임계값을 초과하면 Xamarin.Android가 자동으로 전체 GC를 수행합니다. 이 임계값은 플랫폼에 대해 알려진 최대 grefs의 90%입니다. 에뮬레이터에서 1800 grefs(최대 2000개) 및 하드웨어에서 46800 grefs(최대 52000). 참고: Xamarin.Android는 Android.Runtime.JNIEnv에서 만든 grefs만 계산하며 프로세스에서 생성된 다른 grefs에 대해서는 알 수 없습니다. 이것은 추론 에 불과합니다.

자동 컬렉션이 수행되면 다음과 유사한 메시지가 디버그 로그에 출력됩니다.

I/monodroid-gc(PID): 46800 outstanding GREFs. Performing a full GC!

이러한 발생은 비결정적이며 부적합한 시간에 발생할 수 있습니다(예: 그래픽 렌더링 중). 이 메시지가 표시되면 다른 곳에서 명시적 컬렉션을 수행하거나 피어 개체의 수명을 줄이려고 할 수 있습니다.

GC 브리지 옵션

Xamarin.Android는 Android 및 Android 런타임을 사용하여 투명한 메모리 관리를 제공합니다. GC 브리지라는 Mono 가비지 수집기의 확장으로 구현됩니다.

GC 브리지는 Mono 가비지 수집 중에 작동하며 Android 런타임 힙을 사용하여 "활동성"을 확인해야 하는 피어 개체를 파악합니다. GC 브리지는 다음 단계를 순서대로 수행하여 이 결정을 내린다.

  1. 연결할 수 없는 피어 개체의 모노 참조 그래프를 나타내는 Java 개체로 유도합니다.

  2. Java GC를 수행합니다.

  3. 실제로 죽은 개체를 확인합니다.

이 복잡한 프로세스는 서브클래스가 Java.Lang.Object 모든 개체를 자유롭게 참조할 수 있게 해줍니다. Java 개체가 C#에 바인딩될 수 있는 제한은 제거됩니다. 이러한 복잡성으로 인해 브리지 프로세스는 비용이 많이 들 수 있으며 애플리케이션에서 눈에 띄는 일시 중지가 발생할 수 있습니다. 애플리케이션에서 상당한 일시 중지가 발생하는 경우 다음 세 가지 GC 브리지 구현 중 하나를 조사할 가치가 있습니다.

  • Tarjan - 로버트 타얀의 알고리즘과 뒤로 참조 전파를 기반으로 하는 GC 브리지의 완전히 새로운 디자인입니다. 시뮬레이션된 워크로드에서 최상의 성능을 제공하지만 실험 코드의 점유율도 더 큽니다.

  • 새로운 - 원래 코드의 주요 정밀 검사, 이차 동작의 두 인스턴스를 수정하지만 핵심 알고리즘을 유지 (강하게 연결된 구성 요소를 찾기위한 Kosaraju의 알고리즘따라).

  • 이전 - 원래 구현 (세 가지 중 가장 안정적인 것으로 간주). 일시 중지가 허용되는 경우 애플리케이션에서 GC_BRIDGE 사용해야 하는 브리지입니다.

가장 적합한 GC 브리지를 파악하는 유일한 방법은 애플리케이션에서 실험하고 출력을 분석하는 것입니다. 벤치마킹을 위해 데이터를 수집하는 방법에는 두 가지가 있습니다.

  • 로깅 사용 - 각 GC 브리지 옵션에 대해 로깅을 사용하도록 설정한 다음 각 설정의 로그 출력을 캡처하고 비교합니다. 각 옵션, GC 특히 메시지의 메시지를 검사합니다 GC_BRIDGE . 비대화형 애플리케이션에 대해 최대 150ms의 일시 중지는 견딜 수 있지만, 매우 대화형 애플리케이션(예: 게임)에 대해 60ms 이상 일시 중지하는 것이 문제입니다.

  • 브리지 회계 사용 - 브리지 회계는 브리지 프로세스에 관련된 각 개체가 가리키는 개체의 평균 비용을 표시합니다. 이 정보를 크기별로 정렬하면 가장 많은 양의 추가 개체를 보유하는 항목에 대한 힌트가 제공됩니다.

기본 설정은 Tarjan입니다. 회귀가 발견되면 이 옵션을 이전으로 설정해야 할 수 있습니다. 또한 Tarjan이 성능 향상을 생성하지 않는 경우 보다 안정적인 이전 옵션을 사용하도록 선택할 수 있습니다.

애플리케이션에서 사용해야 하는 GC_BRIDGE 옵션을 지정하려면 환경 변수를 MONO_GC_PARAMS 전달 bridge-implementation=newbridge-implementation=old하거나 bridge-implementation=tarjan 전달합니다. 이 작업은 빌드 작업을 사용하여 프로젝트에 새 파일을 추가하여 수행됩니다AndroidEnvironment. 예시:

MONO_GC_PARAMS=bridge-implementation=tarjan

자세한 내용은 구성을 참고하시기 바랍니다.

GC 지원

GC에서 메모리 사용 및 수집 시간을 줄이는 데 도움이 되는 여러 가지 방법이 있습니다.

피어 인스턴스 삭제

GC는 프로세스의 불완전한 보기를 가지며, GC가 메모리가 낮다는 것을 모르기 때문에 메모리가 부족할 때 실행되지 않을 수 있습니다.

예를 들어 Java.Lang.Object 형식 또는 파생 형식의 인스턴스 크기는 20바이트 이상입니다(예고 없이 변경될 수 있음 등). 관리형 호출 가능 래퍼는 추가 인스턴스 멤버를 추가하지 않으므로 10MB 메모리 Blob을 참조하는 Android.Graphics.Bitmap 인스턴스가 있는 경우 Xamarin.Android의 GC는 20바이트 개체를 표시하며 10MB의 메모리를 유지하는 Android 런타임 할당 개체에 연결되어 있는지 확인할 수 없습니다.

GC를 도와야 하는 경우가 많습니다. 불행하게도, GC. AddMemoryPressure()GC. RemoveMemoryPressure()는 지원되지 않으므로 큰 Java 할당 개체 그래프를 해제한 것을 알고 있는 경우 GC를 수동으로 호출해야 할 수 있습니다. GC에 Java 쪽 메모리를 해제하라는 메시지를 표시하거나 Java.Lang.Object 서브클래스를 명시적으로 삭제하여 관리형 호출 가능 래퍼와 Java 인스턴스 간의 매핑을 끊을 수 있습니다.

참고 항목

서브클래스 인스턴스를 삭제할 Java.Lang.Object 때는 매우 주의해야 합니다.

메모리 손상 가능성을 최소화하려면 호출 Dispose()할 때 다음 지침을 따릅니다.

여러 스레드 간 공유

Java 또는 관리되는 인스턴스가 여러 스레드 간에 공유될 수 있는 경우 반드시 공유해서는 안 됩니다Dispose(). 예를 들어 Typeface.Create()는 캐시된 인스턴스를 반환할 수 있습니다. 여러 스레드가 동일한 인수를 제공하는 경우 동일한 인스턴스를 가져옵니다. 따라서 Dispose()한 스레드에서 인스턴스를 Typeface 처리하면 다른 스레드가 무효화될 수 있으며, 이로 인해 ArgumentException인스턴스가 다른 스레드에서 JNIEnv.CallVoidMethod() 삭제되었기 때문에 다른 스레드의 스레드가 무효화될 수 있습니다.

바인딩된 Java 형식 삭제

인스턴스가 바인딩된 Java 형식인 경우 인스턴스가 관리 코드 에서 다시 사용되지 않고 Java 인스턴스를 스레드 간에 공유할 수 없는 한 인스턴스를 삭제할 수 있습니다(이전 Typeface.Create() 설명 참조). (이 결정을 내리는 것은 어려울 수 있습니다.) 다음에 Java 인스턴스가 관리 코드를 입력하면 새 래퍼가 만들어집니다.

이 기능은 Drawable 및 기타 리소스가 많은 인스턴스와 관련하여 자주 유용합니다.

using (var d = Drawable.CreateFromPath ("path/to/filename"))
    imageView.SetImageDrawable (d);

Drawable.CreateFromPath()가 반환하는 피어는 사용자 피어가 아닌 프레임워크 피어를 참조하므로 위의 내용은 안전합니다. 블록 끝의 using 호출은 Dispose() 관리되는 Drawable 인스턴스와 프레임워크 Drawable 인스턴스 간의 관계를 끊어 Android 런타임이 필요한 즉시 Java 인스턴스를 수집할 수 있도록 합니다. 피어 인스턴스가 사용자 피어를 참조하는 경우 안전하지 않습니다. 여기서는 "외부" 정보를 사용하여 사용자 피어를 Drawable 참조할 수 없으므로 Dispose() 호출이 안전합니다.

다른 형식 삭제

인스턴스가 Java 형식(예: 사용자 지정Activity)의 바인딩이 아닌 형식을 참조하는 경우 해당 인스턴스에서 재정의된 메서드를 호출하지 않는 한 호출 Dispose() 하지 마세요. 이렇게 하지 않으면 s가 발생NotSupportedException합니다.

예를 들어 사용자 지정 클릭 수신기가 있는 경우:

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 멤버에 대한 후속 액세스 시도로 인해 예외가 throw되는 경우가 종종 발생합니다. 특히 첫 번째 삭제 시 관리되는 인스턴스를 기본 Java 인스턴스에 연결하는 개체 Handle 가 무효화되지만 관리 코드는 더 이상 사용할 수 없는 경우에도 이 기본 Java 인스턴스에 액세스하려고 시도합니다(Java 인스턴스와 관리되는 인스턴스 간의 매핑에 대한 자세한 내용은 Managed Callable 래퍼 참조).

이 예외를 방지하는 좋은 방법은 메서드에서 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 유효하지 않은 Handle경우 루프 액세스ArgumentException에서 for . 첫 번째 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개의 인스턴스(1x, 1xstringsBadActivity, 1x string[]strings, 10000x 문자열 인스턴스)가 포함되며, 이 모든 인스턴스는 인스턴스를 스캔할 때마다 BadActivity 검사해야 합니다.

이로 인해 컬렉션 시간에 해로운 영향을 줄 수 있으므로 GC 일시 중지 시간이 늘어날 수 있습니다.

사용자 피어 인스턴스에 의해 루팅되는 개체 그래프의 크기를 줄여 GC를 도울 수 있습니다. 위의 예제에서는 Java.Lang.Object에서 상속되지 않는 별도의 클래스로 이동하여 BadActivity.strings 이 작업을 수행할 수 있습니다.

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를 호출 하여 수동으로 수행할 수 있습니다. Collect(0). 부 컬렉션은 저렴하지만(주요 컬렉션에 비해) 상당한 고정 비용이 있으므로 너무 자주 트리거하지 않고 몇 밀리초의 일시 중지 시간이 있어야 합니다.

애플리케이션에 동일한 작업이 반복해서 수행되는 "의무 주기"가 있는 경우 의무 주기가 종료된 후 부 컬렉션을 수동으로 수행하는 것이 좋습니다. 의무 주기의 예는 다음과 같습니다.

  • 단일 게임 프레임의 렌더링 주기입니다.
  • 지정된 앱 대화 상자와의 전체 상호 작용(열기, 채우기, 닫기)
  • 앱 데이터를 새로 고치거나 동기화하는 네트워크 요청 그룹입니다.

주 컬렉션

GC를 호출 하여 주 컬렉션을 수동으로 수행할 수 있습니다. Collect() 또는 GC.Collect(GC.MaxGeneration).

드물게 수행되어야 하며, 512MB 힙을 수집할 때 Android 스타일 디바이스에서 1초의 일시 중지 시간이 있을 수 있습니다.

주 컬렉션은 다음과 같은 경우에만 수동으로 호출해야 합니다.

  • 긴 작업 주기가 끝나고 일시 중지 시간이 길어지면 사용자에게 문제가 발생하지 않습니다.

  • 재정의된 Android.App.Activity.OnLowMemory() 메서드 내에서

진단

전역 참조가 만들어지고 제거되는 시기를 추적하려면 gref 및/또는 gc를 포함하도록 debug.mono.log 시스템 속성을 설정할 수 있습니다.

구성

환경 변수를 설정하여 Xamarin.Android 가비지 수집기를 MONO_GC_PARAMS 구성할 수 있습니다. 환경 변수는 AndroidEnvironment빌드 작업으로 설정할 수 있습니다.

MONO_GC_PARAMS 환경 변수는 다음 매개 변수의 쉼표로 구분된 목록입니다.

  • nursery-size = 크기 : 보육의 크기를 설정합니다. 크기는 바이트 단위로 지정되며 2의 힘이어야 합니다. 접미사 kmg 킬로, 메가 및 기가바이트 각각을 지정하는 데 사용할 수 있습니다. 보육원은 1세대(2세대)입니다. 더 큰 보육은 일반적으로 프로그램의 속도를 높일 수 있지만 분명히 더 많은 메모리를 사용합니다. 기본 보육 크기는 512kb입니다.

  • soft-heap-limit = 크기 : 앱의 대상 최대 관리 메모리 사용량입니다. 메모리 사용이 지정된 값보다 낮으면 GC는 실행 시간(컬렉션 수 감소)에 최적화됩니다. 이 제한을 초과하면 GC가 메모리 사용(더 많은 컬렉션)에 최적화되어 있습니다.

  • evacuation-threshold = 임계값 : 대피 임계값을 백분율로 설정합니다. 값은 0에서 100까지의 정수여야 합니다. 기본값은 66입니다. 컬렉션의 스윕 단계에서 특정 힙 블록 형식의 점유율이 이 비율보다 작으면 다음 주 컬렉션에서 해당 블록 형식에 대한 복사 컬렉션을 수행하여 점유율이 100%에 가깝게 복원됩니다. 값이 0이면 대피가 해제됩니다.

  • bridge-implementation = 브리지 구현 : GC 성능 문제를 해결하는 데 도움이 되도록 GC 브리지 옵션을 설정합니다. 세 가지 가능한 값 이 있습니다. 이전 , 값, tarjan입니다.

  • bridge-require-precise-merge: Tarjan 브리지에는 드물게 개체가 처음 가비지가 된 후 하나의 GC를 수집할 수 있는 최적화가 포함되어 있습니다. 이 옵션을 포함하면 해당 최적화가 비활성화되어 GC가 더 예측 가능하지만 잠재적으로 느려질 수 있습니다.

예를 들어 힙 크기 제한이 128MB로 GC를 구성하려면 콘텐츠와 함께 빌드 작업을AndroidEnvironment 사용하여 프로젝트에 새 파일을 추가합니다.

MONO_GC_PARAMS=soft-heap-limit=128m