Java에서 메모리 누수를 만드는 방법은 무엇입니까?


3223

방금 인터뷰를했는데 Java 로 메모리 누수 를 만들라는 요청을 받았습니다 .
말할 것도없이, 나는 하나를 만들기 시작하는 방법에 대한 실마리가 전혀없는 바보 같은 느낌이 들었다.

예는 무엇입니까?


275
나는 자바가 가비지 컬렉터를 사용하고 그들에게 "메모리 누수"의 정의에 대해 좀 더 구체적으로 요청하고 JVM 버그를 제외하고는 자바가 메모리와 완전히 같은 방식으로 메모리를 누수 할 수 없다고 설명한다. / C ++는 가능합니다. 어딘가에 객체에 대한 참조가 있어야합니다 .
Darien

371
나는 대부분의 답변에서 사람들이 그러한 최첨단 사례와 트릭을 찾고 있으며 완전히 요점 (IMO)이 누락 된 것처럼 보입니다. 다시는 사용하지 않을 객체에 대한 쓸모없는 참조를 유지하는 코드를 표시 할 수 있으며 동시에 해당 참조를 삭제하지는 않습니다. 해당 객체에 대한 참조가 여전히 있기 때문에 해당 사례가 "실제"메모리 누수가 아니라고 말할 수 있지만, 프로그램이 해당 참조를 다시 사용하지 않고 절대로 삭제하지 않으면 ""와 완전히 동일하며 " 진정한 메모리 누수 ".
ehabkost

62
솔직히 "Go"에 대해 물었던 비슷한 질문이 -1로 하향 조정되었다고 믿을 수 없습니다. here : stackoverflow.com/questions/4400311/… 기본적으로 제가 이야기했던 메모리 누수는 OP에 +200 공감대를 가졌지 만 "Go"에 동일한 문제가 있는지 묻는 공격을 받고 모욕을당했습니다. 어떻게 든 모든 위키가 잘 작동하는지 확실하지 않습니다.
SyntaxT3rr0r

74
@ SyntaxT3rr0r-darien의 답변은 fanboyism이 아닙니다. 그는 특정 JVM에 메모리 누수를 의미하는 버그가있을 수 있음을 명시 적으로 인정했습니다. 이것은 메모리 누수를 허용하는 언어 사양 자체와 다릅니다.
피터 Recore

31
@ehabkost : 아니요, 동일하지 않습니다. (1) 메모리를 되 찾을 수있는 능력이 있지만, "실제 누출"에서는 C / C ++ 프로그램이 할당 된 범위를 잊어 버려 복구 할 수있는 안전한 방법은 없습니다. (2) "부풀림"과 관련된 개체를 볼 수 있기 때문에 프로파일 링 문제를 매우 쉽게 감지 할 수 있습니다. (3) "실제 누출"은 명백한 오류이며, 종료 때까지 많은 객체를 유지하는 프로그램 은 작동 방식의 의도적 인 부분 일 수 있습니다.
Darien

답변:


2309

다음은 순수 Java에서 실제 메모리 누수 (코드를 실행하여 액세스 할 수 없지만 여전히 메모리에 저장된 객체)를 생성하는 좋은 방법입니다.

  1. 응용 프로그램은 오래 실행되는 스레드를 만들거나 스레드 풀을 사용하여 더 빨리 누출됩니다.
  2. 스레드는 (선택적으로 custom)을 통해 클래스를로드합니다 ClassLoader.
  3. 클래스는 큰 메모리 덩어리 (예 :)를 할당하고 이에 new byte[1000000]대한 강력한 참조를 정적 필드에 저장 한 다음 자신에 대한 참조를에 저장합니다 ThreadLocal. 추가 메모리를 할당하는 것은 선택 사항이지만 (클래스 인스턴스를 사용하는 것으로 충분 함) 누출 작업이 훨씬 빨라집니다.
  4. 애플리케이션은 사용자 정의 클래스 또는 ClassLoader로드 된 클래스에 대한 모든 참조를 지 웁니다 .
  5. 반복.

ThreadLocalOracle JDK에서 구현 된 방식으로 인해 메모리 누수가 발생합니다.

  • 각각 Thread에는 개인 필드 threadLocals가 있으며 실제로 스레드 로컬 값을 저장합니다.
  • 이 맵의 각 ThreadLocal객체에 대한 약한 참조 이므로 해당 ThreadLocal객체가 가비지 수집 된 후에 는 해당 항목이 맵에서 제거됩니다.
  • 그러나 각 은 강력한 참조이므로 값이 (직접 또는 간접적으로) 키인ThreadLocal 객체를 가리킬 때 해당 객체 는 스레드가 존재하는 한 가비지 수집되거나 맵에서 제거되지 않습니다.

이 예에서 강력한 참조 체인은 다음과 같습니다.

Thread객체 → threadLocals맵 → 클래스 예 → 클래스 → 정적 ThreadLocal필드 → ThreadLocal객체.

( ClassLoader실제로 누수를 만드는 데 역할을하는 것은 아니며,이 추가 참조 체인으로 인해 누수가 악화 될뿐입니다 : example class → ClassLoader→로드 된 모든 클래스 특히 많은 JVM 구현에서는 더욱 악화되었습니다. Java 7은 클래스와 ClassLoaders가 permgen에 직접 할당되어 결코 가비지 수집되지 않았기 때문에 )

이 패턴의 변형은 Tomcat과 같은 응용 프로그램 컨테이너 ThreadLocal가 어떤 식 으로든 자신을 다시 사용하는 응용 프로그램을 자주 재배치하는 경우 체와 같이 메모리가 누출되는 이유 입니다. 이것은 여러 가지 미묘한 이유로 발생할 수 있으며 종종 디버그 및 / 또는 수정하기가 어렵습니다.

업데이트 : 많은 사람들이 계속 요구하기 때문에이 동작을 보여주는 예제 코드가 있습니다 .


186
+1 ClassLoader 누수는 JEE 세계에서 가장 일반적으로 고통스러운 메모리 누수 중 하나이며, 종종 데이터를 변환하는 타사 라이브러리 (BeanUtils, XML / JSON 코덱)로 인해 발생합니다. 이것은 lib가 응용 프로그램의 루트 클래스 로더 외부에로드되지만 클래스에 대한 참조를 보유 할 때 발생할 수 있습니다 (예 : 캐싱). 앱을 배포 취소 / 재배포 할 때 JVM은 앱의 클래스 로더 (및 이에 의해로드 된 모든 클래스)를 가비지 수집 할 수 없으므로 반복적으로 배포하면 앱 서버가 중단됩니다. ClassCastException이 zxyAbc이 zxyAbc 캐스트 할 수없는 당신은 단서를 얻을 운이 좋은 경우
earcam

7
tomcat은 모든로드 된 클래스에서 트릭을 사용하고 모든 정적 변수를 제외하고 tomcat에는 많은 데이터 레이스와 잘못된 코딩이 있지만 (시간이 걸리고 수정 사항이 필요함) 모든 마음을 사로 잡는 ConcurrentLinkedQueue는 내부 (작은) 객체의 캐시로 사용됩니다. ConcurrentLinkedQueue.Node조차도 더 많은 메모리를 사용합니다.
bestsss 2016 년

57
+1 : 클래스 로더 누출은 악몽입니다. 나는 그들을 알아 내려고 몇 주를 보냈다. 슬픈 점은 @earcam이 말한 것처럼 대부분 타사 라이브러리로 인해 발생하며 대부분의 프로파일 러는 이러한 누출을 감지 할 수 없습니다. 이 블로그에는 Classloader 누수에 대한 명확하고 명확한 설명이 있습니다. blogs.oracle.com/fkieviet/entry/…
Adrian M

4
@Nicolas : 확실합니까? JRockit은 기본적으로 GC 클래스 객체를 수행하지만 HotSpot은 그렇지 않지만 AFAIK JRockit은 여전히 ​​ThreadLocal에서 참조하는 클래스 또는 클래스 로더를 GC 할 수 없습니다.
Daniel Pryden

6
Tomcat은 이러한 누출을 감지하여 wiki.apache.org/tomcat/MemoryLeakProtection 에 대해 경고합니다 . 최신 버전은 때때로 누출을 해결하기도합니다.
Matthijs Bierman

1212

정적 필드 유지 객체 참조 [esp final field]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

String.intern()긴 문자열을 호출

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(닫히지 않은) 열린 스트림 (파일, 네트워크 등)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

닫히지 않은 연결

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

기본 메소드를 통해 할당 된 메모리와 같이 JVM의 가비지 수집기에서 도달 할 수없는 영역

웹 응용 프로그램에서 일부 개체는 응용 프로그램이 명시 적으로 중지되거나 제거 될 때까지 응용 프로그램 범위에 저장됩니다.

getServletContext().setAttribute("SOME_MAP", map);

noclassgc사용하지 않는 클래스 가비지 콜렉션을 방지하는 IBM JDK 의 옵션 과 같이 올바르지 않거나 부적절한 JVM 옵션

IBM jdk 설정을 참조하십시오 .


178
컨텍스트 및 세션 속성이 "누수"라는 데 동의하지 않습니다. 그것들은 단지 오래 지속되는 변수입니다. 그리고 정적 최종 필드는 다소 상수입니다. 큰 상수를 피해야 할 수도 있지만 메모리 누수라고 부르는 것이 공정하지 않다고 생각합니다.
Ian McLaird

80
(닫히지 않은) 공개 스트림 (file, network 등 ...) 은 마무리 (실제로 다음 GC주기 이후에)가 진행되는 동안 close ()가 예정되어 있습니다 ( close()보통 종료 자에서 호출되지 않음). 스레드는 차단 작업 일 수 있으므로). 닫지 않는 것은 좋지 않은 일이지만 누출을 일으키지 않습니다. 닫히지 않은 java.sql.Connection은 동일합니다.
bestsss

33
대부분의 정상적인 JVM에서는 String 클래스에 intern해시 테이블 내용 에 대한 참조가 약한 것처럼 보입니다 . 따라서, 그것은 되는 쓰레기 누수를 제대로 수집하지. (그러나 IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.

42
정적 필드 홀딩 객체 참조 [esp final field]는 어떻게 메모리 누수입니까?
Kanagavelu Sugumar

5
@cHao True. 내가 겪었던 위험은 Streams가 메모리 누수로 인한 문제가 아닙니다. 문제는 메모리 부족으로 메모리가 부족하다는 것입니다. 많은 핸들이 누출 될 수 있지만 여전히 메모리가 충분합니다. 가비지 콜렉터는 여전히 충분한 메모리가 있으므로 전체 콜렉션을 수행하지 않아도됩니다. 이것은 종료자가 호출되지 않았으므로 핸들이 부족하다는 것을 의미합니다. 문제는 누출이 발생하는 스트림에서 메모리가 부족하기 전에 마무리 장치가 (보통) 실행되지만 메모리 이외의 메모리가 부족하기 전에 호출되지 않을 수 있다는 것입니다.
Patrick M

460

할 수있는 간단한 일이 잘못된 (또는 존재하지 않는)와 HashSet에 사용하는 것 hashCode()또는 equals()다음 "중복"을 계속 추가. 복제본을 무시하는 대신 세트는 계속 커져서 제거 할 수 없습니다.

이러한 잘못된 키 / 요소를 걸어 두려면 다음과 같은 정적 필드를 사용할 수 있습니다

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

68
실제로, 요소 클래스가 hashCode를 가져오고 잘못된 경우에도 HashSet에서 요소를 제거 할 수 있습니다. 반복자는 실제로 요소가 아닌 기본 항목 자체에서 작동하므로 세트의 반복자를 가져 와서 remove 메소드를 사용하십시오. (구현되지 않은 hashCode / 같음 만으로는 누출을 유발하기에 충분 하지 않습니다 . 기본값은 간단한 객체 식별을 구현하므로 요소를 가져 와서 정상적으로 제거 할 수 있습니다.)
Donal Fellows

12
@Donal 내가 말하려고하는 것은 메모리 누수에 대한 귀하의 정의에 동의하지 않는 것입니다. 반복자를 제거하는 기술이 누출 상태에서 드립 팬이라고 생각합니다. 드립 팬에 관계없이 누출이 여전히 존재합니다.
corsiKa

94
해시 셋에 대한 참조를 제거하고 GC가 시작될 때까지 기다릴 수 있기 때문에 이것은 메모리 "누수" 가 아니라는 것에 동의합니다 . 메모리가 되돌아갑니다.
user541686

12
@ SyntaxT3rr0r, 귀하의 질문은 자연스럽게 메모리 누수를 유발하는 언어가 있는지 묻는 것으로 해석했습니다. 대답은 '아니오. 이 질문은 메모리 누수와 같은 것을 생성하기 위해 상황을 파악할 수 있는지 묻습니다. 이 예제들 중 어느 것도 C / C ++ 프로그래머가 이해할 수있는 방식으로 메모리 누출이 아닙니다.
피터로 레이

11
@Peter Lawrey : 또한 이것에 대해 어떻게 생각하십니까 : "C 언어에는 할당 한 메모리를 수동으로 해제하는 것을 잊지 않으면 메모리 누수로 자연적으로 누출되는 것은 없습니다 . " 그것이 지적 부정직에 대한 것일까 요? 어쨌든, 나는 피곤하다 : 당신은 마지막 단어를 가질 수있다.
SyntaxT3rr0r

271

아래에는 잊혀진 리스너의 표준 사례, 정적 참조, 해시 맵의 가짜 / 수정 가능 키 또는 수명주기를 종료 할 기회없이 스레드가 멈춘 경우 외에 Java가 유출되는 명백한 사례가 있습니다.

  • File.deleteOnExit() -항상 끈을 새고 문자열이 하위 문자열 인 경우 누출이 더욱 악화됩니다 (기본 char []도 누출 됨)- 자바 7은 또한 복사 하위 문자열 char[](가) 나중에 적용되지 않도록, ; @Daniel, 투표 할 필요는 없습니다.

관리되지 않는 스레드의 위험을 대부분 보여주기 위해 스레드에 집중하고 스윙을 만지고 싶지 않습니다.

  • Runtime.addShutdownHook스레드 그룹 클래스의 버그로 인해 시작되지 않은 스레드가 수집되지 않을 수 있습니다. JGroup은 GossipRouter에서 누출이 있습니다.

  • a를 작성하지만 시작하지는 않으면 Thread위와 동일한 카테고리로 이동합니다.

  • 스레드를 만들면 ContextClassLoaderand 및 AccessControlContextplus ThreadGroup및 any를 상속받습니다. InheritedThreadLocal이러한 모든 참조는 클래스 로더에 의해로드 된 전체 클래스와 모든 정적 참조 및 ja-ja와 함께 잠재적 누출입니다. 이 효과는 매우 단순한 ThreadFactory인터페이스 를 특징으로하는 전체 jucExecutor 프레임 워크에서 볼 수 있지만 대부분의 개발자는 숨어있는 위험에 대한 단서가 없습니다. 또한 많은 라이브러리가 요청에 따라 스레드를 시작합니다 (업계에서 많이 사용되는 라이브러리는 너무 많습니다).

  • ThreadLocal캐시; 그것들은 많은 경우에 악합니다. 모든 사람들이 ThreadLocal을 기반으로 한 간단한 캐시를 많이 보았을 것입니다. 나쁜 소식 : 스레드가 컨텍스트 ClassLoader의 수명을 예상보다 더 많이 계속한다면 순수한 작은 누출입니다. 실제로 필요한 경우가 아니면 ThreadLocal 캐시를 사용하지 마십시오.

  • ThreadGroup.destroy()ThreadGroup에 스레드 자체가없는 경우 호출 하지만 여전히 하위 ThreadGroup을 유지합니다. 스레드 그룹이 상위에서 제거되지 못하게하는 누수로 인해 모든 하위 항목을 열거 할 수 없게됩니다.

  • WeakHashMap 및 값 (in)을 사용하면 키를 직접 참조합니다. 힙 덤프 없이는 찾기가 어렵습니다. Weak/SoftReference보호 대상 객체에 대한 하드 참조를 유지할 수있는 모든 확장에 적용됩니다 .

  • java.net.URLHTTP (S) 프로토콜과 함께 사용 하고 from (!)에서 리소스를로드합니다. 이것은 특별하며, KeepAliveCache현재 스레드의 컨텍스트 클래스 로더를 누출시키는 ThreadGroup 시스템에 새 스레드를 만듭니다. 스레드는 살아있는 스레드가 없을 때 첫 번째 요청에 따라 생성되므로 운이 좋거나 누수가 발생할 수 있습니다. 누출은 이미 Java 7에서 수정되었으며 스레드를 작성하는 코드는 컨텍스트 클래스 로더를 올바르게 제거합니다. 더 적은 경우가 있습니다 (ImageFetcher와 같은, 또한 고정 유사한 스레드를 만드는).

  • 생성자 를 InflaterInputStream전달 new java.util.zip.Inflater()하고 (예 PNGImageDecoder를 들어) end()팽창기를 호출하지 않습니다 . 음, 그냥 생성자와 함께 생성자를 전달하면 new우연히 ... 그리고 close()스트림을 호출 해도 생성자 매개 변수로 수동으로 전달 된 인플레이터가 닫히지 않습니다. 이것은 파이널 라이저가 필요하다고 생각할 때 릴리스되기 때문에 실제 누출이 아닙니다. 그 순간까지 네이티브 메모리를 너무 많이 먹어서 리눅스 oom_killer가 프로세스를 무의식적으로 죽일 수 있습니다. 주요 문제는 Java의 최종화가 매우 신뢰할 수 없으며 G1이 7.0.2까지 악화되었다는 것입니다. 이야기의 도덕 : 가능한 한 빨리 기본 리소스를 해제하십시오. 파이널 라이저는 너무 가난합니다.

  • 와 같은 경우입니다 java.util.zip.Deflater. Deflater는 Java에서 메모리가 부족한 상태이므로 훨씬 더 나쁩니다. 즉, 항상 수백 KB의 기본 메모리를 할당하는 15 비트 (최대) 및 8 개의 메모리 레벨 (9는 최대)을 사용합니다. 다행히도 Deflater널리 사용되지 않으며 JDK에는 오용이 없습니다. 항상 전화를 end()수동으로 작성하는 경우 DeflaterInflater. 마지막 두 가지 중 가장 중요한 부분은 일반적인 프로파일 링 도구를 통해 찾을 수 없다는 것입니다.

(요청에 따라 더 많은 시간 낭비자를 추가 할 수 있습니다.)

행운을 빌어 안전 유지; 누출은 악하다!


23
Creating but not starting a Thread...Yikes, 나는 몇 세기 전에 이것에 심하게 물렸다! (Java 1.3)
leonbloy

@leonbloy, 스레드가 스레드 그룹에 바로 추가되어 더 악화되기 전에 시작하지 않은 것은 매우 심각한 누출을 의미했습니다. 그것은 단지 수를 증가시킬 unstarted뿐 아니라 스레드 그룹이 파괴되는 것을 막아줍니다 (더 적은 악하지만 여전히 누출)
bestsss

감사합니다! " ThreadGroup.destroy()ThreadGroup에 스레드 자체가 없을 때 호출 하는 것은 ..." 는 매우 미묘한 버그입니다. 내 컨트롤 GUI에서 스레드를 열거해도 아무것도 보이지 않았지만 스레드 그룹과 아마도 적어도 하나의 자식 그룹은 사라지지 않았기 때문에 나는 이것을 몇 시간 동안 쫓아 갔다.
Lawrence Dol

1
@bestsss : 궁금합니다. JVM 종료에서 실행되는 경우 종료 후크를 왜 제거하고 싶습니까?
Lawrence Dol

203

여기에있는 대부분의 예는 "너무 복잡합니다". 그들은 엣지 케이스입니다. 이 예제에서 프로그래머는 실수 (등호 / 해시 코드를 재정의하지 않는 것과 같이)를 실수로 만들었거나 JVM / JAVA (정적으로 클래스로드 ...)의 코너 사례에 물린 적이 있습니다. 나는 그것이 면접관이 원하거나 가장 일반적인 경우가 아니라고 생각합니다.

그러나 메모리 누수에 대한 간단한 사례가 있습니다. 가비지 수집기는 더 이상 참조되지 않은 항목 만 해제합니다. 우리는 Java 개발자로서 메모리에 관심이 없습니다. 필요할 때 할당하고 자동으로 해제합니다. 좋아.

그러나 오래 지속되는 응용 프로그램은 공유 상태를 갖는 경향이 있습니다. 정적, 단일 등 무엇이든 될 수 있습니다. 종종 사소한 응용 프로그램은 복잡한 객체 그래프를 만드는 경향이 있습니다. 참조를 null로 설정하는 것을 잊어 버리거나 컬렉션에서 하나의 객체를 제거하는 것을 잊어 버리면 메모리 누수가 발생하기에 충분합니다.

물론 모든 종류의 리스너 (예 : UI 리스너), 캐시 또는 오래 지속되는 공유 상태는 올바르게 처리하지 않으면 메모리 누수가 발생하는 경향이 있습니다. 이해할 수있는 것은 Java 코너 사례가 아니거나 가비지 수집기의 문제가 아니라는 것입니다. 디자인 문제입니다. 수명이 긴 객체에 리스너를 추가하도록 설계되었지만 더 이상 필요하지 않은 경우 리스너를 제거하지 않습니다. 우리는 객체를 캐시하지만 캐시에서 객체를 제거하는 전략은 없습니다.

계산에 필요한 이전 상태를 저장하는 복잡한 그래프가있을 수 있습니다. 그러나 이전 상태 자체는 이전 상태와 연결되어 있습니다.

우리는 SQL 연결이나 파일을 닫아야합니다. null에 대한 적절한 참조를 설정하고 컬렉션에서 요소를 제거해야합니다. 적절한 캐싱 전략 (최대 메모리 크기, 요소 수 또는 타이머)이 있어야합니다. 리스너에게 알릴 수있는 모든 객체는 addListener 및 removeListener 메소드를 모두 제공해야합니다. 이러한 알리미가 더 이상 사용되지 않으면 리스너 목록을 지워야합니다.

메모리 누수는 실제로 가능하며 완벽하게 예측할 수 있습니다. 특별한 언어 기능이나 코너 케이스가 필요 없습니다. 메모리 누수는 무언가 빠졌거나 설계 문제가 있음을 나타내는 지표입니다.


24
다른 답변에서 사람들이 그러한 최첨단 사례와 트릭을 찾고 있으며 그 요점을 완전히 놓친 것 같습니다. 다시는 사용하지 않을 객체에 대한 쓸모없는 참조를 유지하고 해당 참조를 제거하지 않는 코드 만 표시 할 수 있습니다. 해당 객체에 대한 참조가 여전히 있기 때문에 해당 사례가 "실제"메모리 누수가 아니라고 말할 수 있지만, 프로그램이 해당 참조를 다시 사용하지 않고 절대로 삭제하지 않으면 ""와 완전히 동일하며 " 진정한 메모리 누수 ".
ehabkost

@Nicolas Bousquet : "실제로 메모리 누수가 가능합니다"정말 감사합니다. 공감 비 +15 좋은. 내가 이동 언어에 대한 질문의 전제로서, 그 사실을 알리는 여기에 소리있어 : stackoverflow.com/questions/4400311 이 질문은 여전히 :( 부정적인 downvotes을 가지고
SyntaxT3rr0r

Java 및 .NET의 GC는 어떤 의미에서 다른 객체에 대한 참조를 보유하는 객체의 그래프가 다른 객체를 "관심"하는 객체의 그래프와 동일하다고 가정합니다. 실제로 "돌보는"관계를 나타내지 않는 참조 그래프에 간선이 존재할 수 있으며, 직접 또는 간접 참조 경로가없는 경우에도 객체가 다른 객체의 존재를 신경 쓸 수 WeakReference있습니다. 하나에서 다른. 객체 참조에 여분의 비트가있는 경우 "대상에 대한 관심"표시기를 갖는 것이 도움이 될 수 있습니다.
supercat

... PhantomReference물건에 관심이있는 사람이없는 것으로 밝혀지면 시스템에 (와 비슷한 방법으로 ) 알림을 제공하도록 합니다. WeakReference다소 가까워 지지만 사용하기 전에 강력한 참조로 변환해야합니다. 강한 참조가 존재하는 동안 GC주기가 발생하면 목표가 유용한 것으로 간주됩니다.
supercat

이것은 제 생각에는 정답입니다. 몇 년 전에 시뮬레이션을 작성했습니다. 어떻게 든 실수로 이전 상태를 현재 상태에 연결하여 메모리 누수가 발생합니다. 마감 기한으로 인해 메모리 누수를 해결하지는 못했지만이를 문서화하여«기능»으로 만들었습니다.
nalply

163

답은 전적으로 면접관이 생각한 것에 달려 있습니다.

실제로 Java 유출이 가능합니까? 물론 다른 대답에는 많은 예가 있습니다.

그러나 여러 가지 메타 질문이있을 수 있습니까?

  • 이론적으로 "완벽한"Java 구현이 누출에 취약합니까?
  • 응시자는 이론과 현실의 차이를 이해합니까?
  • 후보자는 가비지 수집 작동 방식을 이해합니까?
  • 아니면 이상적인 경우 가비지 수집이 어떻게 작동합니까?
  • 기본 인터페이스를 통해 다른 언어를 호출 할 수 있다는 것을 알고 있습니까?
  • 그들은 다른 언어로 메모리를 유출하는 것을 알고 있습니까?
  • 응시자는 메모리 관리가 무엇인지, Java에서 어떤 일이 벌어지고 있는지 알고 있습니까?

귀하의 메타 질문을 "이 인터뷰 상황에서 사용할 수있는 답변은 무엇입니까?" 따라서 Java 대신 인터뷰 기술에 중점을 둘 것입니다. 인터뷰에서 질문에 대한 답을 알지 못하는 상황을 Java 누출 방법을 알아야 할 필요가있는 곳보다 반복 할 가능성이 더 큽니다. 희망적으로 이것은 도움이 될 것입니다.

면접을 위해 개발할 수있는 가장 중요한 기술 중 하나는 질문을 적극적으로 듣고 면담 자와 함께 그들의 의도를 추출하는 법을 배우는 것입니다. 이를 통해 원하는 방식으로 질문에 답변 할 수있을뿐만 아니라 의사 소통에 필수적인 의사 소통 능력이 있음을 알 수 있습니다. 그리고 동등한 재능을 가진 많은 개발자들 사이에서 선택을 할 때마다, 나는 매번 응답하기 전에 듣고, 생각하고, 이해하는 사람을 고용 할 것입니다.


22
내가 그 질문을 할 때마다, 나는 아주 간단한 대답을 찾고있다-큐를 계속 늘리고, 마지막으로 가까운 db 등을, 이상한 클래스 로더 / 스레드 세부 정보가 아니라 gc가 할 수 있고 당신을 위해 할 수없는 것을 이해한다는 것을 암시한다. 당신이 인터뷰하고있는 직업에 달려 있다고 생각합니다.
DaveC


130

다음은 JDBC를 이해하지 못하는 경우 무의미한 예 입니다. 또는 JDBC는 가까운 개발자 기대 방법 이상 Connection, Statement그리고 ResultSet그것들을 폐기하거나 참조를 잃고, 대신의 구현에 의존하기 전에 인스턴스를 finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

위의 문제는 Connection객체가 닫히지 않았으므로 가비지 수집기가 돌아 와서 도달 할 수 없을 때까지 물리적 연결이 열린 상태로 유지된다는 것입니다. GC는 finalize메소드 를 호출 하지만 finalize적어도 구현 된 것과 같은 방식으로 구현하지 않은 JDBC 드라이버 Connection.close가 있습니다. 결과적으로 도달 할 수없는 오브젝트가 수집되어 메모리가 회수되는 동안 오브젝트와 연관된 자원 (메모리 포함) Connection이 단순히 회수되지 않을 수 있습니다.

이러한 경우 Connectionfinalize방법은 없습니다 정리 모든 것을 수행, 하나는 실제로 데이터베이스 서버가 결국 연결이 살아 아니라는 것을 파악 될 때까지 데이터베이스 서버에 대한 물리적 연결 (여러 가비지 컬렉션 사이클을 지속됩니다 사실을 발견 그것 경우 ) 닫혀 있어야합니다.

JDBC 드라이버가 구현하더라도 finalize최종화 중에 예외가 발생할 수 있습니다. 결과적으로 현재 "휴면"오브젝트와 연관된 메모리 finalize는 한 번만 호출되므로 보증 되지 않습니다 .

위의 객체 종료 중 예외가 발생하는 시나리오는 메모리 누수 (객체 부활)로 이어질 수있는 다른 시나리오와 관련이 있습니다. 개체 부활은 종종 다른 개체에서 마무리되지 않은 개체에 대한 강력한 참조를 만들어 의도적으로 수행됩니다. 객체 부활이 잘못 사용되면 다른 메모리 누수 소스와 함께 메모리 누수가 발생합니다.

당신이 쓸 수있는 더 많은 예가 있습니다-

  • List목록에 추가 만하고 삭제하지 않는 인스턴스 관리 (더 이상 필요없는 요소는 제거해야 함) 또는
  • Sockets 또는 Files를 열지 만 더 이상 필요하지 않을 때 닫지 않습니다 (위의 Connection클래스 관련 예제와 유사 ).
  • Java EE 애플리케이션을 종료 할 때 싱글 톤을 언로드하지 않습니다. 분명히 싱글 톤 클래스를로드 한 클래스 로더는 클래스에 대한 참조를 유지하므로 싱글 톤 인스턴스는 수집되지 않습니다. 애플리케이션의 새 인스턴스가 배치되면 일반적으로 새 클래스 로더가 작성되며 단일 클래스로 인해 이전 클래스 로더가 계속 존재합니다.

98
일반적으로 메모리 한계에 도달하기 전에 최대 열린 연결 한계에 도달합니다. 내가 왜 아는지 묻지 말아라 ...
Hardwareguy

Oracle JDBC 드라이버는이 작업으로 유명합니다.
초키

@Hardwareguy Connection.close모든 SQL 호출의 finally 블록에 넣을 때까지 SQL 데이터베이스의 연결 제한에 많이 도달했습니다 . 여분의 재미를 위해 데이터베이스에 대한 너무 많은 호출을 방지하기 위해 Java 측에서 잠금이 필요한 장기 실행 Oracle 저장 프로 시저를 호출했습니다.
Michael Shopsin

@Hardwareguy 흥미롭지 만 실제로 모든 환경에서 실제 연결 제한에 도달 할 필요는 없습니다. 예를 들어, weblogic 앱 서버 11g에 배포 된 애플리케이션의 경우 대규모 연결 누수가 확인되었습니다. 그러나 누수 된 연결 수취 옵션으로 인해 메모리 누수가 발생하는 동안 데이터베이스 연결을 계속 사용할 수있었습니다. 모든 환경에 대해 잘 모르겠습니다.
Aseem Bansal

내 경험상 연결을 닫아도 메모리 누수가 발생합니다. 먼저 ResultSet 및 PreparedStatement를 닫아야합니다. OutOfMemoryErrors로 인해 몇 시간 또는 며칠 동안 정상적으로 작동 한 후에 서버가 충돌하기 시작했습니다.
Bjørn Stenfeldt

119

잠재적 인 메모리 누수의 가장 간단한 예 중 하나이며이를 피하는 방법은 ArrayList.remove (int)의 구현입니다.

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

직접 구현했다면 더 이상 사용되지 않는 배열 요소를 지우고 elementData[--size] = null싶습니까 ( )? 그 참조는 거대한 물체를 살릴 수 있습니다 ...


5
그리고 여기서 메모리 누수가 어디에 있습니까?
rds

28
@maniek :이 코드가 메모리 누수를 나타내는 것은 아닙니다. 우발적 인 객체 보존을 피하기 위해 때로는 명백하지 않은 코드가 필요하다는 것을 보여주기 위해 인용했습니다.
meriton

RangeCheck (index) 란 무엇입니까? ?
Koray Tugay

6
Joshua Bloch는 효과적인 Java로이 예제를 제공하여 간단한 스택 구현을 보여줍니다. 아주 좋은 답변입니다.
임대

그러나 잊어 버린 경우에도 실제 메모리 누출은 아닙니다. 이 요소는 여전히 Reflection으로 SAFELLY에 액세스 할 수 있으며 List 인터페이스를 통해 명확하고 직접 액세스 할 수는 없지만 오브젝트와 참조는 여전히 존재하며 안전하게 액세스 할 수 있습니다.
DGoiko

68

더 이상 필요하지 않은 객체에 대한 참조를 유지할 때마다 메모리 누수가 발생합니다. Java에서 메모리 누수 가 어떻게 나타나는지와 이에 대해 수행 할 수있는 작업에 대한 예제는 Java 프로그램에서 메모리 누수 처리를 참조하십시오 .


14
나는 이것이 "누수"라고 믿지 않는다. 그것은 버그이며 프로그램과 언어의 설계에 의한 것입니다. 누출은 참조가 없는 주위에 매달려있는 물체 입니다.
user541686

29
@Mehrdad : 모든 언어에 완전히 적용되는 것은 아닙니다. 나는 주장 거라고 어떤 메모리 누수가 프로그램의 빈약 한 디자인에 의한 버그입니다.
Bill the Lizard

9
@ Mehrdad : ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. 나는 당신이 그 결론을 그리는 방법을 모르겠습니다 . 있다어떤 정의로든 Java에서 메모리 누수를 만드는 방법 더 적습니다 . 여전히 유효한 질문입니다.
Bill the Lizard

7
@ 31eee384 : 프로그램이 절대 사용할 수없는 메모리에 객체를 보관하면 기술적으로 메모리가 누출됩니다. 더 큰 문제가 있다는 사실이 실제로는 바뀌지 않습니다.
도마뱀 빌

8
@ 31eee384 : 그렇지 않다는 사실을 알고 있다면 불가능합니다. 작성된 프로그램은 절대로 데이터에 액세스하지 않습니다.
도마뱀 빌

51

sun.misc.Unsafe 클래스 를 사용하면 메모리 누수가 발생할 수 있습니다. 실제로이 서비스 클래스는 다른 표준 클래스 (예 : java.nio 클래스)에서 사용됩니다. 이 클래스의 인스턴스를 직접 만들 수는 없지만 리플렉션을 사용하여이를 수행 할 수 있습니다 .

Eclipse IDE에서 코드가 컴파일되지 않음-명령을 사용하여 컴파일 javac (컴파일 중에 경고가 표시됨)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

1
가비지 수집기에는 할당 된 메모리가 보이지 않습니다.
stemm

4
할당 된 메모리는 Java에도 속하지 않습니다.
bestsss

이 sun / oracle jvm은 구체적입니까? 예를 들어 IBM에서 작동합니까?
베를린 브라운

2
적어도 i) 다른 사람이 사용할 수 없다는 의미에서 ii) Java 응용 프로그램이 종료되면 메모리가 시스템으로 반환된다는 의미에서 메모리는 확실히 "Java와 함께"수행됩니다. JVM 외부에 있습니다.
Michael Anderson

3
이클립스 (최소한 최신 버전에서)로 빌드되지만 컴파일러 설정을 변경해야합니다 : 창> 환경 설정> Java> 컴파일러> 오류 / 경고> 더 이상 사용되지 않고 제한된 API 세트 금지 참조 (액세스 규칙)를 "경고" ".
Michael Anderson

43

여기에서 답변을 복사 할 수 있습니다 : Java에서 메모리 누수를 일으키는 가장 쉬운 방법은 무엇입니까?

"컴퓨터 과학에서 메모리 누수 (또는이 맥락에서 누수)는 컴퓨터 프로그램이 메모리를 소비하지만 운영 체제로 다시 해제 할 수 없을 때 발생합니다." (위키 백과)

쉬운 대답은 : 당신은 할 수 없습니다. Java는 자동 메모리 관리를 수행하며 필요하지 않은 자원을 비 웁니다. 이것을 막을 수는 없습니다. 항상 리소스를 해제 할 수 있습니다. 수동 메모리 관리가있는 프로그램에서는 다릅니다. malloc ()을 사용하여 C에서 약간의 메모리를 얻을 수 있습니다. 메모리를 비우려면 malloc이 리턴 한 포인터가 필요하며 free ()를 호출하십시오. 그러나 더 이상 포인터를 쓰지 않거나 (수명을 초과하거나 수명을 초과 한 경우) 불행히도이 메모리를 해제 할 수 없으므로 메모리 누수가 발생합니다.

지금까지 다른 모든 대답은 실제로 메모리 누수가 아니라는 정의에 있습니다. 그들은 모두 무의미한 물건으로 메모리를 빨리 채우는 것을 목표로합니다. 그러나 언제든지 생성 한 객체를 역 참조하여 메모리를 확보 할 수 있습니다-> NO LEAK. acconrad의 답변 은 그의 솔루션이 가비지 수집기를 끝없는 루프로 강제로 "충돌"시키는 것이므로 효과적으로 인정해야하지만 매우 가깝습니다.

긴 대답은 다음과 같습니다. JNI를 사용하여 Java 용 라이브러리를 작성하면 메모리 누수가 발생할 수 있습니다. JNI는 수동 메모리 관리가 가능하므로 메모리 누수가 발생할 수 있습니다. 이 라이브러리를 호출하면 Java 프로세스에서 메모리가 누출됩니다. 또는 JVM에 버그가있어 JVM이 메모리를 잃을 수 있습니다. JVM에 버그가있을 수 있습니다. 가비지 수집이 그다지 쉽지 않기 때문에 알려진 버그가있을 수도 있지만 여전히 버그입니다. 설계 상으로는 불가능합니다. 그러한 버그에 의해 영향을받는 자바 코드를 요구할 수도 있습니다. 미안하지만 하나도 모르고 다음 Java 버전에서는 더 이상 버그가 아닐 수 있습니다.


12
이는 메모리 누수에 대한 매우 제한적이고 유용하지 않은 정의입니다. 실용적인 목적으로 이해되는 유일한 정의는 "메모리 누수는 프로그램이 보유한 데이터가 더 이상 필요하지 않은 후에 할당 된 메모리를 계속 보유하는 조건"입니다.
메이슨 휠러

1
언급 된 acconrad의 답변이 삭제 되었습니까?
Tomáš Zato-복원 Monica Monica

1
@ TomášZato : 아뇨. 위의 참조를 지금 연결하도록 설정 했으므로 쉽게 찾을 수 있습니다.
양키

개체 부활이란 무엇입니까? 소멸자는 몇 번이나 호출됩니까? 이 질문들이 어떻게이 대답을 반증합니까?
자폐증

1
글쎄, GC에도 불구하고 Java 내부에서 메모리 누수를 만들 수 있으며 위의 정의를 여전히 충족시킬 수 있습니다. 다른 코드가 제거하지 않도록 외부 액세스로부터 보호되는 추가 전용 데이터 구조 만 있으면됩니다. 프로그램에는 코드가 없기 때문에 메모리를 해제 할 수 없습니다.
toolforger

39

다음은 http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 를 통한 단순하고 불쾌한 것 입니다.

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

부분 문자열은 원래의 훨씬 긴 문자열의 내부 표현을 나타내므로 원본은 메모리에 유지됩니다. 따라서 StringLeaker를 사용하는 한 단일 문자 문자열을 잡고 있다고 생각하더라도 전체 원본 문자열도 메모리에 있습니다.

원래 문자열에 대한 원치 않는 참조를 저장하지 않는 방법은 다음과 같습니다.

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

추가적인 악영향을 .intern()위해 하위 문자열 을 사용할 수도 있습니다 .

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

그렇게하면 StringLeaker 인스턴스가 삭제 된 후에도 원래의 긴 문자열과 파생 된 하위 문자열이 모두 메모리에 유지됩니다.


4
나는 그것을 메모리 누수라고 부르지 않을 것이다. 자체 . 해제되면 muchSmallerString( StringLeaker객체가 파괴 되기 때문에 ) 긴 문자열도 해제됩니다. 내가 메모리 누수라고하는 것은이 JVM 인스턴스에서 절대 해제 될 수없는 메모리입니다. 그러나 메모리를 해제하는 방법을 스스로 보여주었습니다 this.muchSmallerString=new String(this.muchSmallerString). 실제 메모리 누수로 수행 할 수있는 작업이 없습니다.
rds

2
@ rds, 그것은 공정한 포인트입니다. 비intern그렇지 경우는 "메모리 누수"보다 "메모리 놀라움"일 수 있습니다. .intern()그러나 하위 문자열을 사용하면 더 긴 문자열에 대한 참조가 유지되고 해제 될 수없는 상황이 발생합니다.
Jon Chambers

15
substring () 메소드는 java7에서 새로운 문자열을 생성합니다 (새로운 행동입니다)
anstarovoyt

하위 문자열 ()을 직접 할 필요조차 없습니다. 정규식 매처를 사용하여 큰 입력의 작은 부분을 일치시키고 "추출 된"문자열을 오랫동안 가지고 다니십시오. 거대한 입력은 Java 6까지 살아 있습니다.
Bananeweizen

37

GUI 코드에서 이것의 일반적인 예는 위젯 / 컴포넌트를 생성하고 정적 / 애플리케이션 범위 객체에 리스너를 추가 한 후 위젯이 파괴 될 때 리스너를 제거하지 않는 경우입니다. 메모리 누수뿐만 아니라 듣고있는 것이 이벤트를 발생시킬 때 성능이 저하 될뿐 아니라 모든 이전 리스너도 호출됩니다.


1
안드로이드 플랫폼은 뷰의 정적 필드에서 비트 맵캐싱하여 생성 된 메모리 누수의 예를 제공합니다 .
rds

36

서블릿 컨테이너 (Tomcat, Jetty, Glassfish 등)에서 실행중인 웹 응용 프로그램을 가져옵니다. 앱을 10 회 또는 20 회 연속으로 재배포하십시오 (서버의 자동 배포 디렉토리에서 WAR을 간단히 터치하기에 충분할 수 있습니다).

아무도 실제로 이것을 테스트하지 않으면 응용 프로그램 자체를 정리하지 않았기 때문에 몇 번의 재배포 후에 OutOfMemoryError가 발생할 가능성이 높습니다. 이 테스트를 통해 서버에서 버그를 발견 할 수도 있습니다.

문제는 컨테이너의 수명이 응용 프로그램의 수명보다 길다는 것입니다. 컨테이너가 응용 프로그램의 객체 또는 클래스에 대한 모든 참조를 가비지 수집 할 수 있는지 확인해야합니다.

웹앱의 배포 제거를 유지하는 참조가 하나만있는 경우 해당 클래스 로더와 결과적으로 웹앱의 모든 클래스를 가비지 수집 할 수 없습니다.

응용 프로그램에서 시작한 스레드, ThreadLocal 변수, 로깅 어 펜더는 클래스 로더 누수를 일으키는 일반적인 용의자 중 일부입니다.


1
이것은 메모리 누수 때문이 아니라 클래스 로더가 이전 클래스 세트를 언로드하지 않기 때문입니다. 따라서 서버를 다시 시작하지 않고 응용 프로그램 서버를 다시 배포하지 않는 것이 좋습니다 (물리적 시스템이 아니라 응용 프로그램 서버). WebSphere와 동일한 문제를 보았습니다.
Sven

35

아마도 JNI를 통해 외부 네이티브 코드를 사용하고 있습니까?

순수한 Java를 사용하면 거의 불가능합니다.

그러나 이는 더 이상 메모리에 액세스 할 수없는 "표준"유형의 메모리 누수에 관한 것이지만 여전히 응용 프로그램에서 소유합니다. 대신 사용하지 않는 객체에 대한 참조를 유지하거나 나중에 닫지 않고 스트림을 열 수 있습니다.


22
"메모리 누수"의 정의에 따라 다릅니다. "지속되었지만 더 이상 필요하지 않은 메모리"인 경우 Java로 쉽게 수행 할 수 있습니다. "메모리가 할당되었지만 코드로 액세스 할 수없는 메모리"이면 약간 어려워집니다.
Joachim Sauer 2016 년

@Joachim Sauer-두 번째 유형을 의미했습니다. 첫 번째는 만들기가 상당히 쉽다 :)
Rogach

6
"순수한 자바로는 거의 불가능하다." 글쎄, 내 경험은 특히 함정을 알지 못하는 사람들이 캐시를 구현할 때 특히 중요합니다.
Fabian Barney 2016 년

4
@Rogach : 사례 모두에서 것을 +10 000 렙 표시와 함께 사람들이 다양한 답변에 400 upvotes 기본적으로 요아킴 사우어는 주석 매우 가능성은. 따라서 "거의 불가능"은 말이되지 않습니다.
SyntaxT3rr0r

32

PermGen 및 XML 구문 분석과 관련하여 멋진 "메모리 누수"가 발생했습니다. 태그 이름에 String.intern ()을 사용하여 XML 구문 분석기를 사용하여 XML 구문 분석기를 사용했습니다 (어떤 것이 있는지 기억이 나지 않습니다). 고객 중 한 명이 XML 속성이나 텍스트가 아닌 태그 이름으로 데이터 값을 저장하는 것이 좋았으므로 다음과 같은 문서가있었습니다.

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

실제로, 그들은 숫자가 아닌 더 긴 텍스트 ID (약 20 자)를 사용했습니다.이 ID는 고유하고 하루에 10-15 백만의 비율로 들어 왔습니다. 그것은 하루에 200MB의 쓰레기를 만듭니다. 다시는 필요하지 않으며 GCed도 아닙니다 (PermGen에 있기 때문에). permgen을 512MB로 설정 했으므로 메모리 부족 예외 (OOME)가 도착하는 데 약 2 일이 걸렸습니다.


4
예제 코드를 nitpick하기 위해 : 숫자 (또는 숫자로 시작하는 문자열)는 XML의 요소 이름으로 사용할 수 없다고 생각합니다.
Paŭlo Ebermann

문자열 인터 닝이 힙에서 발생하는 JDK 7 이상에서는 더 이상 적용되지 않습니다. 자세한 내용은 다음 기사를 참조하십시오. java-performance.info/string-intern-in-java-6-7-8
jmiserez

그래서 String 대신 StringBuffer를 사용하면이 문제가 해결 될 것이라고 생각합니까? 그렇지 않습니까?
anubhs

24

메모리 누수 란 무엇입니까?

  • 그것은 의한 것 버그 또는 나쁜 디자인.
  • 메모리 낭비입니다.
  • 시간이 지남에 따라 악화됩니다.
  • 가비지 수집기가 청소할 수 없습니다.

전형적인 예 :

객체 캐시는 일을 망칠 수있는 좋은 출발점입니다.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

캐시가 커지고 커집니다. 그리고 곧 전체 데이터베이스가 메모리로 빨려 들어갑니다. 더 나은 디자인은 LRUMap을 사용합니다 (최근에 사용한 객체 만 캐시에 유지).

물론 일을 훨씬 더 복잡하게 만들 수 있습니다.

  • ThreadLocal 사용 구조를.
  • 복잡한 참조 트리 추가 .
  • 또는 타사 라이브러리로 인한 누수 .

자주 발생하는 일 :

이 Info 객체에 다른 객체에 대한 참조가 있고 다른 객체에 대한 참조가있는 경우 어떤 방식으로도 (디자인이 잘못되어) 일종의 메모리 누수로 간주 할 수 있습니다.


22

아무도 내부 클래스 예제를 사용하지 않았다는 것이 흥미로웠다. 내부 수업이있는 경우 그것은 본질적으로 포함하는 클래스에 대한 참조를 유지합니다. 물론 Java WILL이 결국 정리하기 때문에 기술적으로 메모리 누수가 아닙니다. 그러나 이로 인해 클래스가 예상보다 오래 걸려있을 수 있습니다.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

이제 Example1을 호출하고 Example2를 폐기하는 Example2를 가져와도 본질적으로 여전히 Example1 오브젝트에 대한 링크가 있습니다.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

또한 특정 시간보다 오래 존재하는 변수가 있다면 소문도 들었습니다. Java는 항상 존재한다고 가정하고 코드에서 더 이상 도달 할 수 없으면 실제로 정리하려고 시도하지 않습니다. 그러나 그것은 완전히 검증되지 않았습니다.


2
내부 수업은 거의 문제가되지 않습니다. 그것들은 간단한 경우이며 감지하기 매우 쉽습니다. 소문도 소문입니다.
bestsss

2
"루머"는 세대 별 GC 작동 방식에 대해 절반 정도 읽은 것처럼 들립니다. JVM은 젊은 세대에서 승진하여 매 패스마다 검사를 중지 할 수 있기 때문에 오래되었지만 아직 도달 할 수없는 객체는 실제로 붙어서 잠시 동안 공간을 차지할 수 있습니다. 그들은 의도적으로 "5000 임시 문자열 정리"패스를 피할 것입니다. 그러나 그들은 불멸의 것이 아닙니다. 그들은 여전히 ​​수집 자격이 있으며 VM이 RAM에 묶여 있으면 결국 전체 GC 스윕을 실행하고 해당 메모리를 다시 소거합니다.
cHao

22

최근에 log4j에 의한 메모리 누수 상황이 발생했습니다.

Log4j에는 NDC (Nested Diagnostic Context) 라고하는이 메커니즘이 있으며, 이는 서로 다른 소스의 인터리브 로그 출력을 구별하는 도구입니다. NDC가 작동하는 단위는 스레드이므로 다른 스레드의 로그 출력을 개별적으로 구분합니다.

스레드 별 태그를 저장하기 위해 log4j의 NDC 클래스는 스레드 객체 자체가 키를 갖는 Hashtable을 사용합니다 (스레드 ID가 아니라). 따라서 NDC 태그가 스레드에 매달린 모든 객체를 메모리에 유지할 때까지 객체는 또한 메모리에 남아 있습니다. 웹 애플리케이션에서는 NDC를 사용하여 로그 ID를 요청 ID로 태그 지정하여 단일 요청과 로그를 별도로 구분합니다. NDC 태그를 스레드와 연결하는 컨테이너는 요청에서 응답을 반환하는 동안 NDC 태그를 제거합니다. 요청을 처리하는 동안 다음 코드와 같은 하위 스레드가 생성 될 때 문제가 발생했습니다.

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

따라서 NDC 컨텍스트는 생성 된 인라인 스레드와 연관되었습니다. 이 NDC 컨텍스트의 핵심이었던 스레드 개체는 hugeList 개체가 매달려있는 인라인 스레드입니다. 따라서 스레드가 수행 한 작업을 완료 한 후에도 hugeList에 대한 참조는 NDC 컨텍스트 Hastable에 의해 유지되어 메모리 누수가 발생합니다.


짜증나 파일에 로깅하는 동안 ZERO 메모리를 할당하는이 로깅 라이브러리를 확인해야합니다. mentalog.soliveirajr.com
TraderJoeChicago

+1 slf4j / logback (동일한 작성자의 후속 제품)에서 MDC와 유사한 문제가 있는지 여부를 알 수 있습니까? 소스에 대해 자세히 알아 보려고하지만 먼저 확인하고 싶었습니다. 어느 쪽이든, 이것을 게시 해 주셔서 감사합니다.
sparc_spread

20

면접관은 아마도 아래 코드와 같은 순환 참조를 찾고 있었을 것입니다 (실제로 참조 계산을 사용하는 아주 오래된 JVM에서는 메모리가 누출되어 더 이상은 아닙니다). 그러나 매우 모호한 질문이므로 JVM 메모리 관리에 대한 이해를 과시 할 수있는 최고의 기회입니다.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

그런 다음 참조 카운트를 사용하면 위 코드에서 메모리가 누출 될 수 있다고 설명 할 수 있습니다. 그러나 대부분의 최신 JVM은 더 이상 참조 계산을 사용하지 않으며 대부분이 메모리를 수집하는 스윕 가비지 수집기를 사용합니다.

다음으로 다음과 같이 기본 고유 자원이있는 오브젝트 작성에 대해 설명 할 수 있습니다.

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

그런 다음 이것이 기술적으로 메모리 누수라고 설명 할 수 있지만 실제로 누수는 기본 원시 자원을 할당하는 JVM의 원시 코드로 인해 발생하며 Java 코드에서 해제되지 않았습니다.

하루가 끝나면 최신 JVM을 사용하여 일반적인 JVM 인식 범위를 벗어난 기본 리소스를 할당하는 Java 코드를 작성해야합니다.


19

누구나 항상 기본 코드 경로를 잊어 버립니다. 누출에 대한 간단한 공식은 다음과 같습니다.

  1. 기본 메소드를 선언하십시오.
  2. 기본 메소드에서을 호출하십시오 malloc. 전화하지 마십시오 free.
  3. 기본 메소드를 호출하십시오.

기본 코드의 메모리 할당은 JVM 힙에서옵니다.


1
실화를 바탕으로합니다.
Reg

18

정적지도를 만들고 하드 참조를 계속 추가하십시오. 그것들은 결코 GC되지 않을 것입니다.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
누수가 어떻게 되나요? 그것은 당신이 원하는 것을 정확하게하고 있습니다. 그것이 누수라면 어디에서나 객체를 생성하고 저장하는 것이 누수입니다.
Falmarri

3
@Falmarri에 동의합니다. 누수가 보이지 않고 객체를 만드는 중입니다. 방금 할당 한 메모리를 'removeFromCache'라는 다른 방법으로 '재 확보'할 수 있습니다. 누수는 메모리를 회수 할 수 없을 때입니다.
Kyle

3
내 요점은 객체를 계속 만들고 캐시에 넣는 사람이 조심하지 않으면 OOM 오류가 발생할 수 있다는 것입니다.
duffymo

8
@ duffymo : 그러나 그것은 실제로 질문이 요구하는 것이 아닙니다. 단순히 모든 메모리를 사용하는 것과는 아무런 관련이 없습니다.
Falmarri

3
절대적으로 유효하지 않습니다. Map 컬렉션에서 많은 객체를 수집하고 있습니다. 지도가 보유하고 있기 때문에 참조가 유지됩니다.
gyorgyabraham

16

해당 클래스의 finalize 메소드에서 클래스의 새 인스턴스를 작성하여 이동 메모리 누수를 작성할 수 있습니다. 종료자가 여러 인스턴스를 만드는 경우 보너스 포인트. 다음은 힙 크기에 따라 몇 초에서 몇 분 사이에 전체 힙을 유출하는 간단한 프로그램입니다.

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

나는 아무도 이것을 아직 말하지 않았다고 생각합니다 : finalize () 메서드를 재정 의하여 finalize ()이 참조를 어딘가에 저장하여 객체를 부활시킬 수 있습니다. 가비지 콜렉터는 오브젝트에서 한 번만 호출되므로 오브젝트는 절대 파괴되지 않습니다.


10
사실이 아닙니다. finalize()호출되지 않지만 참조가 더 이상 없으면 객체가 수집됩니다. 가비지 콜렉터도 '호출'되지 않습니다.
bestsss

1
이 답변은 오해의 소지가 있습니다.이 finalize()메소드는 JVM에서 한 번만 호출 할 수 있지만 오브젝트를 부활시킨 다음 다시 참조 해제하면 다시 쓰레기를 수집 할 수는 없습니다. finalize()메소드 에 자원 종료 코드가있는 경우이 코드가 다시 실행되지 않아 메모리 누수가 발생할 수 있습니다.
Tom Cammann

15

최근에 더 미묘한 종류의 리소스 누수가 발생했습니다. 클래스 로더의 getResourceAsStream을 통해 리소스를 열면 입력 스트림 핸들이 닫히지 않았습니다.

음, 당신은 바보라고 말할 수 있습니다.

글쎄, 이것이 흥미로운 점은이 방법으로 JVM의 힙이 아닌 기본 프로세스의 힙 메모리를 누출시킬 수 있다는 것입니다.

Java 코드에서 참조 할 파일이있는 jar 파일 만 있으면됩니다. jar 파일이 클수록 메모리가 더 빨리 할당됩니다.

다음 클래스를 사용하여 이러한 항아리를 쉽게 만들 수 있습니다.

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

BigJarCreator.java라는 파일에 붙여넣고 명령 행에서 컴파일하고 실행하십시오.

javac BigJarCreator.java
java -cp . BigJarCreator

정보 : 현재 작업중인 디렉토리에 두 개의 파일이있는 jar 아카이브가 있습니다.

두 번째 클래스를 만들어 봅시다 :

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

이 클래스는 기본적으로 아무것도하지 않지만 참조되지 않은 InputStream 객체를 만듭니다. 이러한 객체는 즉시 가비지 수집되므로 힙 크기에 영향을 미치지 않습니다. 이 예제에서는 jar 파일에서 기존 리소스를로드하는 것이 중요하며 여기서 크기는 중요합니다!

의심스러운 경우 위의 클래스를 컴파일하고 시작하지만 적절한 힙 크기 (2MB)를 선택하십시오.

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

참조가 유지되지 않기 때문에 여기에서 OOM 오류가 발생하지 않으며 위 예제에서 ITERATIONS를 선택한 크기에 관계없이 응용 프로그램이 계속 실행됩니다. 응용 프로그램이 대기 명령에 도달하지 않으면 프로세스의 메모리 소비 (맨 위 (RES / RSS) 또는 프로세스 탐색기)가 증가합니다. 위의 설정에서 약 150MB의 메모리를 할당합니다.

응용 프로그램이 안전하게 재생되도록하려면 입력 스트림이 생성 된 곳에서 바로 닫으십시오.

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

프로세스는 반복 횟수와 상관없이 35MB를 초과하지 않습니다.

아주 간단하고 놀라운.


14

많은 사람들이 제안했듯이, Resource Leaks는 JDBC 예제와 같이 상당히 쉽게 발생합니다. 실제 메모리 누수는 좀 더 어려워집니다. 특히 JVM의 깨진 비트에 의존하지 않는 경우 ...

설치 공간이 매우 커서 객체에 액세스 할 수없는 객체를 만드는 아이디어는 실제 메모리 누수가 아닙니다. 아무것도 접근 할 수 없다면 가비지 수집되며, 접근 할 수 있다면 누출이 아닙니다 ...

한 가지 방법은 을 사용 하고 여전히 않는 경우 나도 몰라 - - 일 생각으로는 세 깊은 원형 체인을하는 것입니다. 객체 A에서 객체 B에 대한 참조가있는 것처럼 객체 B에는 객체 C에 대한 참조가 있으며 객체 C는 객체 A에 대한 참조가 있습니다. -A와 B가 다른 것에 의해 접근 할 수 없지만 3 방향 체인을 처리 할 수없는 경우 안전하게 수집 할 수 있습니다.


7
한동안은 그렇지 않았습니다. 현대 GC는 순환 참조를 처리하는 방법을 알고 있습니다.
assylias 2016 년

13

잠재적으로 큰 메모리 누수를 생성하는 또 다른 방법은에 대한 참조를 보유 Map.Entry<K,V>하는 것 TreeMap입니다.

왜 이것이 TreeMaps 에만 적용되는지를 판단하기는 어렵지만 구현을 살펴보면 그 이유는 다음과 같습니다. TreeMap.Entry상점은 형제를 참조하므로 a TreeMap를 수집 할 준비가되었지만 다른 클래스는 그것 Map.Entry전체 맵은 메모리에 유지됩니다.


실제 시나리오 :

TreeMap데이터 구조 를 반환하는 DB 쿼리가 있다고 상상해보십시오 . 사람들은 일반적으로 TreeMap요소 삽입 순서가 유지되므로 s를 사용 합니다.

public static Map<String, Integer> pseudoQueryDatabase();

쿼리를 여러 번 호출하고 각 쿼리에 대해 (즉, 각 Map반환 에 대해 ) Entry어딘가에 저장 하면 메모리가 계속 증가합니다.

다음 랩퍼 클래스를 고려하십시오.

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

신청:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

pseudoQueryDatabase()호출 후에 는 map인스턴스를 수집 할 준비가되어야하지만 적어도 하나 Entry는 다른 곳에 저장되므로 발생하지 않습니다 .

jvm설정 에 따라으로 인해 초기 단계에서 응용 프로그램이 중단 될 수 있습니다 OutOfMemoryError.

visualvm그래프에서 메모리가 어떻게 계속 커지는 지 알 수 있습니다 .

메모리 덤프-TreeMap

해시 된 데이터 구조 ( HashMap)에서도 마찬가지 입니다.

를 사용할 때의 그래프 HashMap입니다.

메모리 덤프-해시 맵

해결책? 를 저장하는 대신 키 / 값을 직접 저장하십시오 (아마도 이미 수행했듯이) Map.Entry.


나는 더 광범위한 기준을 작성했습니다 여기에 .


11

스레드는 종료 될 때까지 수집되지 않습니다. 그들은 역할을 뿌리 가비지 콜렉션. 그것들은 단순히 잊어 버렸거나 참조를 지우는 것으로 간단하게 되찾지 않는 몇 안되는 물건 중 하나입니다.

작업자 스레드를 종료하는 기본 패턴은 스레드에서 볼 수있는 일부 조건 변수를 설정하는 것입니다. 스레드는 변수를 주기적으로 확인하고이를 종료 신호로 사용할 수 있습니다. 변수가 선언 volatile되지 않으면 스레드가 변수 변경 사항을 볼 수 없으므로 종료를 알 수 없습니다. 또는 일부 스레드가 공유 객체를 업데이트하려고하지만 잠금을 시도하는 동안 교착 상태가 발생한다고 상상해보십시오.

소수의 스레드 만있는 경우 프로그램이 제대로 작동하지 않기 때문에 이러한 버그가 분명합니다. 필요에 따라 더 많은 스레드를 작성하는 스레드 풀이있는 경우 사용되지 않는 / 중단 된 스레드가 발견되지 않고 무한정 누적되어 메모리 누수가 발생합니다. 스레드는 응용 프로그램에서 다른 데이터를 사용할 가능성이 있으므로 직접 참조하는 데이터가 수집되는 것을 방지합니다.

장난감 예제로 :

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

System.gc()당신이 좋아하는 모든 것을 부르십시오 . 그러나 전달 된 객체 leakMe는 결코 죽지 않을 것입니다.

(* 편집 *)


1
@Spidey 아무것도 "고착"되지 않습니다. 호출 메서드는 즉시 반환되며 전달 된 개체는 다시 사용되지 않습니다. 정확히 누출입니다.
Boann September

1
프로그램 수명 동안 스레드가 "실행 중"이거나 잠자기 상태입니다. 그것은 나에게 누출로 간주되지 않습니다. 풀을 완전히 사용하지 않더라도 풀뿐만 아니라 누수로 계산되지 않습니다.
Spidey

1
@Spidey "귀하의 프로그램 수명 동안 [일]을 가질 것입니다. 그것은 누수로 간주되지 않습니다." 너 자신이 들리니?
Boann

3
@Spidey 프로세스가 누출되지 않은 것으로 알고있는 메모리를 계산하는 경우 프로세스가 항상 가상 주소 공간의 어떤 페이지가 매핑되는지 추적하기 때문에 여기에있는 모든 대답이 잘못되었습니다. 프로세스가 종료되면 OS는 사용 가능한 페이지 스택에 페이지를 다시 배치하여 모든 누수를 정리합니다. 다음 극단으로 가져 가려면 RAM 칩이나 디스크의 스왑 공간에 물리적 비트가 물리적으로 잘못 배치되거나 파괴되지 않았 음을 지적하여 논쟁의 여지가 생길 수 있습니다. 따라서 컴퓨터를 끌 수 있습니다 그리고 다시 누출을 청소하십시오.
Boann

1
누출에 대한 실질적인 정의는 우리가 알지 못하여이를 되 찾는 데 필요한 절차를 수행 할 수없는 메모리가 손실되었다는 것입니다. 전체 메모리 공간을 분해하고 재 구축해야합니다. 이와 같은 불량 스레드는 교착 상태 또는 복잡한 스레드 풀 구현을 통해 자연스럽게 발생할 수 있습니다. 이러한 스레드가 참조하는 객체는 간접적으로도 수집되지 않으므로 이제는 프로그램 수명 동안 자연스럽게 회수하거나 재사용 할 수없는 메모리가 있습니다. 나는 이것을 문제라고 부를 것이다. 특히 메모리 누수입니다.
Boann

10

올바른 예제는 스레드가 풀링되는 환경에서 ThreadLocal 변수를 사용할 수 있다고 생각합니다.

예를 들어, 서블릿에서 ThreadLocal 변수를 사용하여 다른 웹 구성 요소와 통신하고 컨테이너에 의해 스레드가 생성되고 유휴 상태의 개체는 풀에 유지됩니다. 올바르게 정리되지 않은 경우 ThreadLocal 변수는 동일한 웹 구성 요소가 해당 값을 겹쳐 쓸 때까지 계속 존재합니다.

물론 일단 식별되면 문제를 쉽게 해결할 수 있습니다.


10

면접관은 순환 참조 솔루션을 찾고 있었을 것입니다.

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

이것은 참조 카운트 가비지 수집기의 고전적인 문제입니다. 그런 다음 JVM이이 제한이없는 훨씬 더 정교한 알고리즘을 사용한다고 정중하게 설명합니다.

웨스 탈레


12
이것은 참조 카운트 가비지 수집기의 고전적인 문제입니다. 15 년 전만해도 Java는 참조 횟수 계산을 사용하지 않았습니다. 참조 카운팅도 GC보다 느립니다.
bestsss

4
메모리 누수가 아닙니다. 무한 루프.
Esben Skov Pedersen

2
@Esben 반복 할 때마다 이전 first은 유용하지 않으며 가비지 수집되어야합니다. 에서는 레퍼런스 카운팅 (자체) 그것에 활성 기준이 있기 때문에 가비지 컬렉터 개체 freeed되지 않는다. 무한 루프는 누출을 시연하기 위해 여기에 있습니다. 프로그램을 실행할 때 메모리가 무한정 증가합니다.
rds

@rds @ Wesley Tarle은 루프가 무한 하지 않다고 가정합니다 . 여전히 메모리 누수가 있습니까?
nz_21
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.