1. JVM 메모리 영역 및 개체 메모리 레이아웃

1. 런타임 데이터 영역

JVM의 메모리 영역 다이어그램은 다음과 같습니다.

JVM 메모리 영역 맵

1.1, 프로그램 카운터

프로그램 카운터 레지스터는 작은 메모리 공간으로 현재 스레드가 실행하는 바이트 코드의 줄 번호 표시기로 볼 수 있습니다. 자바 가상 머신의 개념적 모델 [1]에서 바이트 코드 인터프리터가 작동 할 때이 카운터의 값을 변경하여 실행할 다음 바이트 코드 명령어를 선택하며, 프로그램 제어 흐름, 분기 및 루핑의 지표입니다. 점프, 예외 처리 및 스레드 복구와 같은 기본 기능은 모두이 카운터를 사용하여 완료해야합니다. Java 가상 머신의 멀티 스레딩은 스레드 전환 및 프로세서 실행 시간 할당을 통해 실현되므로 특정 순간에 프로세서 (멀티 코어 프로세서의 경우 코어)는 하나의 명령 만 실행합니다. 실. 따라서 스레드가 전환 된 후 올바른 실행 위치로 복원하기 위해서는 각 스레드에 독립적 인 프로그램 카운터가 있어야합니다. 스레드 간의 카운터는 서로 영향을주지 않고 독립적으로 저장됩니다. 이러한 유형의 메모리 영역을 "라고합니다. 스레드 전용 "메모리. 스레드가 Java 메소드를 실행하는 경우이 카운터는 실행중인 가상 머신 바이트 코드 명령의 주소를 기록합니다. 스레드가 Native 메소드를 실행하는 경우 카운터 값은 정의되지 않아야합니다. 이 메모리 영역은 "Java Virtual Machine Specification"에서 OutOfMemoryError 조건을 지정하지 않는 유일한 영역입니다.

1.2, 가상 머신 스택

프로그램 카운터와 마찬가지로 Java Virtual Machine Stack (Java Virtual Machine Stack)도 스레드 전용이며 수명주기는 스레드의 수명주기와 동일합니다. 가상 머신 스택은 Java 메소드 실행의 스레드 메모리 모델을 설명합니다. 각 메소드가 실행될 때 Java 가상 머신은 동기식으로 스택 프레임을 생성하여 로컬 변수 테이블, 피연산자 스택, 동적 연결 및 메소드를 저장합니다. 내보내기 및 기타 정보. 각 메서드가 호출 될 때부터 실행이 완료 될 때까지의 프로세스는 가상 머신 스택에서 푸시에서 팝까지 스택 프레임의 프로세스에 해당합니다.

로컬 변수 테이블에서 이러한 데이터 유형의 저장 공간은 로컬 변수 슬롯 (Slot)으로 표시됩니다. 여기서 64 비트 long 및 double 데이터는 두 개의 변수 슬롯을 차지하고 나머지 데이터 유형은 하나만 차지합니다. 로컬 변수 테이블이 필요로하는 메모리 공간은 컴파일 과정에서 할당됩니다. 메소드 입력시 스택 프레임에서이 메서드가 할당해야하는 로컬 변수 공간의 양이 완전히 결정되며 로컬 변수 테이블의 크기는 컴파일 과정에서 변경되지 않습니다. 메소드 실행. 여기에 언급 된 "크기"는 가변 슬롯의 수, 가상 머신이 실제로 사용하는 메모리 공간 (예 : 32 비트, 64 비트 이상을 차지하는 가변 슬롯에 따라)을 의미합니다. , 이것은 특정 가상 머신의 구현에 의해 완전히 결정됩니다. "Java Virtual Machine Specification"에서이 메모리 영역에 대해 두 가지 유형의 비정상 조건이 지정됩니다. 스레드에서 요청한 스택 깊이가 가상 머신에서 허용하는 깊이보다 크면 StackOverflowError 예외가 발생합니다. 가상 머신 스택 용량은 동적으로 확장 될 수 있습니다. 스택이 확장되면 충분한 메모리를 사용할 수없는 경우 OutOfMemoryError가 발생합니다.

1.3, 로컬 메서드 스택

기본 메소드 스택 (Native Method Stacks)과 가상 머신 스택은 매우 유사한 역할을합니다. 차이점은 가상 머신 스택은 가상 머신을 제공하여 Java 메소드 (즉, 바이트 코드)를 실행하는 반면 원시 메소드 스택은 가상 머신을 제공한다는 것입니다. 머신에서 사용하는 기본 메소드 서비스입니다. "Java Virtual Machine Specification"에는 로컬 메소드 스택에있는 메소드의 언어, 사용법 및 데이터 구조에 대한 필수 조항이 없습니다. 따라서 특정 가상 머신은 필요에 따라 자유롭게 구현할 수 있으며 일부 Java 가상 머신 ( 핫스팟과 같은 가상 머신)은 로컬 메소드 스택과 가상 머신 스택을 하나로 직접 결합합니다. 가상 머신 스택과 마찬가지로 로컬 메서드 스택은 스택 깊이가 오버플로되거나 스택 확장이 실패 할 때 각각 StackOverflowError 및 OutOfMemoryError 예외를 발생시킵니다.

1.4, 자바 힙

Java 애플리케이션의 경우 Java 힙은 가상 머신에서 관리하는 가장 큰 메모리 조각입니다. Java 힙은 모든 스레드가 공유하는 메모리 영역이며 가상 머신이 시작될 때 생성됩니다. 이 메모리 영역의 유일한 목적은 객체 인스턴스를 저장하는 것이며 Java 세계의 "거의"모든 객체 인스턴스는 여기에 메모리를 할당합니다. "Java Virtual Machine Specification"에서 Java 힙에 대한 설명은 "모든 개체 인스턴스 및 배열이 힙에 할당되어야합니다"이며 여기에서 작성자는 "거의"구현 관점에서 Java 언어를 사용함을 의미합니다. 이제 값 유형에 대한 지원이 미래에 나타날 수 있다는 징후를 볼 수 있습니다. 지금 만 고려하더라도 Just-In-Time 컴파일 기술, 특히 점점 더 강력 해지는 이스케이프 분석 기술, 할당 최적화 방법의 발전으로 인해 스택 및 스칼라 교체로 인해 일부 미묘한 변경이 조용히 발생하여 Java 객체 인스턴스가 힙에 할당되고 점차적으로 덜 절대적이됩니다. Java 힙은 가비지 수집기에서 관리하는 메모리 영역이므로 일부 자료에서는 "GC 힙"이라고도합니다 (다행히도 중국에서는 "가비지 힙"으로 번역되지 않음). 기억을 되 찾는 관점에서 보면 대부분의 현대 가비지 수집기는 세대 별 수집 이론을 기반으로 설계 되었기 때문에 종종 "신세대", "구세대", "영구 세대", "에덴 공간"및 "서바이버에서"가 있습니다. Java 힙. "space"및 "To Survivor space"와 같은 명사는이 책의 다음 장에서 반복적으로 나타납니다. 여기서는 이러한 구분이 가비지 수집기의 일반적인 기능 또는 디자인 스타일 중 일부에 불과하다는 점을 먼저 설명하겠습니다. 특정 Java 가상 머신에 의해 구현 된 고유 한 메모리 레이아웃도 아니고 "Java Virtual Machine Specification"에있는 Java 힙의 추가 세부 분할도 아닙니다. 많은 자료는 종종 "자바 가상 머신의 힙 메모리는 젊은 세대, 구세대, 영구 세대, Eden, Survivor ..."로 나뉩니다. 10 년 전 (경계로 G1 수집기의 출현과 함께) 업계의 절대 주류 HotSpot 가상 머신 인 내부 가비지 수집기는 모두 "클래식 세대"를 기반으로 설계되었으므로 신세대 및 구세대 수집기가 In과 일치해야합니다. 이 문맥에서 위의 진술은 너무 모호하지 않습니다. 그러나 오늘날 가비지 수집기 기술은 10 년 전과 동일하지 않으며 HotSpot도 게시했습니다. 세대 별 디자인을 채택하지 않은 새로운 가비지 컬렉터가 있는데, 위의 공식에 따라 논의 할 점이 많다. 메모리 할당의 관점에서 모든 스레드가 공유하는 Java 힙을 여러 스레드 전용 할당 버퍼 (Thread Local Allocation Buffer, TLAB)로 나누어 개체 할당의 효율성을 높일 수 있습니다. 그러나 어떤 각도에서든 분할이 무엇이든 Java 힙에있는 스토리지 컨텐츠의 공통성은 변경되지 않습니다. 어느 영역에서든 스토리지는 객체의 인스턴스 일 수만 있습니다. Java 힙은 더 나은 재활용만을위한 것입니다. 메모리 또는 메모리를 더 빠르게 할당하십시오. 이 장에서는 메모리 영역의 역할에 대해서만 설명하고, 위에서 언급 한 Java 힙 영역의 할당 및 복구에 대한 자세한 내용은 다음 장에서 설명합니다. "Java Virtual Machine Specification"에 따르면 Java 힙은 물리적으로 불연속적인 메모리 공간에있을 수 있지만 논리적으로는 연속적인 것으로 간주되어야합니다. 이는 디스크 공간을 사용하여 파일을 저장할 때와 동일합니다. 각 문서는 다음을 수행해야합니다. 지속적으로 저장됩니다. 그러나 대형 객체 (일반적으로 어레이 객체)의 경우 대부분의 가상 머신 구현에는 간단한 구현과 높은 스토리지 효율성을 위해 연속적인 메모리 공간이 필요할 수 있습니다. Java 힙은 고정 크기 또는 확장 가능으로 구현 될 수 있지만 현재 주류 Java 가상 머신은 확장 성 (매개 변수 -Xmx 및 -Xms로 설정)에 따라 구현됩니다. 인스턴스 할당을 완료하기위한 Java 힙에 메모리가없고 힙을 더 이상 확장 할 수없는 경우 Java 가상 머신은 OutOfMemoryError 예외를 발생시킵니다. "Java Virtual Machine Specification"에 따르면 Java 힙은 물리적으로 불연속적인 메모리 공간에있을 수 있지만 논리적으로는 연속적인 것으로 간주되어야합니다. 이는 디스크 공간을 사용하여 파일을 저장할 때와 동일합니다. 각 문서는 다음을 수행해야합니다. 지속적으로 저장됩니다. 그러나 대형 객체 (일반적으로 어레이 객체)의 경우 대부분의 가상 머신 구현에는 간단한 구현과 높은 스토리지 효율성을 위해 연속적인 메모리 공간이 필요할 수 있습니다. Java 힙은 고정 크기 또는 확장 가능으로 구현 될 수 있지만 현재 주류 Java 가상 머신은 확장 성 (매개 변수 -Xmx 및 -Xms로 설정)에 따라 구현됩니다. 인스턴스 할당을 완료하기위한 Java 힙에 메모리가없고 힙을 더 이상 확장 할 수없는 경우 Java 가상 머신은 OutOfMemoryError 예외를 발생시킵니다. "Java Virtual Machine Specification"에 따르면 Java 힙은 물리적으로 불연속적인 메모리 공간에있을 수 있지만 논리적으로는 연속적인 것으로 간주되어야합니다. 이는 디스크 공간을 사용하여 파일을 저장할 때와 동일합니다. 각 문서는 다음을 수행해야합니다. 지속적으로 저장됩니다. 그러나 대형 객체 (일반적으로 어레이 객체)의 경우 대부분의 가상 머신 구현에는 간단한 구현과 높은 스토리지 효율성을 위해 연속적인 메모리 공간이 필요할 수 있습니다. Java 힙은 고정 크기 또는 확장 가능으로 구현 될 수 있지만 현재 주류 Java 가상 머신은 확장 성 (매개 변수 -Xmx 및 -Xms로 설정)에 따라 구현됩니다. 인스턴스 할당을 완료하기위한 Java 힙에 메모리가없고 힙을 더 이상 확장 할 수없는 경우 Java 가상 머신은 OutOfMemoryError 예외를 발생시킵니다.

1.5, 방법 영역

자바 힙과 같은 메소드 영역은 각 스레드가 공유하는 메모리 영역으로,로드 된 Just-In-Time 컴파일러에 의해 컴파일 된 유형 정보, 상수, 정적 변수 및 코드 캐시와 같은 데이터를 저장하는 데 사용됩니다. 가상 머신에 의해. "Java Virtual Machine Specification"에서는 메소드 영역을 힙의 논리적 부분으로 설명하지만 Java 힙과 구별하기 위해 "Non-Heap"이라는 별명이 있습니다. 메소드 영역에 대해 말하면 특히 JDK 8 이전에 "영구 생성"개념을 언급해야합니다. 많은 Java 프로그래머가 HotSpot 가상 머신에서 프로그램을 개발하고 배포하는 데 익숙하며 많은 사람들이 메소드 영역을 ""라고 부르는 것을 선호합니다. 영구 세대 "(영구 세대) 또는 둘을 혼동하십시오. 본질적으로이 두 가지는 동일하지 않습니다. 당시 HotSpot 가상 머신 설계 팀은 수집기의 세대 별 설계를 메서드 영역으로 확장하거나 영구 생성을 사용하여 메서드 영역을 구현하기로 선택했기 때문에 HotSpot 가비지를 만들 수 있습니다. Java 힙과 같은 메모리의이 부분을 관리하여 메서드 영역에 대한 메모리 관리 코드를 작성하는 작업을 절약 할 수 있습니다. 그러나 BEA JRockit, IBM J9 등과 같은 다른 가상 머신 구현의 경우 영구 생성 개념이 없습니다. 원칙적으로 메소드 영역을 구현하는 방법은 가상 머신의 구현 세부 사항에 속하며 "Java Virtual Machine 사양"의 대상이 아니며 균일 성이 필요하지 않습니다. 그러나 지금 되돌아 보면 영구 생성을 사용하여 메서드 영역을 구현하기로 결정한 것은 좋은 생각이 아니 었습니다.이 설계로 인해 Java 응용 프로그램이 메모리 오버플로 문제가 발생할 가능성이 더 높아졌습니다 (영구 생성의 상한은 -XX : MaxPermSize입니다. 설정에는 기본 크기도 있으며 J9 및 JRockit이 32 비트 시스템의 4GB 제한과 같은 프로세스 사용 가능한 메모리의 상한을 터치하지 않는 한 문제가 없습니다.) 메서드가 거의 없습니다 (예 : String :: intern ())은 영구 생성으로 인해 다른 가상 머신에서 다른 성능을 유발합니다. Oracle은 BEA를 인수하고 JRockit의 소유권을 획득 한 후 Java Mission Control 관리 도구와 같은 JRockit의 우수한 기능을 HotSpot 가상 머신으로 마이그레이션 할 준비가되었지만 두 시스템 간의 방법 영역의 차이로 인해 많은 어려움에 직면했습니다. 둘. HotSpot의 향후 개발을 고려하여 JDK 6에서 HotSpot 개발 팀은 영구 생성을 포기하고 메서드 영역을 구현하기 위해 네이티브 메모리를 사용하는 계획으로 점차 변경했습니다. 문자열 상수 풀, 정적 변수 등이 제거되었고 JDK 8에서는 영구 생성 개념이 완전히 폐기되었습니다. 대신 JRockit 및 J9와 같은 로컬 메모리에 구현 된 메타 스페이스 (Metaspace)를 대신 사용하고 JDK를 사용했습니다. 7에서 영구 세대의 나머지 내용 (주로 유형 정보)은 모두 메타 공간으로 이동됩니다. "Java Virtual Machine Specification"은 메소드 영역에 대한 제한이 매우 완화되어 있습니다. Java 힙과 동일 할뿐만 아니라 연속 메모리가 필요하지 않으며 고정 크기 또는 확장 가능을 선택할 수 있습니다. 가비지 수집을 구현하지 않도록 선택할 수도 있습니다. . 상대적으로 말해서, 가비지 콜렉션은 실제로이 영역에서 비교적 드물지만 데이터가 영구 생성의 이름으로 "영구"로 메소드 영역에 입력되는 것은 아닙니다. 이 영역에서 기억 회복의 목표는 주로 상수 풀의 회복과 유형의 언 로딩에 있습니다. 일반적으로이 영역의 회복 효과는 더 어렵고 만족 스럽습니다. 특히 유형의 언 로딩의 경우 조건이 매우 가혹합니다. ,하지만이 지역의 복구는 때때로 정말로 필요합니다. 이전 Sun 회사의 버그 목록에서 나타난 몇 가지 심각한 버그는이 영역을 완전히 회수하지 못한 HotSpot 가상 머신의 낮은 버전으로 인해 메모리 누수가 발생했기 때문입니다. "Java Virtual Machine Specification"은 메소드 영역에 대한 제한이 매우 완화되어 있습니다. Java 힙과 동일 할뿐만 아니라 연속 메모리가 필요하지 않으며 고정 크기 또는 확장 가능을 선택할 수 있습니다. 가비지 수집을 구현하지 않도록 선택할 수도 있습니다. . 상대적으로 말해서, 가비지 콜렉션은 실제로이 영역에서 비교적 드물지만 데이터가 영구 생성의 이름으로 "영구"로 메소드 영역에 입력되는 것은 아닙니다. 이 영역에서 기억 회복의 목표는 주로 상수 풀의 회복과 유형의 언 로딩에 있습니다. 일반적으로이 영역의 회복 효과는 더 어렵고 만족 스럽습니다. 특히 유형의 언 로딩의 경우 조건이 매우 가혹합니다. ,하지만이 지역의 복구는 때때로 정말로 필요합니다. 이전 Sun 회사의 버그 목록에서 나타난 몇 가지 심각한 버그는이 영역을 완전히 회수하지 못한 HotSpot 가상 머신의 낮은 버전으로 인해 메모리 누수가 발생했기 때문입니다. "Java Virtual Machine Specification"은 메소드 영역에 대한 제한이 매우 완화되어 있습니다. Java 힙과 동일 할뿐만 아니라 연속 메모리가 필요하지 않으며 고정 크기 또는 확장 가능을 선택할 수 있습니다. 가비지 수집을 구현하지 않도록 선택할 수도 있습니다. . 상대적으로 말해서, 가비지 콜렉션은 실제로이 영역에서 비교적 드물지만 데이터가 영구 생성의 이름으로 "영구"로 메소드 영역에 입력되는 것은 아닙니다. 이 영역에서 기억 회복의 목표는 주로 상수 풀의 회복과 유형의 언 로딩에 있습니다. 일반적으로이 영역의 회복 효과는 더 어렵고 만족 스럽습니다. 특히 유형의 언 로딩의 경우 조건이 매우 가혹합니다. ,하지만이 지역의 복구는 때때로 정말로 필요합니다. 이전 Sun 회사의 버그 목록에서 나타난 몇 가지 심각한 버그는이 영역을 완전히 회수하지 못한 HotSpot 가상 머신의 낮은 버전으로 인해 메모리 누수가 발생했기 때문입니다.

1.6, 런타임 상수 풀

런타임 상수 풀은 메소드 영역의 일부입니다. 클래스 파일의 클래스 버전, 필드, 메서드 및 인터페이스에 대한 설명 정보 외에도 컴파일 시간 동안 생성 된 다양한 리터럴 및 기호 참조를 저장하는 데 사용되는 상수 풀 테이블 (Constant Pool Table)도 있습니다. 이 부분 콘텐츠는 클래스가로드 된 후 메서드 영역의 런타임 상수 풀에 저장됩니다. Java 가상 머신은 클래스 파일의 각 부분의 형식에 대해 엄격한 규정을 가지고 있습니다 (자연적으로 상수 풀 포함). 예를 들어, 각 바이트가 저장하는 데 사용되는 데이터의 종류는 인식되기 전에 사양의 요구 사항을 충족해야합니다. , 가상 머신에 의해로드되고 실행되지만 런타임 상수 풀의 경우 "Java Virtual Machine 사양"은 세부적인 요구 사항을 만들지 않습니다. 다른 공급자가 구현 한 가상 머신은 자체 요구에 따라이 메모리 영역을 구현할 수 있지만 일반적으로 말하면 , 클래스 파일 저장 외에도 설명 된 기호 참조 외에도 기호 참조에서 변환 된 직접 참조도 런타임 상수 풀 [1]에 저장됩니다. 클래스 파일 상수 풀과 비교하여 런타임 상수 풀의 또 다른 중요한 기능은 동적이라는 것입니다. Java 언어는 컴파일 타임에만 상수를 생성 할 필요가 없습니다. 클래스 파일을 입력 할 수 있습니다. 메서드 영역은 런타임 상수 풀이며 런타임 중에 새로운 상수를 풀에 넣을 수도 있습니다.이 기능은 개발자가 String 클래스의 intern () 메서드에서 더 자주 사용합니다. 런타임 상수 풀은 메서드 영역의 일부이므로 자연스럽게 메서드 영역의 메모리에 의해 제한되며, 상수 풀이 더 이상 메모리를 적용 할 수없는 경우 OutOfMemoryError 예외가 발생합니다.

1.7, 직접 메모리

직접 메모리 (Direct Memory)는 가상 머신 런타임의 데이터 영역의 일부가 아니며 "Java Virtual Machine Specification"에 정의 된 메모리 영역도 아닙니다. 그러나 메모리의이 부분도 자주 사용되며 OutOfMemoryError가 나타날 수도 있으므로 함께 설명하기 위해 여기에 넣습니다. JDK 1.4에서는 NIO (New Input / Output) 클래스가 새로 추가되고 채널 및 버퍼 기반 I / O 방식이 도입되었습니다. 이 메모리에 대한 참조로 Java 힙에 저장된 DirectByteBuffer 객체를 사용합니다. 이는 Java 힙과 원시 힙 사이에서 데이터를 앞뒤로 복사하지 않기 때문에 일부 시나리오에서 성능을 크게 향상시킬 수 있습니다. 분명히 네이티브 직접 메모리 할당은 Java 힙의 크기에 의해 제한되지 않지만 메모리이기 때문에 전체 네이티브 메모리 (물리적 메모리, SWAP 파티션 또는 페이징 파일 포함)의 크기에 의해 확실히 영향을받습니다. 및 프로세서 주소 지정 공간 가상 머신 매개 ​​변수를 구성 할 때 일반 서버 관리자는 실제 메모리에 따라 -Xmx 및 기타 매개 변수 정보를 설정하지만 종종 직접 메모리를 무시하여 각 메모리 영역의 합계가 실제 메모리 제한 (물리적 및 운영 체제 수준 제한)로 인해 동적 확장 중에 OutOfMemoryError 예외가 발생합니다.

 

2. HotSpot 가상 머신 개체 탐색

2.1 개체 생성

Java 가상 머신이 바이트 코드 새 명령어를 발견하면 먼저이 명령어의 매개 변수가 상수 풀에서 클래스의 기호 참조를 찾을 수 있는지 여부를 확인하고 기호 참조가 나타내는 클래스가로드되고 해결되었는지 여부를 확인합니다. 및 초기화되었습니다. 그렇지 않은 경우 먼저 해당 클래스로드 프로세스를 수행해야합니다. 클래스 로딩 검사를 통과 한 후 가상 머신은 새 개체에 대한 메모리를 할당합니다. 객체에 필요한 메모리 크기는 클래스가로드 된 후 완전히 결정될 수 있습니다.

메모리 할당 방법에는 "포인터 충돌"및 "자유 목록"방법이 포함됩니다.

자바 메모리가 절대적으로 규칙적 일 때 "포인터 충돌"을 사용하여 할당 할 수 있습니다. 사용 된 모든 메모리는 한쪽에 배치되고 여유 메모리는 다른쪽에 배치되며 포인터는 경계 지점의 표시기로 가운데에 배치됩니다. ., 할당 된 메모리는 객체의 크기만큼의 거리만큼 포인터를 빈 공간으로 이동하기위한 것으로,이 할당 방법을 "Bump The Pointer"라고합니다. 이 할당 방법은 표시 및 정렬 기반 GC 알고리즘을 사용할 때 사용할 수 있습니다.

Java 힙의 메모리가 규칙적이지 않고 사용 된 메모리와 사용 가능한 메모리가 서로 인터레이스 된 경우 단순히 포인터를 충돌시킬 방법이 없습니다. 가상 머신은 기록 된 메모리 블록 목록을 유지해야합니다. 사용 가능한 경우 , 할당 중에 개체 인스턴스에 할당 할 수있는 충분한 공간을 목록에서 찾고 목록의 레코드를 업데이트합니다.이 할당 방법을 "Free List"라고합니다. 이 할당 방법은 마크 앤 스윕 기반 GC 알고리즘을 사용할 때 사용할 수 있습니다.

동시 할당을 처리 할 때 두 가지 솔루션이 있습니다. 하나는 메모리 공간 할당 작업을 동기화하는 것입니다. 실제로 가상 머신은 실패 재시 도와 함께 CAS를 사용하여 업데이트 작업의 원 자성을 보장하고 다른 하나는 메모리 작업입니다. 할당은 스레드 분할에 따라 다른 공간에서 수행됩니다. 즉, 각 스레드는 스레드가 할당해야하는 로컬 스레드 할당 버퍼 (Thread Local Allocation Buffer, TLAB)라고하는 Java 힙에 작은 메모리 조각을 미리 할당합니다. 메모리, 해당 스레드의 로컬 버퍼에 할당되며, 로컬 버퍼가 다 사용 된 경우에만 새 버퍼 영역이 할당 될 때 동기화 잠금이 필요합니다. 가상 머신이 TLAB를 사용하는지 여부는 -XX : +/- UseTLAB 매개 변수로 설정할 수 있습니다.

2.2 개체 메모리 레이아웃

HotSpot 가상 머신에서 개체의 메모리 레이아웃은 개체 헤더 (헤더), 인스턴스 데이터 (인스턴스 데이터) 및 정렬 패딩 (패딩)의 세 가지로 나눌 수 있습니다.

2.2.1, 개체 헤더

       그림에서 개체 헤더가 Mark Word와 Class Pointer의 두 부분으로 나뉘어져 있음을 알 수 있습니다.

Mark Word는 객체의 hashCode, GC 정보, 잠금 정보를 저장하고 Class Pointer는 클래스 객체의 정보에 대한 포인터를 저장합니다. 32 비트 JVM에서 객체 헤더의 크기는 8 바이트이고 64 비트 JVM은 16 바이트입니다. 두 가지 유형의 Mark Word 및 Class Pointer가 각각 공간의 절반을 차지합니다. 기본적으로 사용되는 64 비트 JVM에는 압축 된 포인터 옵션 -XX : + UseCompressedOops가 있습니다. 개봉 후 클래스 포인터 부분은 4 바이트로 압축되고 개체 헤더 크기는 12 바이트로 줄어 듭니다.

다음은 이해하기 쉬운 32 비트 JVM에서 오브젝트 헤더의 메모리 분포를 보여주는 그림이다.

2.2.2, 인스턴스 데이터 및 정렬 패딩

       사용자 프로그램에 정의 된 가변 메모리 할당은 가상 머신 할당 전략 매개 변수 (-XX : FieldsAllocationStyle 매개 변수) 및 Java 소스 코드에서 필드가 정의 된 순서의 영향을받습니다. HotSpot 가상 머신의 기본 할당 순서는 longs / doubles, int, shorts / chars, bytes / booleans, oops (Ordinary Object Pointers, OOPs)입니다. 위의 기본 할당 전략에서 볼 수 있듯이 동일한 너비의 필드는 항상 이 전제 조건을 충족하는 경우 함께 저장하면 부모 클래스에 정의 된 변수가 자식 클래스 앞에 나타납니다. HotSpot 가상 머신의 + XX : CompactFields 매개 변수 값이 true (기본값은 true)이면 하위 클래스의 더 좁은 변수를 상위 클래스 변수의 간격에 삽입하여 약간의 공간을 절약 할 수도 있습니다. HotSpot 가상 머신의 자동 메모리 관리 시스템에서는 개체의 시작 주소가 8 바이트의 정수배 여야하므로 개체의 크기는 8 바이트의 정수배 여야합니다. 인스턴스 데이터 부분이 정렬되지 않았습니다. 그러면 정렬해야합니다.

2.3. 객체 접근 위치

주류 액세스 방법에는 주로 핸들 및 직접 포인터 사용이 포함됩니다.

1. 핸들 액세스를 사용하면 메모리 조각을 핸들 풀로 Java 힙으로 나눌 수 있습니다. 객체의 핸들 주소는 참조에 저장되고 핸들에는 객체 인스턴스 데이터의 특정 주소 정보가 포함되어 있습니다. 유형 데이터.

2. 직접 포인터 액세스를 사용하는 경우 Java 힙에있는 객체의 메모리 레이아웃은 액세스 유형 데이터에 대한 정보를 배치하는 방법을 고려해야합니다. 객체에 저장된 참조는 객체의 주소입니다. 개체 자체를 다시 수행 할 필요가 없습니다. 간접 액세스의 오버 헤드입니다.

 

3. OutOfMemoryError 예외

3.1, 자바 힙 오버 플로우

/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

테스트 코드는 위와 같으며 Java 힙 메모리의 OutOfMemoryError 예외는 실제 응용 프로그램에서 가장 일반적인 메모리 오버플로 예외입니다. Java 힙 메모리 오버 플로우가 발생하면 예외 스택 정보 "java.lang.OutOfMemoryError"가 추가 프롬프트 "Java 힙 공간"뒤에 나옵니다.

이 메모리 영역의 이상 현상을 해결하기 위해 기존의 처리 방법은 먼저 메모리 이미지 분석 도구 (예 : Eclipse Memory Analyzer)를 통해 Dump에서 힙 덤프 스냅 샷을 분석하는 것입니다. 첫 번째 단계는 OOM을 유발하는 메모리의 개체가 필요한지 확인하는 것입니다. 즉, 메모리 누수 (Memory Leak) 또는 메모리 오버플로 (Memory Overflow)가 있는지 구분합니다.

메모리 누수 인 경우 도구를 통해 누수 된 개체의 참조 체인을 GC Roots로 추가로 확인하고 누수 된 개체가 통과하는 참조 경로와 연결된 GC Roots를 확인하여 가비지 수집기가 누수 된 개체의 유형에 따라이를 회수 할 수 없습니다. 정보 및 GC Roots 참조 체인에 대한 정보는 일반적으로 이러한 개체가 생성 된 위치를보다 정확하게 찾은 다음 메모리 누수를 일으킨 코드의 특정 위치를 찾을 수 있습니다.

메모리 누수가 아닌 경우, 즉 메모리의 모든 오브젝트가 활성 상태 여야합니다. Java 가상 머신의 힙 매개 변수 (-Xmx 및 -Xms) 설정을 확인하고이를 머신의 메모리와 비교하여 상향 조정 공간이 있는지 확인하십시오. 그런 다음 코드에서 프로그램 작동 중 메모리 소비를 최소화하기 위해 너무 긴 수명주기, 너무 긴 유지 상태 시간, 비합리적인 저장 구조 설계 등의 개체가 있는지 확인합니다.

3.2 가상 머신 스택 및 로컬 메서드 스택 오버플로

가상 머신 스택 및 로컬 메소드 스택과 관련하여 두 가지 예외가 "Java 가상 머신 사양"에 설명되어 있습니다.

1) 스레드에서 요청한 스택 깊이가 가상 머신에서 허용하는 최대 깊이보다 크면 StackOverflowError 예외가 발생합니다.

2) 가상 머신의 스택 메모리가 동적 확장을 허용하는 경우 확장 스택 용량이 충분한 메모리를 적용 할 수 없을 때 OutOfMemoryError 예외가 발생합니다.

/**
* 不停的递归导致栈溢出
* VM Args:-Xss128k
* @author zzm
*/
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

/**
* 不停创建线程导致栈溢出
* VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)
* @author zzm
*/
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {
        }
    }
    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

StackOverflowError 예외가 발생하면 분석을위한 명확한 오류 스택이 생성되어 비교적 문제를 쉽게 찾을 수 있습니다. HotSpot 가상 머신의 기본 매개 변수를 사용하는 경우 스택 깊이는 대부분의 경우입니다 (각 방법으로 스택에 푸시되는 프레임 크기가 동일하지 않기 때문에 대부분의 경우에만 말할 수 있음). 일반 메소드 호출 (꼬리 재귀에 최적화 할 수없는 재귀 호출 포함)의 경우이 깊이이면 충분합니다. 그러나 너무 많은 스레드를 설정하여 메모리 오버플로가 발생한 경우 스레드 수를 줄일 수 없거나 64 비트 가상 머신을 교체 할 수없는 경우 더 많은 스레드를 교환 할 수있는 유일한 방법은 최대 힙을 줄이고 스택 용량을 줄이십시오. 메모리 오버플로를 해결하기 위해 메모리를 줄이는이 방법은 일반적으로이 분야에 대한 경험 없이는 생각하기 어렵습니다. 독자는 32 비트 시스템 용 멀티 스레드 응용 프로그램을 개발할 때이 점에주의를 기울여야합니다. 또한이 문제가 상대적으로 숨겨져 있기 때문입니다. 위의 프롬프트 메시지에서 "네이티브 스레드를 생성 할 수 없음"이후에 JDK 7부터 가상 머신은 구체적으로 이유가 "메모리 부족 또는 프로세스 / 리소스 제한에 도달했을 수 있음"을 나타냅니다. ".

 

3.3 메서드 영역 및 런타임 상수 풀 오버플로

/**
* 使用GLIBC不断生成动态类型导致方法区溢出
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class JavaMethodAreaOOM {
	public static void main(String[] args) {
		while (true) {
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {
				public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
					return proxy.invokeSuper(obj, args);
				}
			});
			enhancer.create();
		}
	}
	static class OOMObject {
	}
}

메서드 영역 오버플로도 일반적인 메모리 오버플로 예외입니다. 가비지 수집기에서 클래스를 재활용해야하는 경우 달성해야하는 조건은 상대적으로 가혹합니다. 빈번한 작동 중에 많은 동적 클래스가 생성되는 애플리케이션 시나리오에서는 이러한 클래스의 재활용 상태에 특별한주의를 기울여야합니다. JDK 8 이후, 영구 세대는 역사의 단계에서 완전히 철수되었으며 Metaspace는 그 대체물로 나타납니다.

HotSpot은 여전히 ​​다음을 포함하여 메타 스페이스에 대한 방어 수단으로 일부 매개 변수를 제공합니다.

-XX : MaxMetaspaceSize : 최대 메타 공간 크기를 설정합니다. 기본값은 -1, 즉 제한이 없거나 로컬 메모리 크기에 의해서만 제한됩니다. ·

-XX : MetaspaceSize : 메타 공간의 초기 공간 크기 (바이트)를 지정합니다.이 값에 도달하면 유형 언로드를 위해 가비지 콜렉션이 트리거되고 수집기가 값을 조정합니다. 많은 공간이 해제되면 이 값을 줄이십시오. 공간이 거의 확보되지 않으면 값이 -XX : MaxMetaspaceSize (설정된 경우)를 초과하지 않는 경우 적절하게 증가하십시오.

-XX : MinMetaspaceFreeRatio : 가비지 콜렉션 후 가장 작은 메타 스페이스의 남은 용량 비율을 제어하는 ​​기능으로, 메타 스페이스 부족으로 인한 가비지 콜렉션 빈도를 줄일 수 있습니다. 마찬가지로 가장 큰 메타 공간의 남은 용량 비율을 제어하는 ​​데 사용되는 -XX : Max-MetaspaceFreeRatio가 있습니다.

3.4 네이티브 직접 메모리 오버플로

/**
* 通过Unsafe不断分配堆外内存最后导致本机内存溢出
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author zzm
*/
public class DirectMemoryOOM {
	private static final int _1MB = 1024 * 1024;
	public static void main(String[] args) throws Exception {
		Field unsafeField = Unsafe.class.getDeclaredFields()[0];
		unsafeField.setAccessible(true);
		Unsafe unsafe = (Unsafe) unsafeField.get(null);
		while (true) {
			unsafe.allocateMemory(_1MB);
		}
	}
}

직접 메모리 (Direct Memory)의 크기는 -XX : MaxDirectMemorySize 매개 변수로 지정할 수 있으며, 지정하지 않을 경우 기본값은 최대 Java 힙 (-Xmx로 지정)과 동일합니다. 직접 메모리로 인한 메모리 오버 플로우의 명백한 특징은 힙 덤프 파일에 명백한 이상이 보이지 않는다는 것입니다. 리더가 메모리 오버 플로우 이후 생성 된 덤프 파일이 매우 작다는 것을 발견하면 프로그램에서 직접 또는 간접적으로 사용됩니다. . DirectMemory (일반적인 간접 사용은 NIO 임), 직접 메모리의 이유를 확인하는 데 집중할 수 있습니다.

 

참조 : <자바 가상 머신에 대한 심층적 인 이해 : JVM 고급 기능 및 모범 사례 제 3 판>

 

추천

출처blog.csdn.net/qq_32323239/article/details/108742375