etc./StackOverFlow

Java에서 메모리 누수를 어떻게 만들 수 있습니까?

청렴결백한 만능 재주꾼 2021. 10. 1. 03:21
반응형

질문자 :Community Wiki


방금 인터뷰에서 Java 로 메모리 누수 를 생성하라는 요청을 받았습니다.

말할 필요도 없이, 나는 그것을 만드는 방법에 대한 단서조차 없이 꽤 멍청하다고 느꼈습니다.

어떤 예가 될까요?



다음은 순수한 Java에서 진정한 메모리 누수(코드를 실행하여 액세스할 수 없지만 여전히 메모리에 저장되어 있는 개체)를 만드는 좋은 방법입니다.

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

ThreadLocal 이 구현되는 방식으로 인해 메모리 누수가 발생합니다.

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

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

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

( ClassLoader 는 실제로 누수를 만드는 역할을 하지 않습니다. 단지 이 추가 참조 체인으로 인해 누수를 더 악화시킬 뿐입니다: example class → ClassLoader → 그것이 로드한 모든 클래스. 많은 JVM 구현에서 특히 더 나빴습니다. Java 7 이전에는 클래스와 ClassLoader 가 permgen에 직접 할당되었고 가비지 수집이 전혀 되지 않았기 때문입니다.)

이 패턴의 변형은 어떤 식으로든 자신을 다시 가리키는 ThreadLocal 을 사용하는 응용 프로그램을 자주 재배포하는 경우 응용 프로그램 컨테이너(예: Tomcat)가 체처럼 메모리를 누출할 수 있는 이유입니다. 이것은 여러 가지 미묘한 이유로 발생할 수 있으며 디버그 및/또는 수정하기 어려운 경우가 많습니다.

업데이트 : 많은 사람들이 계속해서 요청하기 때문에 다음 은 이 동작을 실제로 보여주는 몇 가지 예제 코드입니다 .


Community Wiki

개체 참조를 보유하는 정적 필드[특히 최종 필드]

 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);

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

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


Community Wiki

할 간단한 일은 잘못된(또는 존재하지 않는) hashCode() 또는 equals() 와 함께 HashSet을 사용한 다음 "중복"을 계속 추가하는 것입니다. 중복을 무시하는 대신 세트가 커지기만 하고 제거할 수 없습니다.

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

 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.

Community Wiki

아래에는 잊혀진 리스너, 정적 참조, 해시맵의 가짜/수정 가능한 키 또는 수명 주기를 끝낼 기회가 없는 스레드가 멈춘 표준 사례 외에 Java가 누출되는 명확하지 않은 경우가 있습니다.

  • File.deleteOnExit() - 항상 문자열을 누출합니다. 문자열이 하위 문자열이면 누수가 더욱 악화됩니다(기본 char[]도 누수됨) - Java 7에서 하위 문자열은 char[] 도 복사하므로 나중에 적용되지 않습니다 . @Daniel, 하지만 투표는 필요하지 않습니다.

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

  • Runtime.addShutdownHook 및 제거하지 않음 ... 시작되지 않은 스레드와 관련된 ThreadGroup 클래스의 버그로 인해 removeShutdownHook을 사용하더라도 수집되지 않을 수 있으므로 ThreadGroup을 효과적으로 누출합니다. JGroup은 GossipRouter에 누수가 있습니다.

  • Thread 생성하지만 시작하지 않는 것은 위와 같은 범주에 속합니다.

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

  • ThreadLocal 캐시; 그것들은 많은 경우에 사악합니다. 나는 모두가 ThreadLocal을 기반으로 하는 간단한 캐시를 꽤 많이 보았을 것이라고 확신합니다. 나쁜 소식입니다. 스레드가 컨텍스트 ClassLoader의 수명을 예상보다 계속해서 계속 진행한다면, 그것은 순전히 좋은 작은 누출입니다. 꼭 필요한 경우가 아니면 ThreadLocal 캐시를 사용하지 마십시오.

  • ThreadGroup에 스레드 자체가 없지만 여전히 자식 ThreadGroup을 유지하는 경우 ThreadGroup.destroy() 호출합니다. ThreadGroup이 부모에서 제거하는 것을 방지하지만 모든 자식이 열거할 수 없게 되는 잘못된 누수입니다.

  • WeakHashMap과 값(in)을 사용하면 키를 직접 참조합니다. 이것은 힙 덤프 없이는 찾기 힘든 것입니다. 이는 보호된 개체에 대한 하드 참조를 다시 유지할 수 있는 모든 확장된 Weak/SoftReference

  • java.net.URL 을 사용하고(!)에서 리소스를 로드합니다. 이것은 특별합니다. KeepAliveCache 는 현재 스레드의 컨텍스트 클래스 로더를 누출하는 시스템 ThreadGroup에 새 스레드를 만듭니다. 스레드는 살아있는 스레드가 존재하지 않을 때 첫 번째 요청 시 생성되므로 운이 좋아지거나 누출될 수 있습니다. 누수는 이미 Java 7에서 수정되었으며 스레드를 생성하는 코드는 컨텍스트 클래스 로더를 적절하게 제거합니다. 몇 가지 경우가 더 있습니다( ImageFetcher처럼 , 또한 수정됨 ) 유사한 스레드를 생성합니다.

  • InflaterInputStream 을 사용하여 생성자( new java.util.zip.Inflater() PNGImageDecoder end() 를 호출하지 않습니다. 음, 생성자를 new 전달하면 기회가 없습니다... 그리고 예, 생성자 매개변수로 수동으로 전달된 경우 스트림에서 close() 이것은 종료자가 필요하다고 판단할 때 공개했기 때문에 진정한 누출이 아닙니다. 그 순간까지 네이티브 메모리를 너무 많이 먹어서 Linux oom_killer가 아무런 처벌 없이 프로세스를 종료할 수 있습니다. 주요 문제는 Java의 마무리가 매우 불안정하고 G1이 7.0.2까지 악화되었다는 것입니다. 이야기의 교훈: 가능한 한 빨리 네이티브 리소스를 공개합니다. 종료자가 너무 약합니다.

  • java.util.zip.Deflater 와 같은 경우입니다. 이것은 Deflater가 Java에서 메모리가 부족하기 때문에 훨씬 더 나쁩니다. 즉, 항상 수백 KB의 기본 메모리를 할당하는 15비트(최대) 및 8개 메모리 수준(9가 최대)을 사용합니다. 다행히 Deflater 는 널리 사용되지 않으며 내가 아는 한 JDK에는 오용이 없습니다. Deflater 또는 Inflater 를 수동으로 생성하는 경우 end() 호출하십시오. 마지막 두 가지의 가장 좋은 점은 사용 가능한 일반 프로파일링 도구를 통해 찾을 수 없다는 것입니다.

(요청 시 발생하는 시간 낭비를 더 추가할 수 있습니다.)

행운을 빕니다. 누출은 악이다!


Community Wiki

여기에 있는 대부분의 예는 "너무 복잡"합니다. 그들은 극단적 인 경우입니다. 이 예제에서 프로그래머는 실수(예: equals/hashcode를 재정의하지 않음)를 하거나 JVM/JAVA의 모서리 케이스에 물렸습니다(정적 클래스 로드...). 나는 그것이 면접관이 원하는 유형이나 가장 일반적인 사례가 아니라고 생각합니다.

그러나 메모리 누수에 대한 정말 간단한 경우가 있습니다. 가비지 수집기는 더 이상 참조되지 않는 항목만 해제합니다. Java 개발자인 우리는 메모리에 대해 신경 쓰지 않습니다. 필요할 때 할당하고 자동으로 해제합니다. 괜찮은.

그러나 수명이 긴 모든 응용 프로그램은 상태를 공유하는 경향이 있습니다. 정적, 싱글톤 등 무엇이든 될 수 있습니다. 종종 중요하지 않은 응용 프로그램은 복잡한 개체 그래프를 만드는 경향이 있습니다. null에 대한 참조를 설정하는 것을 잊거나 컬렉션에서 하나의 객체를 제거하는 것을 더 자주 잊는 것만으로도 메모리 누수가 발생하기에 충분합니다.

물론 모든 종류의 리스너(UI 리스너와 같은), 캐시 또는 오래 지속되는 공유 상태는 제대로 처리되지 않으면 메모리 누수가 발생하는 경향이 있습니다. 이해해야 할 것은 이것이 Java 코너 케이스나 가비지 수집기의 문제가 아니라는 것입니다. 설계상의 문제입니다. 우리는 수명이 긴 객체에 리스너를 추가하도록 설계하지만 더 이상 필요하지 않을 때 리스너를 제거하지 않습니다. 우리는 개체를 캐시하지만 캐시에서 개체를 제거할 전략이 없습니다.

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

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

메모리 누수는 실제로 가능하며 완벽하게 예측 가능합니다. 특별한 언어 기능이나 코너 케이스가 필요하지 않습니다. 메모리 누수는 무언가가 누락되었거나 디자인 문제가 있음을 나타내는 지표입니다.


Community Wiki

대답은 전적으로 면접관이 무엇을 묻고 있다고 생각했는지에 달려 있습니다.

실제로 Java 누출을 만드는 것이 가능합니까? 물론 그렇습니다. 다른 답변에는 많은 예가 있습니다.

그러나 질문을 받았을 수 있는 여러 메타 질문이 있습니까?

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

귀하의 메타 질문을 "이 인터뷰 상황에서 사용할 수 있었던 답변은 무엇입니까?"로 읽고 있습니다. 따라서 Java 대신 인터뷰 기술에 중점을 둘 것입니다. Java 누수를 만드는 방법을 알아야 하는 상황보다 인터뷰에서 질문에 대한 답을 모르는 상황을 반복할 가능성이 더 높다고 생각합니다. 따라서 이것이 도움이 되기를 바랍니다.

인터뷰를 위해 개발할 수 있는 가장 중요한 기술 중 하나는 질문을 적극적으로 경청하고 면접관과 협력하여 질문의 의도를 추출하는 방법을 배우는 것입니다. 이것은 당신이 그들이 원하는 방식으로 그들의 질문에 대답할 수 있게 해줄 뿐만 아니라 당신이 몇 가지 중요한 의사 소통 기술을 가지고 있음을 보여줍니다. 그리고 동등하게 재능 있는 많은 개발자들 사이에서 선택을 해야 할 때 매번 응답하기 전에 듣고 생각하고 이해하는 사람을 고용할 것입니다.


Community Wiki

다음은 JDBC를 이해하지 못한다면 매우 무의미한 예입니다. finalize 구현에 의존하는 대신 버리거나 참조를 잃기 전에 Connection , StatementResultSet 인스턴스를 닫을 것으로 예상하는 방법입니다.

 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 Connection.close 가 구현된 것과 같은 방식으로 finalize 구현하지 않는 JDBC 드라이버가 있습니다. 결과 동작은 접근할 수 없는 개체가 수집되어 메모리가 회수되지만 Connection 개체와 관련된 리소스(메모리 포함)는 회수되지 않을 수 있다는 것입니다.

Connectionfinalize 메소드가 모든 것을 정리하지 못하는 경우에, 데이터베이스 서버가 연결이 살아 있지 않다는 것을 최종적으로 알아낼 때까지 데이터베이스 서버에 대한 물리적 연결이 여러 가비지 수집 주기를 지속한다는 것을 실제로 발견할 수 있습니다. 그렇다면) 닫혀야 합니다.

finalize 를 구현하는 경우에도 종료 중에 예외가 throw될 수 있습니다. finalize 가 한 번만 호출되도록 보장되므로 현재 "휴면" 개체와 연결된 메모리가 회수되지 않는다는 것입니다.

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

생각해낼 수 있는 더 많은 예가 있습니다.

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

Community Wiki

아마도 잠재적인 메모리 누수의 가장 간단한 예 중 하나와 이를 방지하는 방법은 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 )를 지울 생각을 했을까요? 그 참조는 거대한 물체를 살아있게 할 수 있습니다 ...


Community Wiki

더 이상 필요하지 않은 개체에 대한 참조를 유지할 때마다 메모리 누수가 발생합니다. Java에서 메모리 누수가 나타나는 방법과 이에 대해 수행할 수 있는 작업에 대한 예제는 Java 프로그램에서 메모리 누수 처리를 참조하세요.


Community Wiki

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(); } } }

Community Wiki

여기에서 내 대답을 복사할 수 있습니다. Java에서 메모리 누수를 일으키는 가장 쉬운 방법

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

쉬운 대답은 다음과 같습니다. 할 수 없습니다. Java는 자동 메모리 관리를 수행하고 필요하지 않은 리소스를 해제합니다. 이런 일이 일어나는 것을 막을 수는 없습니다. 항상 리소스를 해제할 수 있습니다. 수동 메모리 관리가 있는 프로그램에서는 다릅니다. malloc()을 사용하여 C에서 메모리를 얻을 수 있습니다. 메모리를 해제하려면 malloc이 반환하고 free()를 호출한 포인터가 필요합니다. 그러나 포인터가 더 이상 없으면(덮어쓰기 또는 수명 초과) 불행히도 이 메모리를 해제할 수 없으므로 메모리 누수가 발생합니다.

지금까지 다른 모든 답변은 실제로 메모리 누수가 아닌 내 정의에 있습니다. 그것들은 모두 무의미한 것들로 기억을 빠르게 채우는 것을 목표로 합니다. 그러나 언제든지 생성한 객체를 역참조할 수 있으므로 메모리를 해제할 수 있습니다. --> 누수가 없습니다. 그의 솔루션이 가비지 수집기를 끝없는 루프로 강제 실행하여 "충돌"시키는 것이기 때문에 acconrad의 대답은 꽤 가깝습니다.

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


Community Wiki

다음은 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 인스턴스가 삭제된 후에도 원래의 긴 문자열과 파생된 하위 문자열이 모두 메모리에 유지됩니다.


Community Wiki

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


Community Wiki

모든 서블릿 컨테이너( Tomcat , Jetty , GlassFish 등...)에서 실행 중인 웹 응용 프로그램을 가져옵니다. 응용 프로그램을 연속으로 10번 또는 20번 재배포합니다(서버의 autodeploy 디렉토리에서 WAR를 터치하기만 하면 충분할 수 있습니다.

아무도 이것을 실제로 테스트하지 않은 경우 애플리케이션이 자체적으로 정리를 처리하지 않았기 때문에 몇 번의 재배포 후에 OutOfMemoryError가 발생할 가능성이 높습니다. 이 테스트를 통해 서버에서 버그를 찾을 수도 있습니다.

문제는 컨테이너의 수명이 애플리케이션의 수명보다 길다는 것입니다. 컨테이너가 애플리케이션의 개체 또는 클래스에 대해 가질 수 있는 모든 참조가 가비지 수집될 수 있는지 확인해야 합니다.

웹 응용 프로그램의 배포 취소에서 살아남은 참조가 하나만 있는 경우 해당 클래스 로더와 결과적으로 웹 응용 프로그램의 모든 클래스를 가비지 수집할 수 없습니다.

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


Community Wiki

아마도 JNI를 통해 외부 네이티브 코드를 사용하여?

순수한 Java에서는 거의 불가능합니다.

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


Community Wiki

PermGen 및 XML 구문 분석과 관련하여 좋은 "메모리 누수"가 한 번 있었습니다. 우리가 사용한 XML 파서(어떤 것이었는지 기억나지 않음)는 비교를 더 빠르게 하기 위해 태그 이름에 String.intern()을 수행했습니다. 우리 고객 중 한 명이 데이터 값을 XML 속성이나 텍스트가 아닌 태그 이름으로 저장하는 좋은 아이디어를 가지고 있어서 다음과 같은 문서를 만들었습니다.

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

실제로 그들은 숫자가 아니라 더 긴 텍스트 ID(약 20자)를 사용했는데, 이 ID는 고유했고 하루에 1000만~1500만 개의 비율로 들어왔습니다. 그러면 하루에 200MB의 쓰레기가 만들어지며 다시는 필요하지 않으며 GC 처리되지 않습니다(PermGen에 있기 때문에). permgen을 512MB로 설정했기 때문에 메모리 부족 예외(OOME)가 도착하는 데 약 2일이 걸렸습니다...


Community Wiki

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

  • 버그잘못된 디자인으로 인해 발생합니다.
  • 메모리 낭비입니다.
  • 시간이 지남에 따라 악화됩니다.
  • 가비지 수집기가 청소할 수 없습니다.

전형적인 예:

개체 캐시는 문제를 해결하기 위한 좋은 출발점입니다.

 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 개체에 다른 개체에 대한 참조가 있는 경우 이 개체에는 다른 개체에 대한 참조가 다시 포함됩니다. 어떤 면에서는 이것을 일종의 메모리 누수로 간주할 수도 있습니다(잘못된 디자인으로 인해 발생함).


Community Wiki

면접관은 아마 아래 코드와 같은 순환 참조를 찾고 있었을 것입니다. 그러나 이것은 매우 모호한 질문이므로 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. */ } }

그런 다음 이것이 기술적으로 메모리 누수라고 설명할 수 있지만 실제로 누수는 Java 코드에 의해 해제되지 않은 기본 기본 리소스를 할당하는 JVM의 기본 코드로 인해 발생합니다.

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


Community Wiki

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

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

이제 Example1을 호출하고 Example1을 버리는 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는 항상 존재한다고 가정하고 코드에서 더 이상 도달할 수 없는 경우 실제로 이를 정리하려고 시도하지 않습니다. 그러나 그것은 완전히 확인되지 않았습니다.


Community Wiki

최근에 log4j로 인해 메모리 누수 상황이 발생했습니다.

Log4j에는 다른 소스에서 인터리브된 로그 출력을 구별하는 도구인 NDC(Nested Diagnostic Context) 라는 메커니즘이 있습니다. NDC가 작동하는 세분성은 스레드이므로 다른 스레드의 로그 출력을 별도로 구별합니다.

스레드별 태그를 저장하기 위해 log4j의 NDC 클래스는 스레드 개체 자체에 의해 키가 지정되는 해시 테이블을 사용하므로(스레드 ID와 반대) NDC 태그가 메모리에 남을 때까지 스레드에서 중단되는 모든 개체 객체도 메모리에 남아 있습니다. 웹 애플리케이션에서 NDC를 사용하여 요청 ID로 로그 출력에 태그를 지정하여 로그를 단일 요청과 별도로 구분합니다. 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 컨텍스트의 핵심인 스레드 개체는 거대한 목록 개체가 매달려 있는 인라인 스레드입니다. 따라서 스레드가 하던 일을 마친 후에도 NDC 컨텍스트 Hastable에 의해 hugeList에 대한 참조가 유지되어 메모리 누수가 발생합니다.


Community Wiki

정적 맵을 만들고 여기에 대한 하드 참조를 계속 추가하십시오. 그것들은 절대 가비지 수집되지 않습니다.

 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); } }

Community Wiki

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

  1. 네이티브 메서드를 선언합니다.
  2. 기본 메서드에서 malloc 호출합니다. free 전화하지 마십시오.
  3. 네이티브 메서드를 호출합니다.

네이티브 코드의 메모리 할당은 JVM 힙에서 가져온다는 것을 기억하십시오.


Community Wiki

해당 클래스의 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()); } } }

Community Wiki

아직 아무도 이 말을 하지 않았다고 생각합니다. finalize()가 this에 대한 참조를 어딘가에 저장하도록 finalize() 메서드를 재정의하여 객체를 부활시킬 수 있습니다. 가비지 수집기는 개체에 대해 한 번만 호출되므로 그 이후에는 개체가 파괴되지 않습니다.


Community Wiki

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

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

글쎄, 이것을 흥미롭게 만드는 것은: 이런 식으로 JVM의 힙이 아닌 기본 프로세스의 힙 메모리를 누출할 수 있다는 것입니다.

Java 코드에서 참조할 내부 파일이 있는 jar 파일만 있으면 됩니다. 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를 초과하지 않습니다.

아주 간단하고 놀랍습니다.


Community Wiki

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

그것은이에만 적용됩니다 왜 엉덩이에 어려운 TreeMap A : S,하지만 구현을 보면 그 이유는 될 수 TreeMap.Entry 때문에 경우에, 그 형제에 저장 참조 TreeMap 수집 할 준비가되어 있지만, 다른 클래스는 보유 Map.Entry 중 하나에 대한 참조가 있으면 전체 Map이 메모리에 유지됩니다.


실제 시나리오:

TreeMap 데이터 구조를 반환하는 db 쿼리가 있다고 상상해보십시오. 사람들은 일반적으로 요소 삽입 순서가 유지되기 때문에 TreeMap

 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 사용할 때의 그래프입니다.

메모리 덤프 - HashMap

해결책? Map.Entry 저장하는 대신 키/값을 직접 저장하기만 하면 됩니다.


여기 에서 보다 광범위한 벤치마크를 작성했습니다.


Community Wiki

많은 사람들이 제안한 것처럼 리소스 누수 는 JDBC 예제와 같이 상당히 쉽게 발생합니다. 실제 메모리 누수는 좀 더 어렵습니다. 특히 JVM의 깨진 비트에 의존하지 않는 경우에 그렇습니다...

매우 큰 공간을 차지하는 개체를 만든 다음 액세스할 수 없다는 아이디어도 실제 메모리 누수가 아닙니다. 아무 것도 액세스할 수 없으면 가비지 수집되고, 액세스할 수 있는 경우 누출이 아닙니다...

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


Community Wiki

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

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

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


Community Wiki

스레드는 종료될 때까지 수집되지 않습니다. 가비지 컬렉션의 루트 역할을 합니다. 그것들을 잊어버리거나 참조를 지우는 것만으로는 회수되지 않는 몇 안 되는 개체 중 하나입니다.

고려하십시오: 작업자 스레드를 종료하는 기본 패턴은 스레드에서 볼 수 있는 일부 조건 변수를 설정하는 것입니다. 스레드는 주기적으로 변수를 확인하고 이를 종료 신호로 사용할 수 있습니다. 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 전달된 객체는 절대 죽지 않습니다.


Community Wiki

스레드가 풀링되는 환경에서 ThreadLocal 변수를 사용하는 것이 유효한 예일 수 있다고 생각합니다.

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

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


Community Wiki

출처 : 여기를 클릭하세요


출처 : http:www.stackoverflow.com/questions/6470651/how-can-i-create-a-memory-leak-in-java

반응형