수색…


비고

Java에서 객체는 힙에 할당되고 힙 메모리는 자동 가비지 수집으로 회수됩니다. 응용 프로그램은 Java 오브젝트를 명시 적으로 삭제할 수 없습니다.

Java 가비지 콜렉션의 기본 원칙은 가비지 콜렉션 예제에 설명되어 있습니다. 다른 예는 마무리, 손으로 가비지 컬렉터를 트리거하는 방법 및 스토리지 누출 문제를 설명합니다.

완료

Java 객체는 finalize 메소드를 선언 할 수 있습니다. 이 메소드는 Java가 객체의 메모리를 해제하기 바로 전에 호출됩니다. 일반적으로 다음과 같이 표시됩니다.

public class MyClass {
  
    //Methods for the class

    @Override
    protected void finalize() throws Throwable {
        // Cleanup code
    }
}

그러나 Java 파이널라이제이션의 동작에 대한 몇 가지 중요한주의 사항이 있습니다.

  • Java는 finalize() 메서드가 호출 될 때를 보장하지 않습니다.
  • 자바는 실행중인 애플리케이션의 수명 동안 finalize() 메소드가 언젠가 호출되도록 보장하지 않는다.
  • 유일한 보장은 객체가 삭제되기 전에 메소드가 호출된다는 것입니다. 객체가 삭제되면.

위의주의 사항은 적시에 수행해야하는 정리 (또는 기타) 작업을 수행하기 위해 finalize 메소드에 의존하는 것은 좋지 않은 아이디어라는 것을 의미합니다. 마무리에 대한 의존도가 높아지면 저장소 누수, 메모리 누수 및 기타 문제가 발생할 수 있습니다.

즉, 최종 마무리가 실제로 좋은 솔루션 인 상황은 거의 없습니다.

종료자는 한 번만 실행됩니다.

일반적으로 개체는 마무리 된 후에 삭제됩니다. 그러나 이것은 항상 발생하지는 않습니다. 다음 예제 1을 고려하십시오.

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

    protected void finalize() {
        // Resurrection!
        notDeadYet = this;
    }
}

CaptainJack 의 인스턴스가 도달 할 수 없게되고 가비지 컬렉터가이를 다시 시도하면 finalize() 메서드는 인스턴스에 대한 참조를 notDeadYet 변수에 notDeadYet 합니다. 이렇게하면 인스턴스를 다시 연결할 수있게되고 가비지 수집기가 인스턴스를 삭제하지 않습니다.

질문 : 캡틴 잭은 불멸의 존재입니까?

답변 : 아니오.

catch는 JVM이 수명 기간 동안 한 번만 객체에 finalizer를 실행한다는 것입니다. notDeadYet 인스턴스에 다시 도달 할 수 notDeadYet null 을 할당하면 가비지 수집기가 객체에서 finalize() 를 호출하지 않습니다.

1 - https://en.wikipedia.org/wiki/Jack_Harkness를 참조 하십시오 .

수동으로 GC 트리거

호출하여 수동으로 가비지 수집기를 트리거 할 수 있습니다.

System.gc();

그러나 Java는 호출이 리턴 될 때 가비지 콜렉터가 실행되었음을 보장하지 않습니다. 이 메소드는 JVM (Java Virtual Machine)에 가비지 컬렉터를 실행하기를 "제안"하지만 그렇게하지는 않습니다.

일반적으로 수동으로 가비지 수집을 시도하는 것은 나쁜 습관으로 간주됩니다. JVM은 -XX:+DisableExplicitGC 옵션과 함께 실행하여 System.gc() 호출을 비활성화 할 수 있습니다. System.gc() 를 호출하여 가비지 수집을 트리거하면 JVM에서 사용하는 특정 가비지 수집기 구현의 일반적인 가비지 관리 / 객체 승격 활동이 중단 될 수 있습니다.

가비지 수집

C ++ 접근법 - 신규 및 삭제

C ++과 같은 언어에서 응용 프로그램은 동적으로 할당 된 메모리가 사용하는 메모리를 관리합니다. new 연산자를 사용하여 C ++ 힙에서 객체를 만들면 delete 연산자를 사용하여 객체를 delete 해야합니다.

  • 프로그램이 객체를 delete 하는 것을 잊어 버렸고 객체를 잊어 버리면 관련 메모리가 응용 프로그램에 유실됩니다. 이 상황에 대한 용어는 메모리 누수 이며 너무 많은 메모리 누수가 발생하면 응용 프로그램이 점점 더 많은 메모리를 사용하게되어 결국 충돌하게됩니다.

  • 반면에 응용 프로그램이 동일한 객체를 두 번 delete 하려고 시도하거나 객체를 delete 한 후에 사용하면 메모리 손상 문제로 인해 응용 프로그램이 충돌하기 쉽습니다

복잡한 C ++ 프로그램에서는 newdelete 사용하여 메모리 관리를 구현하는 데 많은 시간이 소요될 수 있습니다. 실제로 메모리 관리는 버그의 공통 소스입니다.

Java 접근 방식 - 가비지 수집

Java는 다른 접근 방식을 취합니다. 명시 적 delete 연산자 대신 Java는 가비지 콜렉션이라는 자동 메커니즘을 제공하여 더 이상 필요하지 않은 객체가 사용하는 메모리를 회수합니다. Java 런타임 시스템은 처리 할 객체를 찾는 책임을집니다. 이 작업은 GC ( garbage collector ) 또는 GC라고하는 구성 요소에 의해 수행됩니다.

Java 프로그램을 실행하는 동안 언제든지 기존의 모든 객체 세트를 두 개의 개별 하위 세트 1 로 나눌 수 있습니다.

  • 도달 가능한 객체는 다음과 같이 JLS에 의해 정의됩니다.

    도달 가능한 객체 란 실제 스레드에서 계속되는 잠재적 계산에서 액세스 할 수있는 객체입니다.

    실제로 이것은 범위 내 로컬 변수 또는 일부 코드가 객체에 도달 할 수있는 static 변수에서 시작하는 참조 체인이 있음을 의미합니다.

  • 연결할 수없는 개체는 위와 같이 도달 할 수없는 개체입니다.

도달 할 수없는 모든 개체는 가비지 수집 대상 입니다. 이것은 그들이 쓰레기 수집되는 것은 아닙니다. 사실로:

  • 연결할 수없는 개체 도달 할 수 없을 때 즉시 수집 되지 않습니다 1 .
  • 도달 할 수없는 개체 가비지 수집 되지 않을 수 있습니다.

Java 언어 스펙은 도달 할 수없는 오브젝트를 수집 할시기를 결정하기 위해 JVM 구현에 많은 위도를 제공합니다. 또한 (실제로) 도달 할 수없는 객체를 감지하는 방식에서 JVM 구현이 보수적 인 권한을 부여합니다.

JLS가 보장하는 한 가지는 도달 가능한 객체가 가비지 수집되지 않는다는 것입니다.

객체가 도달 할 수 없게되면 어떻게됩니까?

우선, 객체 도달 할 수 없게되면 특별히 아무것도 발생하지 않습니다. 가비지 수집기가 실행시에 개체에 접근 할 수없는 것을 감지하면 상황에만 발생합니다. 또한 GC 실행이 도달 할 수없는 모든 객체를 감지하지 못하는 것이 일반적입니다.

GC가 도달 할 수없는 개체를 감지하면 다음 이벤트가 발생할 수 있습니다.

  1. 어떤이있는 경우 Reference 객체 참조 객체 객체가 삭제되기 전에, 이러한 참조가 삭제됩니다.

  2. 객체가 finalizable 이면 finalize됩니다. 이것은 객체가 삭제되기 전에 발생합니다.

  3. 개체를 삭제할 수 있으며 개체를 다시 사용할 수 있습니다.

위의 이벤트 발생할 있는 명확한 시퀀스가 ​​있지만 가비지 수집기가 특정 시간 프레임에서 특정 개체의 최종 삭제를 수행 할 필요는 없습니다.

도달 가능한 객체 및 도달 할 수없는 객체의 예

다음 예제 클래스를 고려하십시오.

// A node in simple "open" linked-list.
public class Node {
    private static int counter = 0;

    public int nodeNumber = ++counter;
    public Node next;
}

public class ListTest {
    public static void main(String[] args) {
        test();                    // M1
        System.out.prinln("Done"); // M2
    }
    
    private static void test() {
        Node n1 = new Node();      // T1
        Node n2 = new Node();      // T2
        Node n3 = new Node();      // T3
        n1.next = n2;              // T4
        n2 = null;                 // T5
        n3 = null;                 // T6
    }
}

test() 가 호출 될 때 어떤 일이 일어나는지 살펴 보겠습니다. 문 T1, T2 및 T3은 Node 객체를 만들고 객체는 모두 n1 , n2n3 변수를 통해 모두 도달 할 수 있습니다. 문 T4는 두 번째 Node 객체에 대한 참조를 첫 번째 Node 객체의 next 필드에 할당합니다. 완료되면 두 번째 Node 를 두 경로를 통해 연결할 수 있습니다.

 n2 -> Node2
 n1 -> Node1, Node1.next -> Node2

문 T5에서 우리는 n2 null 을 할당합니다. 이로 인해 Node2 의 도달 가능성 체인 중 첫 번째가 중단되지만 두 번째 Node2 는 중단되지 않으므로 Node2 는 여전히 도달 할 수 있습니다.

T6 문에서는 n3 null 을 할당합니다. 이 유일한 도달 체인 나누기 Node3 수, Node3 도달 할 수 있습니다. 그러나 Node1Node2 는 모두 n1 변수를 통해 여전히 도달 할 수 있습니다.

마지막으로 test() 메서드가 반환되면 로컬 변수 n1 , n2n3 이 범위를 벗어나서 아무 것도 액세스 할 수 없습니다. 이렇게하면 Node1Node2 의 나머지 도달 가능 체인이 끊어지며 모든 Node 객체가 도달 할 수 없으며 가비지 수집 대상이 됩니다.


1 - 이것은 finalization 및 Reference 클래스를 무시하는 단순화입니다. 가설에 따르면 Java 구현은이를 수행 할 수 있지만이를 수행하기위한 성능 비용은 비실용적입니다.

힙, PermGen 및 스택 크기 설정

Java 가상 머신이 시작될 때, 힙을 생성하는 데 필요한 크기와 스레드 스택의 기본 크기를 알아야합니다. 이러한 명령은 java 명령의 명령 행 옵션을 사용하여 지정할 수 있습니다. Java 8 이전의 Java 버전의 경우 힙의 PermGen 영역 크기를 지정할 수도 있습니다.

PermGen은 Java 8에서 제거되었으며 PermGen 크기를 설정하려고 시도하면 옵션이 무시됩니다 (경고 메시지와 함께).

힙 및 스택 크기를 명시 적으로 지정하지 않으면 JVM은 버전 및 플랫폼 특정 방식으로 계산 된 기본값을 사용합니다. 이로 인해 응용 프로그램이 너무 적거나 너무 많은 메모리를 사용하게 될 수 있습니다. 일반적으로 스레드 스택에서는 문제가 없지만 많은 메모리를 사용하는 프로그램에서는 문제가 될 수 있습니다.

힙, PermGen 및 기본 스택 크기 설정 :

다음 JVM 옵션은 힙 크기를 설정합니다.

  • -Xms<size> - 초기 힙 크기를 설정합니다.
  • -Xmx<size> - 최대 힙 크기를 설정합니다.
  • -XX:PermSize<size> - 초기 PermGen 크기를 설정합니다.
  • -XX:MaxPermSize<size> - 최대 PermGen 크기를 설정합니다.
  • -Xss<size> - 기본 스레드 스택 크기를 설정합니다.

<size> 매개 변수는 바이트 수이거나 k , m 또는 g 접미어를 가질 수 있습니다. 후자는 크기를 각각 킬로바이트, 메가 바이트 및 기가 바이트로 지정합니다.

예 :

$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp

기본 크기 찾기 :

-XX:+printFlagsFinal 옵션을 사용하여 JVM을 시작하기 전에 모든 플래그의 값을 인쇄 할 수 있습니다. 이것은 다음과 같이 힙 및 스택 크기 설정의 기본값을 인쇄하는 데 사용할 수 있습니다.

  • Linux, Unix, Solaris 및 Mac OSX의 경우

    $ java -XX : + PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'

  • Windows의 경우 :

    java -XX : + PrintFlagsFinal -version | findstr / i "HeapSize PermSize ThreadStackSize"

위의 명령의 출력은 다음과 유사합니다.

uintx InitialHeapSize                          := 20655360        {product}
uintx MaxHeapSize                              := 331350016       {product}
uintx PermSize                                  = 21757952        {pd product}
uintx MaxPermSize                               = 85983232        {pd product}
 intx ThreadStackSize                           = 1024            {pd product}

크기는 바이트 단위로 표시됩니다.

자바에서 메모리 누출

가비지 컬렉션 예제에서 우리는 Java가 메모리 누수 문제를 해결했다는 것을 의미합니다. 이것은 사실이 아닙니다. 자바 프로그램은 누수의 원인이 다소 다르긴하지만 메모리를 누출시킬 수 있습니다.

도달 가능한 객체가 새어 나올 수 있습니다.

다음 순진한 스택 구현을 고려하십시오.

public class NaiveStack {
    private Object[] stack = new Object[100];
    private int top = 0;

    public void push(Object obj) {
        if (top >= stack.length) {
            throw new StackException("stack overflow");
        }
        stack[top++] = obj;
    }

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        return stack[--top];
    }

    public boolean isEmpty() {
        return top == 0;
    }
}

객체를 push 하고 즉시 pop 하면 객체에 대한 참조가 stack 배열에 계속 남아 있습니다.

스택 구현의 논리는 해당 참조를 API 클라이언트에 반환 할 수 없음을 의미합니다. 객체가 팝되면 (자), 「라이브 thread로부터의 잠재적 인 계속되는 계산으로 액세스 할 수 없다 것을 증명할 수 있습니다. 문제는 현재의 JVM이 이것을 증명할 수 없다는 것입니다. 현재 세대의 JVM은 참조가 도달 가능한지 여부를 결정할 때 프로그램의 논리를 고려하지 않습니다. (처음에는 실용적이지 않습니다.)

그러나 도달 가능성이 실제로 무엇을 의미하는지에 대한 문제를 제외하고, 우리는 NaiveStack 구현이 매립되어야하는 객체를 "매달아 매는"상황을 명확하게 여기 있습니다. 그것은 메모리 누수입니다.

이 경우 솔루션은 간단합니다.

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        Object popped = stack[--top];
        stack[top] = null;              // Overwrite popped reference with null.
        return popped;
    }

캐시는 메모리 누수 일 수 있습니다.

서비스 성능을 개선하기위한 일반적인 전략은 결과를 캐시하는 것입니다. 아이디어는 일반적인 요청 및 결과를 캐시라고하는 메모리 내 데이터 구조에 기록하는 것입니다. 그런 다음 요청이있을 때마다 캐시에서 요청을 조회합니다. 조회가 성공하면 해당 저장된 결과를 리턴합니다.

이 전략은 제대로 구현되면 매우 효과적 일 수 있습니다. 그러나 잘못 구현되면 캐시가 메모리 누출이 될 수 있습니다. 다음 예제를 고려하십시오.

public class RequestHandler {
    private Map<Task, Result> cache = new HashMap<>();

    public Result doRequest(Task task) {
        Result result = cache.get(task);
        if (result == null) {
            result == doRequestProcessing(task);
            cache.put(task, result);
        }
        return result;
    }
}

이 코드의 문제점은 doRequest 대한 호출이 캐시에 새 항목을 추가 할 수 있지만 제거 할 수있는 것이 없다는 점입니다. 서비스가 지속적으로 다른 작업을받는다면 캐시는 결국 모든 사용 가능한 메모리를 소비합니다. 이것은 메모리 누수의 한 형태입니다.

이 문제를 해결하기위한 한 가지 방법은 최대 크기의 캐시를 사용하고 캐시가 최대 값을 초과하면 이전 항목을 버리는 것입니다. 가장 최근에 사용되지 않은 항목을 버리는 것이 좋습니다. 또 다른 방법은 WeakHashMap 사용하여 캐시를 작성하여 힙이 너무 가득 차기 시작하면 JVM에서 캐시 항목을 제거 할 수 있도록하는 것입니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow