해제 Java로 메모리 누수 생성하기




자바 메모리 해제 (24)

방금 인터뷰를했고 Java로 메모리 누출을 만들도록 요청 받았습니다. 말할 필요도없이 나는 심지어 하나 만드는 것을 시작하는 방법에 대한 단서가없는 꽤 바보 같았다.

예를 들면?


아래에는 잊혀진 청취자, 정적 참조, 해시 맵의 가짜 / 수정 가능한 키 또는 수명주기를 종료 할 수있는 기회가없는 스레드 만있는 Java 외에도 Java가 누출되는 명백한 사례가 있습니다.

  • File.deleteOnExit() - 항상 문자열을 누설합니다. 문자열이 하위 문자열이면 누수가 더욱 심각합니다 (기본 char []도 유출됩니다) - Java 7 하위 문자열에서도 char[] 복사되므로 나중에 적용되지 않습니다 . @ 대니얼, 투표 필요 없음.

스레드에 집중하여 관리되지 않는 스레드의 위험을 대부분 나타내며 스윙을 만지지 않기를 바랍니다.

  • Runtime.addShutdownHook 그리고 제거하지 ... 그리고 ThreadGroup 클래스의 버그로 인해 unstarted 스레드에 대한 removeShutdownHook의 경우에도 수집되지 않을 수 있으므로 실제로 ThreadGroup이 누수됩니다. JGroup은 GossipRouter에서 누수가 발생했습니다.

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

  • 스레드를 생성하면 ContextClassLoaderAccessControlContextThreadGroupInheritedThreadLocal 상속받습니다.이 모든 참조는 클래스 로더 및 모든 정적 참조로로드 된 전체 클래스 및 ja-ja와 함께 누출 가능성이 있습니다. 이 효과는 특히 매우 간단한 ThreadFactory 인터페이스를 특징으로하는 jucExecutor 프레임 워크 전체에서 볼 수 있습니다. 그러나 대부분의 개발자는 숨어있는 위험의 단서를 가지고 있지 않습니다. 또한 많은 라이브러리가 요청에 따라 스레드를 시작합니다 (너무 많은 산업 대중 라이브러리).

  • ThreadLocal 캐시; 그것들은 많은 경우에 악합니다. ThreadLocal을 기반으로하는 간단한 캐시를 모두 보았을 것입니다. 나쁜 소식입니다. 스레드가 예상보다 수명이 길면 ClassLoader라는 컨텍스트가 순수한 멋진 누출입니다. 정말로 필요하지 않으면 ThreadLocal 캐시를 사용하지 마십시오.

  • ThreadGroup.destroy() 호출하면 ThreadGroup 자체에 스레드가 없지만 여전히 자식 ThreadGroups가 유지됩니다. ThreadGroup이 부모로부터 제거되는 것을 막아 주지만, 모든 자식은 열거되지 않게되는 나쁜 누수입니다.

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

  • HTTP (S) 프로토콜과 java.net.URL 사용 및 (!)에서 자원로드. 이것은 특별한 것이고, KeepAliveCache 는 현재 스레드의 컨텍스트 클래스 로더를 유출하는 새로운 스레드를 시스템 ThreadGroup에 생성합니다. 살아있는 스레드가 존재하지 않을 때 첫 번째 요청시 스레드가 생성되므로 운이 좋거나 누출 될 수 있습니다. 누수는 Java 7에서 이미 수정되었으며 스레드를 생성하는 코드는 컨텍스트 클래스 로더를 올바르게 제거합니다. 몇 가지 사례가 더 있습니다 ( ImageFetcher와 같은 , 고정 된 ) 유사한 스레드를 생성합니다.

  • InflaterInputStream 을 사용하여 생성자 (예 : PNGImageDecoder new java.util.zip.Inflater() 에서 new java.util.zip.Inflater() 를 전달하고 inflater의 end() 를 호출하지 않습니다. new , 기회가없는 생성자를 전달하면, 생성자 매개 변수로 수동으로 전달되는 경우 스트림에서 close() 를 호출해도 인플레이터가 닫히지 않습니다. 이것은 최종 자에 의해 발표 될 것이기 때문에 진정한 누수가 아닙니다 ... 필요하다고 판단 될 때. 그 순간까지 네이티브 메모리를 먹어 버려서 리눅스 oom_killer가 아무런 처벌없이 프로세스를 죽게 만들 수 있습니다. 가장 중요한 문제는 Java의 마무리가 매우 불안정하고 G1이 7.0.2까지 악화되었다는 것입니다. 이야기의 도덕 : 최대한 빨리 네이티브 리소스를 배포하십시오. finalizer는 너무 가난합니다.

  • java.util.zip.Deflater 경우와 같습니다. 이것은 Deflater가 Java에서 메모리가 부족하기 때문에 더 심합니다. 즉, 항상 수백 비트의 기본 메모리를 할당하는 15 비트 (최대) 및 8 메모리 레벨 (최대 9)을 사용합니다. 다행스럽게도 Deflater 는 널리 사용되지 않으며 JDK는 오용을 포함하지 않습니다. Deflater 또는 Inflater 를 수동으로 생성하는 경우 항상 end() 호출하십시오. 마지막 두 부분 중 가장 중요한 부분은 일반 프로파일 링 도구를 통해 찾을 수 없다는 것입니다.

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

행운을 빈다. 누출은 악합니다!


많은 사람들이 제안했듯이, Resource Leaks는 JDBC 예제처럼 매우 쉽게 발생합니다. 실제 메모리 누수는 조금 더 어렵습니다. 특히 JVM의 깨진 비트에 의존하지 않는 경우 ...

공간이 매우 크고 액세스 할 수없는 객체를 만드는 아이디어는 실제 메모리 누수가 아닙니다. 아무것도 액세스 할 수 없으면 가비지 수집되고 뭔가가 액세스 할 수있는 경우 누수가 아닙니다 ...

예전 에 일 해왔 던 한 가지 방법은 - 그리고 아직도 그랬는지는 모르지만 - 3 깊어 진 원형 체인을 사용하는 것입니다. 객체 A가 객체 B를 참조하고 있기 때문에 객체 B는 객체 C를 참조하고 객체 C는 객체 A를 참조합니다. GC는 두 개의 깊은 사슬 (예 : A <-> B - A와 B가 다른 것으로 접근 할 수 없지만 3 자 체인을 처리 할 수없는 경우 안전하게 수집 할 수 있습니다 ...


메모리 누출이란?

  • 버그 또는 잘못된 디자인으로 인해 발생합니다 .
  • 기억의 낭비입니다.
  • 시간이 지남에 따라 악화됩니다.
  • 가비지 컬렉터가 청소할 수 없습니다.

대표적인 예 :

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

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 객체가 다른 객체에 대한 참조를 가지면 다시 다른 객체에 대한 참조를 갖습니다. 당신은 또한 이것을 어떤 종류의 메모리 유출이라고 생각할 수 있습니다 (나쁜 디자인으로 인해).


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


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

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

실제로, 그들은 숫자를 사용하지 않고 고유 한 문자 ID (약 20 자)를 사용하여 하루에 1 천만 -1 천 5 백만의 비율로 들어 왔습니다. 따라서 200 MB의 쓰레기가 하루에 다시 만들어지며 결코 다시는 필요하지 않으며 GCed는 PermGen에 있기 때문에 절대 사용하지 않습니다. 우리는 permgen을 512MB로 설정했기 때문에 메모리 부족 예외 (OOME)가 도착하는 데 약 2 일이 걸렸습니다 ...


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

}

다음은 JDBC 이해하지 못한다면 꽤 의미없는 예입니다. 또는 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 메소드가 모든 것을 정리하지 않는 이벤트에서 데이터베이스 서버가 실제로 연결이 작동하지 않는다고 판단 할 때까지 데이터베이스 서버에 대한 실제 연결이 여러 가비지 수집주기를 지속한다는 것을 실제로 발견 할 수 있습니다 그렇다면 닫아야 함).

JDBC 드라이버가 finalize 를 구현하는 경우에도 종료하는 동안 예외가 throw 될 수 있습니다. 결과적으로 finalize 는 한 번만 호출되는 것이 보장되므로 현재 "휴면"객체와 관련된 모든 메모리는 회수되지 않습니다.

객체 완료 중에 예외가 발생하는 위의 시나리오는 메모리 누수 (객체 부활)로 이어질 수있는 또 다른 시나리오와 관련이 있습니다. 객체 부활은 다른 객체에서 객체가 확정되는 것을 강력히 참조하여 의도적으로 수행됩니다. 객체 부활이 오용되면 다른 메모리 누수 원인과 함께 메모리 누수가 발생합니다.

당신이 상상할 수있는 많은 예들이 있습니다.

  • List 추가 만하고 목록에서 삭제하지 않는 List 인스턴스 관리 (더 이상 필요없는 요소를 없애야 함) 또는
  • Socket 또는 File 열지 만 더 이상 필요하지 않으면 닫지 않습니다 ( Connection 클래스와 관련된 위의 예제와 유사).
  • Java EE 응용 프로그램을 종료 할 때 Singleton을 언로드하지 않습니다. 분명히 싱글 톤 클래스를로드 한 Classloader는 클래스에 대한 참조를 유지하므로 싱글 톤 인스턴스는 절대로 수집되지 않습니다. 응용 프로그램의 새 인스턴스가 배포되면 일반적으로 새 클래스 로더가 만들어지고 이전 클래스 로더는 싱글 톤으로 인해 계속 존재하게됩니다.

서블릿 컨테이너 (Tomcat, Jetty, Glassfish, 무엇이든간에 ...)에서 실행중인 웹 응용 프로그램을 가져옵니다. 앱을 10-20 번 연속 배포하십시오 (서버의 autodeploy 디렉토리에서 WAR를 터치하는 것으로 충분할 수 있습니다).

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

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

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

애플리케이션에 의해 시작된 스레드, ThreadLocal 변수, 로깅 애펜더는 클래스 로더 유출을 야기 할 수있는 일반적인 용의자 중 일부입니다.


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

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

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


누구나 항상 원시 코드 경로를 잊어 버립니다. 다음은 누수에 대한 간단한 공식입니다.

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

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



나는 아직 누군가가 이것을 말했다고 생각하지 않는다 : finalize () 메소드를 오버라이드하여 객체를 부활시킬 수있다. finalize ()는이 참조를 어딘가에 저장한다. 가비지 컬렉터는 객체에 대해 한 번만 호출되므로 객체는 결코 파괴되지 않습니다.


면접자는 순환 참조 솔루션을 찾고있을 수 있습니다.

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

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

-Wes Tarle


내 대답을 복사 할 수 있습니다 : 자바에서 메모리 누출을 일으키는 가장 쉬운 방법은?

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

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

지금까지의 다른 모든 대답은 메모리 정의가 아닙니다. 그들은 모두 메모리를 무의미한 물건으로 빠르게 채우는 것을 목표로합니다. 그러나 언제든지 생성 한 객체를 참조 할 수 있으므로 메모리를 해제 할 수 있습니다 -> 누출 없음. acconrad의 대답 은 꽤 가깝습니다. 그의 솔루션이 끝없이 반복적으로 강제로 가비지 컬렉터를 "충돌"시킵니다.

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


나는 최근에 좀 더 미묘한 종류의 리소스 누출을 발견했다. 클래스 로더의 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

Et voilà : 내부에 두 개의 파일이있는 현재 작업 디렉토리에서 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를 선택한 것과 상관없이 응용 프로그램은 계속 실행됩니다. 응용 프로그램이 wait 명령에 도달하지 않는 한 프로세스의 메모리 소비 (맨 위 (RES / RSS) 또는 프로세스 탐색기에 표시)가 커집니다. 위의 설정에서 약 150MB의 메모리를 할당합니다.

응용 프로그램을 안전하게 재생하려면 입력 스트림을 닫습니다.

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

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

아주 간단하고 놀라운.


아마도 잠재적 인 메모리 누수의 가장 간단한 예제 중 하나이며이를 피하는 방법은 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 )를 지우겠습니까? 그 참조는 거대한 개체를 살아있을 수 있습니다 ...


나는 유효한 예제가 쓰레드가 풀링되는 환경에서 ThreadLocal 변수를 사용할 수 있다고 생각한다.

예를 들어, 서블릿에서 ThreadLocal 변수를 사용하여 다른 웹 구성 요소와 통신하고 컨테이너에서 스레드를 만들고 풀에서 유휴 스레드를 유지 관리 할 수 ​​있습니다. ThreadLocal 변수가 올바르게 정리되지 않으면 같은 웹 구성 요소가 해당 값을 덮어 쓸 때까지 거기에 살 수 있습니다.

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


GUI 코드의 일반적인 예는 위젯 / 컴포넌트를 생성하고 일부 정적 / 애플리케이션 범위 객체에 리스너를 추가 한 다음 위젯이 삭제 될 때 리스너를 제거하지 않는 경우입니다. 뿐만 아니라 당신이 메모리 누수를 얻을뿐만 아니라, 당신이 듣고있는 것이 무엇이든 이벤트를 발생시킬 때, 당신의 모든 오래된 청취자들도 호출됩니다.


스레드는 종료 될 때까지 수집되지 않습니다. 그들은 가비지 콜렉션의 roots 입니다. 그것들은 단순히 그것들을 잊어 버렸거나 그것들에 대한 참조를 삭제함으로써 교정되지 않는 몇 안되는 것들 중 하나입니다.

고려 : 작업자 스레드를 종료하는 기본 패턴은 스레드가 보는 일부 조건 변수를 설정하는 것입니다. 스레드는 주기적으로 변수를 검사하여이를 종료 신호로 사용할 수 있습니다. 변수가 선언되지 않으면 변수 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은 절대로 죽지 않을 것입니다.

(* 편집 *)


메모리가 누출되는 많은 다른 상황이 있습니다. 하나는 내가 노출, 다른 장소에서 사용해서는 안된다는지도를 노출 발생했습니다.

public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}

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

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

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


면접자는 아마 아래의 코드와 같은 순환 참조를 찾고 있었을 것입니다. (참조 카운팅을 사용한 아주 오래된 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은 더 이상 참조 카운팅을 사용하지 않고 대부분 가비지 수집기를 사용합니다.이 수집기는 실제로이 메모리를 수집합니다.

다음과 같이 기본 네이티브 리소스가있는 Object를 만드는 방법을 설명 할 수 있습니다.

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 코드를 작성해야합니다.


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

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

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

  • 이론적으로 "완벽한"자바 구현은 누출에 취약합니까?
  • 후보자는 이론과 현실의 차이를 이해합니까?
  • 후보자는 쓰레기 수거가 어떻게 작동하는지 이해합니까?
  • 또는 가비지 콜렉션이 이상적인 경우에 작동하는 방법은 무엇입니까?
  • 네이티브 인터페이스를 통해 다른 언어를 호출 할 수 있다는 것을 알고 있습니까?
  • 그들은 다른 언어로 기억을 누설하는 것을 알고 있습니까?
  • 후보자는 심지어 메모리 관리가 무엇인지, 그리고 Java에서 어떤 일이 일어나고 있는지 알고 있습니다.

나는 당신의 메타 질문을 "이 인터뷰 상황에서 사용할 수있는 대답은 무엇입니까?"라고 읽습니다. 따라서 Java 대신 인터뷰 기술에 중점을 둘 것입니다. 나는 인터뷰에서 질문에 대한 답을 모르는 상황을 반복 할 가능성이 높다고 생각한다. 자바 누출을 만드는 법을 알 필요가있는 곳에 있어야한다. 잘하면, 이것이 도움이 될 것입니다.

인터뷰를 위해 개발할 수있는 가장 중요한 기술 중 하나는 질문을 적극적으로 듣고 인터뷰를 통해 자신의 의도를 추출하는 것입니다. 이 방법으로 자신이 원하는 방식으로 질문에 대답 할 수있을뿐만 아니라 의사 소통 능력이 있음을 알 수 있습니다. 그리고 똑같이 재능있는 많은 개발자들 사이에서 선택이 내려지면 매번 응답하기 전에 듣고 생각하고 이해하는 사람을 고용 할 것입니다.


정적 맵을 생성하고 정적 참조를 계속 추가하십시오. 그것들은 결코 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); }
}






memory-leaks