이벤트 핸들러 메모리 누수를 피하는 이유와 방법은 무엇입니까?


154

방금 +=C # (또는 다른 .net 언어)을 사용하여 이벤트 핸들러를 추가 하면 일반적인 메모리 누수가 발생할 수 있음 을 StackOverflow에 대한 몇 가지 질문과 답변을 읽음으로써 알게되었습니다 ...

과거에 이와 같은 이벤트 핸들러를 여러 번 사용해 왔으며 애플리케이션에서 메모리 누수를 일으킬 수 있다는 것을 결코 알지 못했습니다.

이것은 어떻게 작동합니까 (즉, 왜 실제로 메모리 누수가 발생합니까?)?
이 문제를 어떻게 해결할 수 있습니까? -=같은 이벤트 핸들러를 사용 하고 있습니까?
이와 같은 상황을 처리하기위한 일반적인 디자인 패턴이나 모범 사례가 있습니까?
예 : UI에서 여러 이벤트를 발생시키기 위해 많은 다른 이벤트 핸들러를 사용하여 다른 스레드가 많은 응용 프로그램을 어떻게 처리해야합니까?

이미 구축 된 큰 응용 프로그램에서이를 효율적으로 모니터링 할 수있는 좋고 간단한 방법이 있습니까?

답변:


188

원인은 설명이 간단합니다. 이벤트 핸들러가 등록되는 동안 이벤트 게시자 는 이벤트 핸들러 대리자를 통해 구독자에 대한 참조를 보유합니다 (대리자가 인스턴스 메소드 인 경우).

게시자가 구독자보다 수명이 길면 구독자에 대한 다른 참조가없는 경우에도 구독자가 활성 상태를 유지합니다.

동등한 핸들러로 이벤트를 구독 취소하면 예, 처리기와 누수를 제거합니다. 그러나 내 경험상 이것은 실제로 거의 문제가되지 않습니다. 일반적으로 게시자와 가입자의 수명이 거의 같습니다.

그것은 이다 가능한 원인 ...하지만 내 경험에 오히려 과대입니다. 마일리지는 다를 수 있습니다. 물론 조심해야합니다.


... 어떤 사람들은 ".net에서 가장 일반적인 메모리 누수는 무엇입니까?"
gillyb

32
게시자 측 에서이 문제를 해결하는 방법은 더 이상 발생하지 않을 것이라고 확신하는 경우 이벤트를 null로 설정하는 것입니다. 이렇게하면 모든 가입자를 암시 적으로 제거 할 수 있으며 특정 이벤트가 개체 수명의 특정 단계에서만 시작될 때 유용 할 수 있습니다.
JSB ձոգչ

2
Dipose 메서드는 이벤트를 null로 설정하기에 좋은 순간입니다.
Davi Fiamenghi

6
@DaviFiamenghi : 글쎄, 만약 무언가가 폐기된다면, 그것은 가비지 수집을 곧받을 수있을 것이라는 증거 일 것입니다.
Jon Skeet

1
@ BrainSlugs83 : "그리고 일반적인 이벤트 패턴은 어쨌든 발신자를 포함합니다"-예, 그러나 그것은 이벤트 프로듀서 입니다. 일반적으로 이벤트 구독자 인스턴스는 관련이 있으며 보낸 사람은 관련이 없습니다. 정적 인 방법을 사용하여 가입 할 수 있다면 이것은 문제가되지 않지만 내 경험에서 거의 옵션이 아닙니다.
Jon Skeet

13

예, -=충분하지만 할당 된 모든 이벤트를 추적하는 것은 매우 어려울 수 있습니다. (자세한 내용은 Jon의 게시물을 참조하십시오). 디자인 패턴에 대해서는 약한 이벤트 패턴을 살펴보십시오 .


1
msdn.microsoft.com/ko-kr/library/aa970850(v=vs.100).aspx 4.0 버전에는 아직 버전이 있습니다.
Femaref 2016 년

게시자가 구독자보다 오래 산다는 것을 안다면 구독자를 만들고 IDisposable이벤트를 구독 취소합니다.
Shimmy Weitzhandler

9

https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 의 블로그에서 이러한 혼란을 설명했습니다 . 나는 당신이 명확한 생각을 가질 수 있도록 여기에 요약하려고 노력할 것입니다.

참조는 "필요함"을 의미합니다.

우선, 객체 A가 객체 B에 대한 참조를 보유하고 있다면 객체 A가 작동하려면 객체 B가 필요하다는 것을 이해해야합니다. 따라서 가비지 수집기는 개체 A가 메모리에 존재하는 한 개체 B를 수집하지 않습니다.

이 부분은 개발자에게 분명해야한다고 생각합니다.

+ = 왼쪽 개체에 오른쪽 개체의 참조를 주입하는 의미 :

그러나 혼란은 C # + = 연산자에서 비롯됩니다. 이 연산자는 개발자에게이 연산자의 오른쪽이 실제로 왼쪽 개체에 대한 참조를 주입하고 있다고 명확하게 알리지 않습니다.

여기에 이미지 설명을 입력하십시오

그리고 그렇게함으로써, 객체 A는 객체 B가 필요하지만, 비록 객체 B가 존재하는지 아닌지는 상관하지 않아도 객체 B가 필요하다고 생각합니다. 객체 A는 객체 B가 필요하다고 생각하기 때문에 객체 A는 객체 A가 존재하는 한 가비지 수집기로부터 객체 B를 보호합니다. 그러나 이벤트 가입자 객체에 대한 보호 기능을 원하지 않으면 메모리 누수가 발생했다고 말할 수 있습니다.

여기에 이미지 설명을 입력하십시오

이벤트 핸들러를 분리하여 이러한 누수를 피할 수 있습니다.

결정하는 방법?

그러나 전체 코드베이스에는 많은 이벤트와 이벤트 핸들러가 있습니다. 그것은 어디서나 이벤트 처리기를 분리해야한다는 것을 의미합니까? 정답은 '아니요'입니다. 그렇게해야한다면 코드베이스가 장황하게 보일 것입니다.

오히려 간단한 플로우 차트를 따라 분리 이벤트 핸들러가 필요한지 여부를 판별 할 수 있습니다.

여기에 이미지 설명을 입력하십시오

대부분의 경우 이벤트 구독자 개체가 이벤트 게시자 개체만큼 중요하며 둘 다 동시에 존재해야합니다.

걱정할 필요가없는 시나리오의 예

예를 들어, 창의 단추 클릭 이벤트.

여기에 이미지 설명을 입력하십시오

여기서 이벤트 게시자는 Button이고 이벤트 구독자는 MainWindow입니다. 해당 순서도를 적용하여 질문하십시오. 기본 창 (이벤트 구독자)이 단추 (이벤트 게시자)보다 먼저 종료되어야합니까? 분명히 아닙니다. 맞습니까? 심지어 말이되지 않습니다. 그렇다면 왜 클릭 이벤트 핸들러를 분리해야합니까?

이벤트 핸들러 분리가 필수 인 예입니다.

구독자 개체가 게시자 개체보다 먼저 종료되어야하는 한 가지 예를 제공하겠습니다. MainWindow가 "SomethingHappened"라는 이벤트를 게시하고 버튼 클릭으로 기본 창에서 하위 창을 표시합니다. 자식 창은 기본 창의 해당 이벤트를 구독합니다.

여기에 이미지 설명을 입력하십시오

그리고 자식 창은 기본 창의 이벤트를 구독합니다.

여기에 이미지 설명을 입력하십시오

이 코드를 통해 Main Window에 버튼이 있음을 분명히 이해할 수 있습니다. 해당 버튼을 클릭하면 자식 창이 나타납니다. 자식 창은 기본 창에서 이벤트를 수신합니다. 무언가를 한 후에, 사용자는 자식 창을 닫습니다.

이제 플로우 차트에 따르면 "이벤트 게시자 (메인 윈도우) 전에 자식 윈도우 (이벤트 구독자)가 죽어야합니까?" 나는 보통 언로드 된 윈도우 이벤트에서 그렇게한다.

경험 법칙: 뷰 (예 : WPF, WinForm, UWP, Xamarin Form 등)가 ViewModel의 이벤트를 구독하는 경우 항상 이벤트 핸들러를 분리해야합니다. ViewModel은 일반적으로 뷰보다 수명이 길기 때문입니다. 따라서 ViewModel이 파괴되지 않으면 해당 ViewModel의 이벤트를 구독 한 모든 뷰는 메모리에 남아 있으므로 좋지 않습니다.

메모리 프로파일 러를 사용한 개념 증명.

메모리 프로파일 러로 개념을 검증 할 수 없다면 재미가 없을 것입니다. 이 실험에서 JetBrain dotMemory 프로파일 러를 사용했습니다.

먼저 MainWindow를 실행했는데 다음과 같이 나타납니다.

여기에 이미지 설명을 입력하십시오

그런 다음 메모리 스냅 샷을 찍었습니다. 그런 다음 버튼을 세 번 클릭했습니다 . 세 개의 자식 창이 나타났습니다. 모든 하위 창을 닫고 dotMemory 프로파일 러에서 Force GC 버튼을 클릭하여 가비지 콜렉터가 호출되는지 확인했습니다. 그런 다음 다른 메모리 스냅 샷을 만들어 비교했습니다. 보다! 우리의 두려움은 사실이었다. 가비지 수집기가 닫은 후에도 자식 창을 수집하지 않았습니다. 뿐만 아니라 ChildWindow 객체의 누수 된 객체 수에도 " 3 "이 표시됩니다 (버튼을 세 번 클릭하여 3 개의 자식 창을 표시합니다).

여기에 이미지 설명을 입력하십시오

그런 다음 아래와 같이 이벤트 핸들러를 분리했습니다.

여기에 이미지 설명을 입력하십시오

그런 다음 동일한 단계를 수행하고 메모리 프로파일 러를 확인했습니다. 이번에는와! 더 이상 메모리 누수가 없습니다.

여기에 이미지 설명을 입력하십시오


3

이벤트는 실제로 연결된 이벤트 핸들러 목록입니다.

이벤트에서 + = new EventHandler를 수행하면이 특정 함수가 이전에 리스너로 추가되었는지 여부는 중요하지 않으며 + = 당 한 번 추가됩니다.

이벤트가 발생하면 링크 된 목록, 항목 별 항목을 통해이 목록에 추가 된 모든 메소드 (이벤트 핸들러)를 호출하므로 페이지가 더 이상 실행되지 않는 한 이벤트 핸들러가 여전히 호출되는 이유입니다 살아 있고 (뿌리) 그들은 연결되어있는 한 살아있을 것입니다. 따라서 이벤트 핸들러가-= new EventHandler로 연결 해제 될 때까지 호출됩니다.

여기를 보아라

그리고 여기 MSDN


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.