1. Java 메모리 모델
Java Virtual Machine은 프로그램을 실행할 때 관리하는 메모리를 여러 데이터 영역으로 나누어서 이러한 데이터 영역의 분포를 보여줍니다.
프로그램 카운터: 현재 실행 중인 바이트코드를 가리키는 작은 메모리 영역입니다. 스레드가 Java 메소드를 실행하는 경우 이 카운터는 실행 중인 가상 머신 바이트코드 명령의 주소를 기록합니다. 기본 메소드가 실행되는 경우 이 카운터 값은 비어 있습니다.
Java 가상 머신 스택: 스레드 프라이빗, 해당 수명 주기가 스레드와 일치합니다. 각 메소드가 실행될 때 로컬 변수 테이블, 피연산자 스택, 동적 링크와 같은 정보를 저장하는 스택 프레임이 생성됩니다. 메소드 종료 등.
로컬 메서드 스택: 이 기능은 가상 머신 스택과 유사합니다. 단, 가상 머신 스택은 Java 메서드를 실행하기 위해 가상 머신을 제공하는 반면, 로컬 메서드 스택은 사용된 기본 메서드를 제공합니다.
Java 힙: 가상 머신이 관리하는 가장 큰 메모리 조각으로 모든 스레드에서 공유됩니다. 이 영역은 객체 인스턴스를 저장하는 데 사용됩니다. Java 힙은 메모리 재활용의 관점에서 볼 때 대부분의 현재 수집기가 세대별 수집 알고리즘을 사용하기 때문에 Java 힙을 더 세분화하면 다음과 같이 나눌 수 있습니다. Eden 공간, From Survivor 공간, To Survivor 공간 등으로 나눌 수 있습니다. Java Virtual Machine 사양에 따르면 Java 힙은 논리적으로 연속적인 한 물리적으로 불연속적인 공간에 있을 수 있습니다.
메서드 영역 : Java와 마찬가지로 각 스레드에서 공유하며 가상 머신에서 로드한 클래스 정보, 상수 조명, 정적 변수, Just-In-Time에서 컴파일된 코드 등의 데이터를 저장하는 데 사용됩니다. 컴파일러.
런타임 상수 풀. 런타임 상수 풀은 클래스 버전, 필드, 메소드, 인터페이스 등과 같은 설명 정보 외에도 상수 풀을 포함합니다. , 컴파일 중에 생성된 다양한 리터럴 및 기호 참조를 저장하는 데 사용됩니다. 작업 중에 새로운 상수를 상수 풀에 넣을 수 있습니다. 가장 일반적으로 사용되는 메서드는 String 클래스의 intern() 메서드입니다. String 인스턴스가 intern을 호출하면 Java는 상수에 동일한 유니코드 문자열 상수가 있는지 검색합니다. 있는 경우 해당 참조를 반환하고, 그렇지 않은 경우 상수 풀의 인스턴스 문자열과 동일한 유니코드 문자열을 추가하고 해당 참조를 반환합니다.
2. 가비지 개체를 확인하는 방법
Java 힙에는 여러 개체 인스턴스가 저장되어 있으므로 가비지 수집기가 힙을 재활용하기 전에 먼저 어떤 개체가 아직 "활성"인지 확인해야 합니다. . "죽은" 개체, 즉 어떤 방식으로도 사용되지 않는 개체입니다.
참조 카운팅 방법
참조 카운팅 방법은 구현이 간단하고 효율성이 높으며 대부분의 경우 좋은 알고리즘입니다. 원칙은 객체에 참조 카운터를 추가하는 것입니다. 객체에 대한 참조가 있을 때마다 카운터는 1씩 증가합니다. 참조가 만료되면 카운터는 1씩 감소합니다. 카운터 값이 0이면 객체가 있음을 의미합니다. 더 이상 사용되지 않습니다. 참조 카운팅 방법은 객체 간 순환 참조 문제를 해결하기 어렵다는 점에 유의해야 합니다. 주류 Java 가상 머신은 메모리 관리에 참조 카운팅 방법을 사용하지 않습니다.
접근성 분석 알고리즘
이 알고리즘의 기본 아이디어는 "GC Roots"라는 일련의 개체를 시작점으로 사용하고, 이 노드에서 아래쪽으로 검색하여 경로를 검색하는 것입니다. 전달된 경로를 참조 체인이라고 합니다. 객체를 GC 루트에 연결하는 참조 체인이 없는 경우(그래프 이론에서는 해당 객체가 GC 루트에서 도달할 수 없음을 의미함) 그림과 같이 object 5, object 6, object 7이 서로 연관되어 있지만 GC Roots에서는 도달할 수 없으므로 재활용 가능한 개체로 판단하게 됩니다.
Java 언어에서 GC Roots로 사용할 수 있는 개체는 다음과 같습니다.
가상 머신 스택( 스택 프레임) 참조된 객체입니다.
메서드 영역의 클래스 정적 속성이 참조하는 객체입니다.
메서드 영역의 상수가 참조하는 개체입니다.
로컬 메서드 스택에서 JNI(일반적으로 네이티브 메서드)가 참조하는 개체입니다.
이제 질문이 생깁니다. 도달성 분석 알고리즘이 객체 간 순환 참조 문제를 일으킬까요? 대답은 '예'입니다. 즉, 개체 간에 순환 참조 문제가 발생하지 않습니다. GC 루트는 개체 그래프 외부에 특별히 정의된 "시작점"이며 개체 그래프의 개체에서 참조할 수 없습니다.
죽느냐 안 죽느냐
도달 가능성 분석 알고리즘에서는 도달할 수 없는 객체라도 "죽을 필요"가 없습니다. 이때는 일시적으로 "검증" 단계에 있는 것이므로 객체가 실제로 죽었다고 선언하려면 최소한 두 번의 표시 과정을 거쳐야 합니다. : 도달성 분석 후 해당 객체에 GC Roots에 연결된 참조 체인이 없는 것으로 확인되면 처음으로 표시하고 한 번 필터링합니다. 필터링 조건은 해당 객체가 finapze() 메서드를 실행해야 하는지 여부입니다. . 개체가 finapze() 메서드를 포함하지 않거나 가상 머신에서 finapze() 메서드를 호출한 경우 가상 머신은 두 상황을 모두 "실행할 필요 없음"으로 처리합니다. 프로그램은 finapze()를 재정의하여 "스릴 넘치는" 자체 구조 프로세스를 수행할 수 있지만 기회는 한 번뿐입니다.
/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finapze()方法最多只会被系统自动调用一次 * @author zzm */ pubpc class FinapzeEscapeGC { pubpc static FinapzeEscapeGC SAVE_HOOK = null; pubpc void isApve() { System.out.println("yes, i am still apve :)"); } @Override protected void finapze() throws Throwable { super.finapze(); System.out.println("finapze mehtod executed!"); FinapzeEscapeGC.SAVE_HOOK = this; } pubpc static void main(String[] args) throws Throwable { SAVE_HOOK = new FinapzeEscapeGC(); //对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); //因为finapze方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isApve(); } else { System.out.println("no, i am dead :("); } //下面这段代码与上面的完全相同,但是这次自救却失败了 SAVE_HOOK = null; System.gc(); //因为finapze方法优先级很低,所以暂停0.5秒以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isApve(); } else { System.out.println("no, i am dead :("); } } }
실행 결과는 다음과 같습니다.
finapze mehtod executed! yes, i am still apve :) no, i am dead :(
그럼 참조에 대해 이야기해 보겠습니다
참조 카운팅 알고리즘을 통해 객체에 대한 참조 수를 결정하는 것인지 아니면 도달성 분석 알고리즘을 통해 객체에 대한 참조를 결정합니다. 체인에 도달할 수 있는지 여부, 객체가 살아 있는지 여부는 모두 "참조"와 관련됩니다. JDK 1.2 이전에는 Java에서 참조의 정의가 매우 전통적이었습니다. 참조 유형 데이터에 저장된 값이 다른 메모리의 시작 주소를 나타내는 경우 이 메모리는 참조를 나타낸다고 합니다. JDK 1.2 이후 Java는 참조의 개념을 확장하여 참조를 Strong Reference, Soft Reference, Weak Reference 및 Phantom Reference의 네 가지 유형으로 나누었습니다. 이 네 가지 인용 강도는 점차 약해집니다.
• 강력한 참조는 "Object obj = new Object()"와 같이 프로그램 코드에 널리 퍼져 있는 참조를 의미합니다. 강력한 참조가 여전히 존재하는 한 가비지 수집기는 참조된 항목을 회수하지 않습니다. 물체.
• 소프트 참조는 유용하지만 필요하지 않은 일부 개체를 설명하는 데 사용됩니다. 소프트 참조와 연결된 개체의 경우 이러한 개체는 시스템에서 메모리 오버플로 예외가 발생하기 전에 두 번째 재활용을 위한 재활용 범위에 포함됩니다. 이 재활용을 위한 메모리가 충분하지 않으면 메모리 오버플로 예외가 발생합니다. JDK 1.2 이후에는 소프트 참조를 구현하기 위해 SoftReference 클래스가 제공됩니다.
• 약한 참조는 필수적이지 않은 개체를 설명하는 데에도 사용되지만 약한 참조와 관련된 개체는 다음 가비지 수집이 발생할 때까지만 생존할 수 있습니다. 가비지 수집기가 작동하면 현재 메모리가 충분한지 여부에 관계없이 약한 참조와 관련된 개체만 재활용됩니다. JDK 1.2 이후에는 약한 참조를 구현하기 위해 WeakReference 클래스가 제공됩니다.
• 유령 참조는 유령 참조 또는 팬텀 참조라고도 합니다. 가장 약한 종류의 참조 관계입니다. 객체에 가상 참조가 있는지 여부는 수명에 영향을 주지 않으며 가상 참조를 통해 객체 인스턴스를 얻는 것은 불가능합니다. 객체에 대한 가상 참조 연관을 설정하는 유일한 목적은 컬렉터가 객체를 회수할 때 시스템 알림을 받는 것입니다. JDK 1.2 이후에는 가상 참조를 구현하기 위해 PhantomReference 클래스가 제공됩니다.
소프트 참조 사용 예:
package jvm; import java.lang.ref.SoftReference; class Node { pubpc String msg = ""; } pubpc class Hello { pubpc static void main(String[] args) { Node node1 = new Node(); // 强引用 node1.msg = "node1"; SoftReference<Node> node2 = new SoftReference<Node>(node1); // 软引用 node2.get().msg = "node2"; System.out.println(node1.msg); System.out.println(node2.get().msg); } }
출력 결과는 다음과 같습니다.
node2 node2
3. 일반적인 가비지 수집 알고리즘
1. Mark-Sweep(mark-clear) 알고리즘
가장 기본적인 가비지 컬렉션 알고리즘인 이유는 구현하기가 가장 쉽고 아이디어도 가장 간단하기 때문입니다. 마크 스윕 알고리즘은 마크 단계와 클리어 단계의 두 단계로 나뉩니다. 마킹 단계의 임무는 재활용이 필요한 모든 객체를 표시하는 것이고, 클리어 단계는 표시된 객체가 차지한 공간을 복구하는 것입니다. 구체적인 과정은 아래 그림과 같습니다.
그림을 보면 Mark-Clear 알고리즘이 상대적으로 구현하기는 쉽지만 심각한 문제가 있다는 것을 알 수 있습니다. 메모리 조각이 너무 많이 생성되면 후속 프로세스에서 큰 개체에 대한 공간을 할당해야 할 때 공간이 부족하여 사전에 새로운 가비지 수집 작업이 트리거될 수 있습니다.
2. 복사 알고리즘
Mark-Sweep 알고리즘의 단점을 해결하기 위해 복사 알고리즘이 제안되었습니다. 사용 가능한 메모리를 용량에 따라 두 개의 동일한 크기 블록으로 나누고 한 번에 그 중 하나만 사용합니다. 이 메모리 블록이 부족해지면 살아남은 개체를 다른 블록에 복사한 다음 사용한 메모리 공간을 한꺼번에 정리하여 메모리 조각화 문제가 발생할 가능성을 줄입니다. 구체적인 프로세스는 아래 그림에 나와 있습니다.
이 알고리즘은 구현이 간단하고 작동이 효율적이며 메모리 조각화가 발생하지 않지만 메모리 공간을 많이 사용합니다. 비용은 사용할 수 있는 메모리가 원래의 절반으로 줄어드는 것입니다.
분명히 복사 알고리즘의 효율성은 살아남은 개체의 수와 밀접한 관련이 있습니다. 살아남은 개체가 많으면 복사 알고리즘의 효율성이 크게 떨어지게 됩니다.
3. 마크-컴팩트(mark-compact) 알고리즘
Copying 알고리즘의 단점을 해결하고 메모리 공간을 최대한 활용하기 위해 Mark-Compact 알고리즘이 제안되었습니다. 이 알고리즘의 마킹 단계는 Mark-Sweep과 동일하지만 마킹 완료 후 재활용 가능한 객체를 직접 정리하지 않고, 살아남은 객체를 한쪽 끝으로 이동한 다음 끝 경계 외부의 메모리를 정리합니다. 구체적인 프로세스는 아래 그림과 같습니다.
4. 세대별 수집(세대별 수집) 알고리즘
세대별 수집 알고리즘은 대부분의 가비지 수집입니다. 현재 프로세서에서 사용하는 알고리즘입니다. 핵심 아이디어는 객체의 수명 주기에 따라 메모리를 여러 영역으로 나누는 것입니다. 일반적인 상황에서 힙 영역은 Tenured Generation과 Young Generation으로 구분됩니다. Old Generation의 특징은 각 가비지 수집 중에 소수의 객체만 재활용하면 된다는 점이며 Young Generation의 특징은 다음과 같습니다. 모든 가비지 컬렉션은 재활용해야 할 개체가 많기 때문에 다양한 세대의 특성에 따라 가장 적합한 컬렉션 알고리즘을 채택할 수 있습니다.
현재 대부분의 가비지 컬렉터는 신세대에 복사 알고리즘을 채택하고 있습니다. 왜냐하면 대부분의 객체는 신세대의 각 가비지 컬렉터에서 재활용되어야 하기 때문입니다. 이는 복사 작업 횟수가 적다는 것을 의미하지만 실제로는 신세대의 공간은 1:1의 비율로 나누어지지 않습니다. 일반적으로 신세대는 더 큰 Eden 공간과 두 개의 작은 Survivor 공간으로 나누어집니다(보통 8:1:1). 재활용 시 Eden과 Survivor 공간에 남아 있는 객체를 다른 Survivor 공간에 복사한 후 방금 사용한 Eden과 Survivor 공간을 정리합니다.
구세대의 특징은 매번 적은 수의 객체만 재활용된다는 점이므로 Mark-Compact 알고리즘이 일반적으로 사용됩니다.
위의 Java 메모리 모델 및 가비지 컬렉션에 대한 간략한 분석은 모두 편집자가 공유한 내용이므로 참고가 되기를 바라며 PHP 중국어 웹사이트에 많은 지원 부탁드립니다.
Java 메모리 모델 및 가비지 수집에 대한 더 많은 기사를 보려면 PHP 중국어 웹사이트를 주목하세요!