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

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

예는 무엇입니까?



답변

다음은 순수 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가 어떤 식 으로든 자신을 다시 사용하는 응용 프로그램을 자주 재배치하는 경우 체와 같이 메모리가 누출되는 이유 입니다. 이것은 여러 가지 미묘한 이유로 발생할 수 있으며 종종 디버그 및 / 또는 수정하기가 어렵습니다.

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


답변

정적 필드 유지 객체 참조 [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 설정을 참조하십시오 .


답변

할 수있는 간단한 일이 잘못된 (또는 존재하지 않는)와 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.


답변

아래에는 잊혀진 리스너의 표준 사례, 정적 참조, 해시 맵의 가짜 / 수정 가능 키 또는 수명주기를 종료 할 기회없이 스레드가 멈춘 경우 외에 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. 마지막 두 가지 중 가장 중요한 부분은 일반적인 프로파일 링 도구를 통해 찾을 수 없다는 것입니다.

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

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


답변

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

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

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

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

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

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

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


답변

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

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

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

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

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

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


답변

다음은 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 애플리케이션을 종료 할 때 싱글 톤을 언로드하지 않습니다. 분명히 싱글 톤 클래스를로드 한 클래스 로더는 클래스에 대한 참조를 유지하므로 싱글 톤 인스턴스는 수집되지 않습니다. 애플리케이션의 새 인스턴스가 배치되면 일반적으로 새 클래스 로더가 작성되며 단일 클래스로 인해 이전 클래스 로더가 계속 존재합니다.