객체 마무리 및 정리

3 개월 전, 저는 개체의 수명이 시작될 때 적절한 초기화에 초점을 맞춘 디자인 원칙에 대한 논의와 함께 개체 디자인에 대한 미니 시리즈 기사를 시작했습니다. 이 디자인 기술 기사에서는 개체 수명이 끝날 때 적절한 정리를 보장하는 데 도움이되는 디자인 원칙에 초점을 맞출 것입니다.

왜 청소합니까?

Java 프로그램의 모든 개체는 유한 컴퓨팅 리소스를 사용합니다. 가장 분명한 것은 모든 객체가 이미지를 힙에 저장하기 위해 약간의 메모리를 사용한다는 것입니다. (인스턴스 변수를 선언하지 않는 객체의 경우에도 마찬가지입니다. 각 객체 이미지는 클래스 데이터에 대한 포인터를 포함해야하며 다른 구현 종속 정보도 포함 할 수 있습니다.) 그러나 객체는 메모리 외에 다른 유한 리소스를 사용할 수도 있습니다. 예를 들어, 일부 객체는 파일 핸들, 그래픽 컨텍스트, 소켓 등과 같은 리소스를 사용할 수 있습니다. 객체를 디자인 할 때 시스템이 해당 리소스를 모두 사용하지 않도록 객체가 사용하는 유한 리소스를 결국 해제해야합니다.

Java는 가비지 수집 언어이기 때문에 객체와 관련된 메모리를 쉽게 해제 할 수 있습니다. 객체에 대한 모든 참조를 놓기 만하면됩니다. C 또는 C ++와 같은 언어 에서처럼 명시 적으로 객체를 해제하는 것에 대해 걱정할 필요가 없기 때문에 실수로 동일한 객체를 두 번 해제하여 메모리가 손상되는 것에 대해 걱정할 필요가 없습니다. 그러나 실제로 개체에 대한 모든 참조를 해제해야합니다. 그렇지 않으면 명시 적으로 객체를 해제하는 것을 잊었을 때 C ++ 프로그램에서 발생하는 메모리 누수처럼 메모리 누수가 발생할 수 있습니다. 그럼에도 불구하고 객체에 대한 모든 참조를 해제하는 한 해당 메모리를 명시 적으로 "해제"하는 것에 대해 걱정할 필요가 없습니다.

마찬가지로, 더 이상 필요하지 않은 객체의 인스턴스 변수가 참조하는 구성 객체를 명시 적으로 해제하는 것에 대해 걱정할 필요가 없습니다. 불필요한 개체에 대한 모든 참조를 해제하면 실제로 해당 개체의 인스턴스 변수에 포함 된 구성 개체 참조가 무효화됩니다. 현재 무효화 된 참조가 해당 구성 개체에 대한 유일한 나머지 참조 인 경우 구성 개체도 가비지 수집에 사용할 수 있습니다. 케이크 조각 맞죠?

가비지 컬렉션 규칙

가비지 콜렉션은 실제로 C 또는 C ++보다 Java에서 메모리 관리를 훨씬 쉽게 만들지 만 Java로 프로그래밍 할 때 메모리를 완전히 잊을 수는 없습니다. Java의 메모리 관리에 대해 생각해야하는시기를 알기 위해서는 Java 사양에서 가비지 콜렉션이 처리되는 방식에 대해 약간 알아야합니다.

가비지 수집은 의무 사항이 아닙니다.

가장 먼저 알아야 할 것은 JVM 사양 (Java Virtual Machine Specification)을 아무리 열심히 검색해도 명령하는 문장을 찾을 수 없다는 것입니다. 모든 JVM에는 가비지 수집기가 있어야합니다. Java Virtual Machine 사양은 VM 설계자에게 가비지 수집을 전혀 사용할지 여부를 결정하는 것을 포함하여 구현에서 메모리를 관리하는 방법을 결정하는 데 많은 여유를 제공합니다. 따라서 일부 JVM (예 : 베어 본 스마트 카드 JVM)은 각 세션에서 실행되는 프로그램이 사용 가능한 메모리에 "적합"되도록 요구할 수 있습니다.

물론 가상 메모리 시스템에서도 항상 메모리가 부족할 수 있습니다. JVM 사양에는 JVM에서 사용할 수있는 메모리 양이 명시되어 있지 않습니다. 그냥 JVM이 때마다한다고 않습니다 메모리가 부족, 그것이 던져해야합니다 OutOfMemoryError.

그럼에도 불구하고 Java 애플리케이션이 메모리 부족없이 실행될 수있는 최상의 기회를 제공하기 위해 대부분의 JVM은 가비지 수집기를 사용합니다. 가비지 수집기는 힙에서 참조되지 않은 개체가 차지하는 메모리를 회수하므로 새 개체에서 메모리를 다시 사용할 수 있으며 일반적으로 프로그램이 실행될 때 힙 조각 모음을 수행합니다.

가비지 수집 알고리즘이 정의되지 않았습니다.

JVM 사양에서 찾을 수없는 또 다른 명령은 가비지 수집을 사용하는 모든 JVM이 XXX 알고리즘을 사용해야한다는 것입니다. 각 JVM의 설계자는 구현에서 가비지 콜렉션이 작동하는 방식을 결정합니다. 가비지 수집 알고리즘은 JVM 공급 업체가 경쟁 제품보다 더 나은 구현을 위해 노력할 수있는 영역 중 하나입니다. 이는 다음과 같은 이유로 Java 프로그래머에게 중요합니다.

일반적으로 JVM 내에서 가비지 수집이 수행되는 방법을 알지 못하기 때문에 특정 개체가 언제 가비지 수집되는지 알 수 없습니다.

그래서 뭐? 물어볼 수 있습니다. 객체가 가비지 수집 될 때 관심을 가질 수있는 이유는 종료 자와 관련이 있습니다. (A 파이널이 라는 일반 자바 인스턴스 메소드로 정의 finalize()무효 반환하고 인수를 취하지 없다.) 자바 사양은 파이 나라에 대한 다음과 같은 약속을합니다

종료자가있는 개체가 차지하는 메모리를 회수하기 전에 가비지 수집기는 해당 개체의 종료자를 호출합니다.

객체가 언제 가비지 수집되는지는 모르지만 완료 가능한 객체는 가비지 수집되므로 종료된다는 것을 알고 있으므로 다음과 같이 큰 공제를 할 수 있습니다.

개체가 언제 완료되는지 알 수 없습니다.

이 중요한 사실을 뇌에 각인하고 이것이 Java 객체 설계에 정보를 제공 할 수 있도록해야합니다.

피해야 할 종료 자

파이널 라이저와 관련된 주요 경험 법칙은 다음과 같습니다.

정확성이 "시의 적절한"마무리에 따라 달라 지도록 Java 프로그램을 설계하지 마십시오.

즉, 특정 개체가 프로그램 실행 중 특정 시점에 의해 완료되지 않으면 중단되는 프로그램을 작성하지 마십시오. 그러한 프로그램을 작성하면 JVM의 일부 구현에서는 작동하지만 다른 구현에서는 실패 할 수 있습니다.

비 메모리 리소스를 해제하기 위해 종료 자에 의존하지 마십시오.

이 규칙을 위반하는 객체의 예로는 생성자에서 파일을 열고 해당 finalize()메서드 에서 파일을 닫는 객체가 있습니다. 이 디자인은 깔끔하고 깔끔하며 대칭 적으로 보이지만 잠재적으로 교활한 버그를 만들 수 있습니다. Java 프로그램은 일반적으로 처리 할 수있는 제한된 수의 파일 핸들 만 갖습니다. 모든 핸들이 사용 중이면 프로그램은 더 이상 파일을 열 수 없습니다.

이러한 객체 (생성자에서 파일을 열고 종료 자에서 닫는 객체)를 사용하는 Java 프로그램은 일부 JVM 구현에서 제대로 작동 할 수 있습니다. 이러한 구현에서는 항상 충분한 수의 파일 핸들을 사용할 수 있도록 충분히 자주 종료됩니다. 그러나 가비지 수집기가 프로그램의 파일 핸들 부족을 방지 할만큼 자주 종료되지 않는 다른 JVM에서는 동일한 프로그램이 실패 할 수 있습니다. 또는 더욱 교활한 것은 프로그램이 현재 모든 JVM 구현에서 작동하지만 향후 몇 년 (및 릴리스주기)에 미션 크리티컬 상황에서 실패 할 수 있다는 것입니다.

기타 종료 자 규칙

JVM 설계자에게 남은 두 가지 결정은 종료자를 실행할 스레드 (또는 스레드)와 종료자가 실행되는 순서를 선택하는 것입니다. 종료자는 단일 스레드에서 순차적으로 또는 여러 스레드에서 동시에 임의의 순서로 실행할 수 있습니다. 프로그램이 특정 순서로 실행되거나 특정 스레드에 의해 실행되는 종료 자에 대한 정확성에 어떻게 든 의존하는 경우 일부 JVM 구현에서는 작동하지만 다른 구현에서는 실패 할 수 있습니다.

또한 Java는 finalize()메서드가 정상적으로 반환 되는지 또는 예외를 throw하여 갑작스럽게 완료되는지 여부에 관계없이 객체가 완료되는 것으로 간주한다는 점을 염두에 두어야합니다 . 가비지 수집기는 종료자가 throw 한 예외를 무시하고 예외가 throw되었음을 애플리케이션의 나머지 부분에 알리지 않습니다. 특정 종료자가 특정 임무를 완수하도록해야하는 경우 종료자가 임무를 완료하기 전에 발생할 수있는 모든 예외를 처리하도록 해당 종료자를 작성해야합니다.

종료 자에 대한 또 하나의 경험 법칙은 애플리케이션의 수명이 끝날 때 힙에 남아있는 객체와 관련이 있습니다. 기본적으로 가비지 수집기는 응용 프로그램이 종료 될 때 힙에 남아있는 개체의 종료자를 실행하지 않습니다. 이 기본값을 변경하려면 단일 매개 변수로 전달 하여 또는 runFinalizersOnExit()클래스 의 메소드를 호출해야합니다 . 프로그램에 프로그램이 종료되기 전에 반드시 종료자를 호출해야하는 객체가 포함되어있는 경우 프로그램의 어딘가에서 호출 해야합니다.RuntimeSystemtruerunFinalizersOnExit()

그렇다면 종료자는 무엇에 좋은가요?

이제 파이널 라이저를 많이 사용하지 않는다는 느낌을받을 수 있습니다. 디자인하는 대부분의 클래스에는 종료자를 포함하지 않을 가능성이 있지만 종료자를 사용해야하는 몇 가지 이유가 있습니다.

드물기는하지만 종료자를위한 합리적인 응용 프로그램 중 하나는 네이티브 메서드에 의해 할당 된 메모리를 해제하는 것입니다. 객체가 메모리를 할당하는 기본 메서드 (아마도를 호출하는 C 함수)를 호출하는 malloc()경우 해당 객체의 종료자는 해당 메모리를 해제하는 기본 메서드를 호출 할 수 있습니다 (호출 free()). 이 상황에서는 종료자를 사용하여 개체 대신 할당 된 메모리 (가비지 수집기에 의해 자동으로 회수되지 않는 메모리)를 해제합니다.

더 일반적으로 사용되는 또 다른 종료자는 파일 핸들이나 소켓과 같은 비 메모리 유한 리소스를 해제하기위한 대체 메커니즘을 제공하는 것입니다. 앞서 언급했듯이 유한 비 메모리 리소스를 해제하기 위해 종료 자에 의존해서는 안됩니다. 대신 리소스를 해제 할 방법을 제공해야합니다. 그러나 리소스가 이미 해제되었는지 확인하는 종료자를 포함 할 수도 있습니다. 그렇지 않은 경우 계속 진행하여 해제합니다. 이러한 종료자는 수업의 부주의 한 사용을 방지합니다 (그리고 권장하지 않을 것입니다). 클라이언트 프로그래머가 리소스를 해제하기 위해 제공 한 메서드를 호출하는 것을 잊은 경우 개체가 가비지 수집되면 종료자가 리소스를 해제합니다. finalize()의 방법LogFileManager 이 기사의 뒷부분에 나오는 클래스는 이러한 종류의 종료 자의 예입니다.

종료 자 남용 방지

최종화의 존재는 JVM에 대한 흥미로운 복잡성과 Java 프로그래머에게 흥미로운 가능성을 생성합니다. 최종화가 프로그래머에게 부여하는 것은 객체의 삶과 죽음에 대한 권한입니다. 요컨대, 자바에서는 객체를 종료 자에서 부활시키는 것이 가능하고 완전히 합법적입니다. 즉, 객체를 다시 참조하여 다시 생명으로 되돌릴 수 있습니다. (종료자가이 작업을 수행 할 수있는 한 가지 방법은 아직 "살아있는"정적 연결 목록에 마무리중인 객체에 대한 참조를 추가하는 것입니다.) 이러한 힘은 중요하다고 느끼기 때문에 연습하고 싶은 유혹이있을 수 있지만 경험의 법칙 이 힘을 사용하려는 유혹에 저항하는 것입니다. 일반적으로 종료 자에서 개체를 부활시키는 것은 종료 자 남용으로 간주됩니다.

이 규칙의 주된 이유는 부활을 사용하는 모든 프로그램을 부활을 사용하지 않는 이해하기 쉬운 프로그램으로 재 설계 할 수 있다는 것입니다. 이 정리에 대한 공식적인 증거는 독자에게 연습으로 남겨져 있지만 (저는 항상 그렇게 말하고 싶었습니다), 비공식적으로 객체 부활은 객체 완성만큼 무작위적이고 예측할 수 없다는 것을 고려하십시오. 따라서 부활을 사용하는 디자인은 Java에서 가비지 수집의 특이성을 완전히 이해하지 못하는 다음 유지 관리 프로그래머가 파악하기 어려울 것입니다.

개체를 다시 활성화해야한다고 생각하는 경우 동일한 이전 개체를 부활시키는 대신 개체의 새 복사본을 복제하는 것이 좋습니다. 이 조언의이면에있는 이유는 JVM의 가비지 수집기 finalize()가 객체 의 메서드를 한 번만 호출 하기 때문입니다. 해당 객체가 부활하고 두 번째로 가비지 수집에 사용할 수있게되면 객체의 finalize()메서드가 다시 호출되지 않습니다.