JVM 가비지 수집 메커니즘에 대한 GC 이해

JVM 가비지 컬렉션

가비지 컬렉션이 필요한 이유 : Java는 C 문과 다릅니다. 사용할 때 코드를 통해 공간을 열고 사용 후 수동으로 제거해야 합니다. Java는 우리 프로그램에 가상 머신을 제공하므로 코드를 작성할 때 비즈니스 로직에 더 많은 관심을 기울일 수 있습니다.메모리 공간의 개발은 JVM에 릴리스하십시오. 코드는 JVM에서 실행되며 JVM은 알고리즘을 통해 어느 것이 가비지인지 식별한 다음 가비지 수집 알고리즘을 통해 프로그램에서 사용되지 않은 공간을 자동으로 해제합니다. C언어, C++은 수동변속기, 자바는 자동변속기와 같습니다.
GC: Garbage Collection 垃圾收集, Young Generation의 가비지 컬렉션을 GC, Old Generation의 가비지 컬렉션을 Full GC라고도 합니다.
가비지란
프로그램이 실행 중일 때 데이터를 저장하기 위해 메모리 공간을 열어야 합니다. 그러나 이 공간은 한 번 사용하고 나면 다시는 사용할 수 없으며, 이를 가리키는 주소가 없으면 프로그램은 메모리의 유령처럼 이 주소 공간의 저장 내용을 사용할 수 없습니다. 그러나 이러한 데이터는 실제로 메모리에 존재하며 이는 쓰레기입니다. Java 프로그램을 작성할 때 공간을 열 뿐 공간을 해제하지는 않습니다. 이로 인해 메모리에서 쓸모없는 데이터가 차지하는 메모리 공간이 너무 많아 프로그램의 실제 사용 가능한 메모리가 약간 줄어들고 결국 메모리 오버플로가 발생합니다.
아래에서 예를 들어 단일 연결 목록을 사용하여 노드를 삭제하면 삭제된 노드는 가비지입니다.

public class Application {
    
    
    public static void main(String[] args) {
    
    
        // 创建三个节点
        Node n1 = new Node("张三");
        Node n2 = new Node("李四");
        Node n3 = new Node("王五");

        // 将3个节点连接起来,形成 n1->n2->n3
        n1.next=n2;
        n2.next=n3;

        // 遍历链表
        Node node=n1;
        while (node!=null){
    
    
            System.out.println(node.data);
            node=node.next;
        }

        // 删除一个节点 将中间的节点删除
        n1.next=n3;
        n3.pre=n1;
        n2=null;

        // 遍历链表
        node=n1;
        while (node!=null){
    
    
            System.out.println(node.data);
            node=node.next;
        }
    }
}
@Data
class Node{
    
    
    Node pre;
    Node next;
    Object data;
    public Node(Object data){
    
    
        this.data=data;
    }
}

생각: n2 노드는 쓸모가 없습니까? 그렇다면 여전히 공간을 차지하는 것은 무엇입니까?
원본 연결 목록:
여기에 이미지 설명 삽입

n2 노드 삭제 후: 우리는 이미 n2가 쓰레기라는 것을 알고 있지만 C와 달리 Java는 메모리 해제 공간을 수동으로 운영할 수 없습니다. 그러나 JVM은 자동으로 가비지를 인식하고 우리를 위해 해제합니다.
여기에 이미지 설명 삽입

이때 JVM은 알고리즘(도달 가능성 분석)을 통해 n2가 쓰레기임을 인식하고 최종적으로 n2가 위치한 메모리 공간을 회수합니다.

가비지 컬렉션의 영역 및 범위
여기에 이미지 설명 삽입

속담처럼 스택이 실행되고 힙이 저장되며 힙이 가장 많은 공간을 차지합니다. GC는 메서드 영역과 힙의 가비지 수집을 담당하지만 주로 힙을 담당합니다. 다른 지역은 공간을 거의 차지하지 않으며 쓰레기를 생성하지 않습니다.

힙은 가비지 컬렉션의 주요 장소이므로 먼저 가비지 컬렉션의 일련의 문제를 무시한 다음 힙의 특정 논리적 구조와 힙에서 가비지가 어떻게 전송되는지 소개하겠습니다.

세대별 컬렉션

GC 가비지 콜렉션은 1회 이상 재활용될 가능성이 매우 높으며, 동일한 메모리 영역이 1차 가비지 콜렉션에서는 가비지가 아니지만 2차 콜렉션에서는 가비지일 수 있습니다. GC는 가비지를 효율적으로 스캔하고 효율적으로 가비지를 제거하며 JVM의 정상 작동에 최대한 영향을 미치지 않는 방법으로 힙 공간을 논리적으로 3세대(물리적으로 메모리 내), 즉 신세대, 구세대, 위안 공간(JDK7 이전의 영구 생성). JVM은 영역의 특성에 따라 이 세 영역에 고유한 방식으로 정보를 저장합니다 分代管理. 예를 들어 GC(YoungGC)는 Young Generation에서 수행되고 FullGC는 Old Generation에서 수행됩니다.
세대별 컬렉션에는 두 가지 주요 아이디어가 있습니다.

  • 대부분의 피험자들은 살고 죽는다
  • 더 많은 가비지 수집에서 살아남은 객체는 가비지가 될 가능성이 적습니다.

여기에 이미지 설명 삽입
다음으로 이러한 영역이 실제 가비지 컬렉션에서 어떻게 조정되고 작동하는지 그리고 왜 이렇게 분할되는지 설명하겠습니다
. 점점 많아질수록 새로 생성된 객체가 Eden에 저장될 공간이 없으면 YoungGC가 트리거됩니다. YongGC가 처음으로 트리거됩니다. Eden 영역과 생존자의 from 영역(현재 from 영역은 비어 있음)의 모든 쓰레기를 스캔한 다음 to 영역으로 전송 합니다 不是垃圾. 复制생존자 영역, 그리고 清空Eden区생존자의 from 영역에 있는 모든 데이터가 동시에 경험될 것입니다. GC 스캔 후 쓰레기가 아니라고 판단되는 콘텐츠의 나이는 +1입니다.
1차 GC 이후 힙 공간의 현재 상태: Eden 영역은 비어 있고, 생존자의 to 영역에는 소량의 생존 데이터가 있습니다. 이러한 데이터의 age는 +1이고, to 영역은 to 영역으로 전환되고 to 영역으로 전환되며 이때 to 영역은 비어 있습니다. GC 후 공간이 해제되기 때문에 새로 생성된 객체는 Eden 영역에 배치되고 프로그램이 실행됨에 따라 점점 더 많은 객체가 생성되며 Eden이 다시 가득 차면 두 번째 GC가 트리거됩니다.(참고: 그리고 이번에는 생존자 영역 중 하나가 비어 있습니다. 즉, to 영역) GC는 Eden 영역에서 쓰레기가 아닌 부분을 스캔한 和幸存者from区다음 이 부분을 다른 빈 생존자 영역 to 영역에 복사하고 동시에 시간은 가비지의 데이터 연령을 다시 +1하지 않습니다. GC 후에 공간이 해제되기 때문에 새로 생성된 객체는 Eden 영역에 들어가며 주기가 반복됩니다.
일정 시간(최소 15 GC 이후)까지는 쓰레기로 간주되지 않는 각 데이터의 나이가 +1이 되므로 일부 데이터의 나이가 15세가 될 때까지(JVM 매개 변수를 설정하여 수정할 수 있음) at 이번에는 15년이 된 데이터는 이미 15GCs 후에 시스템은 이러한 데이터가 미래에 쓰레기가 될 확률이 낮을 것이라고 믿고 매번 앞뒤로 이동하는 데 시간과 노력이 필요하므로 데이터는 old generation에 저장됩니다. YoungGC가 트리거될 때 Old Generation은 관여하지 않습니다.
새로운 세대의 과정은 이러합니다. 요약은 다음과 같습니다.复制+1->清空->互换. 빈 서바이버 영역(영역 0으로 가정, 이때 영역 0이 to 영역)에 쓰레기가 아닌 데이터를 복사하고 이 데이터의 나이를 +1한 다음 Eden 영역과 이전에 비어 있지 않았던 서바이버 영역을 지우십시오. 복사(영역 1 가정, 이때 영역 1은 시작 영역임)한 다음 시작 영역을 대상 영역으로 교환합니다. 이때 zone 0은 from zone, zone 1은 to zone이다. 다음 GC에서 다시 스왑합니다.
다음으로 old 영역에서 나이가 15배가 된 데이터로 돌아가서 young generation에서 발생하는 모든 GC는 여기서 영향을 받지 않지만 GC 이후에 새로운 데이터가 추가될 가능성이 있습니다. 빈번한 GC를 통해 새로운 데이터가 지속적으로 추가되며, 시스템은 Old Generation의 공간이 가득 찰지 여부(각 새로운 데이터의 평균값 > Old Generation의 남은 공간)와 예측 시점을 미리 예측합니다. 이전 세대의 공간이 다음 번에 가득 차게 될 수 있습니다 Full GC. 구세대에서 가비지를 스캔하고 지우면 구세대의 메모리 공간이 해제됩니다.

여기에 이미지 설명 삽입
(이 수치는 일반적인 흐름일 뿐, 많은 세부 사항은 무시) 다음으로 JDK7 이전의 메타스페이스를
소개하겠습니다.
더미. 그러나 논리적으로는 힙의 일부이며(힙은 Young Generation, Old Generation 및 Permanent Generation으로 구분됨) 물리적으로 동일한 메모리 조각을 사용합니다. 그것은 구세대에 묶여 있고 누가 가득 차 있든 구세대와 영구 세대에서 쓰레기 제거를 촉발시킬 것입니다. 이렇게 하면 영구세대에 해당하는 코드를 따로 작성할 필요 없이 구세대를 바로 사용할 수 있다. 영구 세대는 주로 , , 类信息普通常量저장합니다. 예를 들어, 우리가 생성한 개체는 어떤 클래스에 따라 생성되며, 이 클래스의 템플릿은 영구 세대에 있고 개체의 헤더 정보에 있는 클래스 바늘은 영구 세대에 있는 인스턴스의 클래스를 가리킬 것입니다. JDK7 이후에는 문자열 상수 풀이 힙으로 이동되었습니다. 그러나 여전히 문제가 있는데, 영구세대에 저장되는 데이터와 힙에 저장되는 데이터가 다르기 때문에 적절한 영구세대의 크기를 결정하기 어려우므로 영구세대를 중간으로 이동시켜 이름을 변경하게 된다 . Metaspace(메타스페이스), Old Generation과 논리적으로 Up되고 물리적으로 분리됨. 이와 같이 메타스페이스는 로컬 메모리를 사용하는데 기본적으로 사용되는 최대 공간은 로컬 메모리의 크기(상한도 설정 가능)이며 클래스 정보는 실제 상황에 따라 자유롭게 로드할 수 있다. 이전 세대를 따르지 않고 자체 가비지 수집 빈도가 있습니다. 메타스페이스는 이전 세대 대신 메서드 영역 사양을 구현한 것입니다. 영구 생성과 메타스페이스의 가장 큰 차이점은 메타스페이스가 직접 메모리를 사용하고 크기를 하드 코딩할 필요가 없다는 것입니다. 참고:静态常量编译器编译后的代码JDK8直接内存


  • 新创建的对象都会放入Eden区,但在扫描垃圾时,会将Eden区和不为空的幸存者区放在一起扫描。
  • 幸存者from区和幸存者to区大小永远一致
  • 모든 물건이 15세 이상이어야 노인 구역에 입장할 수 있는 것은 아닙니다. 예를 들어, 일부 큰 개체 또는 Eden 영역의 생존자 공간 크기와 Survivor From 영역이 Survivor to 영역 크기의 절반을 초과하는 경우 등이 있습니다. 이들은 이전 영역에 직접 배치됩니다.
  • 이전 영역에서 Full GC를 트리거하는 많은 조건이 있습니다.
    • 코드에서 실행 System.gc(), 코드에서 거의 사용되지 않음
    • Old Generation의 공간 부족
    • 공간 할당의 보장 실패 GC 전에 Young Generation에서 Old Generation으로 승격된 공간의 평균 크기를 계산하여 그 크기가 Old Generation의 남은 공간을 초과할 경우 FUll GC를 수행한다.
    • 메타스페이스가 임계값을 초과합니다. 기본적으로 메타스페이스의 크기는 로컬 크기에 상대적입니다. 전체 메타스페이스의 크기가 임계값을 초과하면 Full GC가 수행됩니다.
  • Full GC로 인한 STW 시간이 너무 길기 때문에(STW는 GC의 10배 이상), GC의 튜닝 아이디어는 Full GC 횟수를 줄이거나, Full GC의 STW 시간을 줄이는 것입니다.

생각: 既然从Eden区和幸存者区中扫描出不是垃圾的内容要放入另一个幸存者区,那这两个区的功能是固定好的吗?
복사 후 교환이 있다 비어 있는 것이 to 영역 이고
첫 번째 GC 이후 비어 있는 것이 to 영역이다. 젊은 세대에서 가비지 제거에 복사 알고리즘이 사용되기 때문입니다. 정크가 아닌 부품을 스캔하여 다른 목적지로 전환합니다. 목적지가 누구인지는 고정되어 있지 않고 고정되어 있으면 생존자 1 영역은 항상 시작 영역이고 생존자 0 영역은 항상 목적지 영역이라고 가정하여 첫 번째 GC에서 non-garbage 부분을 복사합니다. to 영역, 두 번째 쓰레기 재활용은 Eden 영역과 to 영역을 스캔해야 하며, 복사할 위치는 to 영역이 아니라 from 영역이어야 합니다. Survivor 영역도 가비지 콜렉션이 필요하기 때문에 to 영역이 있어야 하고 from 영역과 to 영역은 논리적 교환이 가능해야 합니다.
생각: 为什么在新生区清除垃圾时是复制有用的,而不是直接清除无用的?这样不是就不需要幸存者区,会更加节省空间了吗?
통계에 따르면 사용 중인 개체의 98%가 임시 개체로, 에덴 지역의 대부분이 쓰레기라는 의미입니다. 쓰레기를 하나씩 치우는 것보다 쓰레기 더미에서 유용한 것을 골라내는 것이 더 효율적이므로 새로운 세대는 유용한 부분을 복사하여 사용합니다. 그러나 이러한 복사 방법에는 추가 공간이 필요합니다. Old generation의 데이터는 15GC 이후에 쓰레기가 될 확률이 낮으므로 쓸모없는 부분만 지우면 됩니다.
생각: 什么时候会触发GC?(GC通常指YoungGC)
Eden 영역이 가득 차면 GC가 트리거됩니다. 객체 생성 시 Eden 영역에 저장되며 생존자의 from 영역은 새로 생성된 객체 저장에 직접 참여하지 않습니다.

위에서 가비지 콜렉션 프로세스에 대해 간략하게 소개했는데, 미시적인 관점에서 가비지 콜렉터는 젊은 세대인지 구세대인지 어떻게 가비지인지 식별할까요?

쓰레기를 식별하는 방법

어떤 알고리즘이든 프로그램에서 참조 관계가 없는 메모리 주소는 참조 관계가 없으면 프로그램에서 절대 사용되지 않고 재활용되기 때문에 핵심은 프로그램에서 참조 관계가 없는 메모리 주소를 찾는 것입니다.

참조 카운팅

힙 메모리의 데이터 참조 횟수는 +1이고 참조 횟수는 1씩 감소합니다. 힙 메모리의 데이터 조각에 대한 참조 횟수가 0이면 참조할 곳이 없음을 의미합니다. 이 데이터를 참조하면 쓰레기로 판단됩니다.
예: Node 클래스가 있고, node1에서 참조하고, 참조 횟수는 1->node1이고, node2는 모두 참조하고, 참조 횟수는 2->node1에서 역참조하고, 참조 횟수는 1->node2에서 역참조합니다. , 참조 횟수가 0이고 이 주소의 데이터는 쓰레기입니다.

public class Application {
    
    
    public static void main(String[] args) {
    
    
        Node node1 = new Node("张三");
        Node node2=node1;
        System.out.println(node2);
        node1=null;
        System.out.println(node2);
        node2=null;
    }
}

그러나 이 접근 방식에는 다음과 같은 문제가 있습니다.循环引用问题

public class Application {
    
    
    public static void main(String[] args) {
    
    
        Node node1 = new Node("张三");
        Node node2 = new Node("李四");
        node1.setData(node2);
        node2.setData(node1);
        node1=null;
        node2=null;
    }
}

이름이 "Zhang San"인 노드를 예로 들어 보겠습니다. node1 참조, 참조 횟수는 1->node2 데이터 참조, 참조 횟수는 2->node1 참조가 유효하지 않음, 참조 횟수는 1이지만 현재로서는 Node2는 이미 null이므로 이 Node는 더 이상 프로그램에서 사용할 수 없으며 이 Node는 이미 쓰레기이지만 참조 수가 1이지만 0이 아니므로 쓰레기로 인식할 수 없습니다. 이것은 참조 카운팅 방식으로 인해 발생하는 순환 참조 문제입니다.
여기에 이미지 설명 삽입
참조 카운팅의 단점:

  • 각 개체는 성능 손실이 있는 참조 카운터를 유지해야 합니다.
  • 순환 참조 처리

레퍼런스 카운팅의 두드러진 단점으로 인해 현재는 레퍼런스 카운팅 방식을 기본적으로 사용하지 않으나 다음과 같은 도달가능성 분석 방식을 사용한다.

접근성 분석

GC 루트로 사용할 수 있는 개체에서 시작하여 해당 참조 관계를 따르고 해당 참조 체인을 트래버스합니다. 순회하지 않은 모든 GC 루트 개체는 메모리의 가비지이며 이 주소의 데이터는 프로그램에서 더 이상 사용되지 않습니다. 포도송이가 포도송이처럼 뿌리에서 시작하여 포도나무 아래로 내려갑니다. 찾을 수 있는 포도송이는 포도송이에 있어야 합니다. 떨어진 포도만 건너지 않을 것입니다. 이것은 쓰레기입니다.
여전히 위의 기술적인 방법으로는 해결할 수 없는 예입니다.

public class Application {
    
    
    public static void main(String[] args) {
    
    
        Node node1 = new Node("张三");
        Node node2 = new Node("李四");
        Node node3 = new Node("王五");
       	new Node("赵六");
        node1.setData(node2);
        node2.setData(node1);
        node1=node3;
        node2=null;
    }
}

위의 코드는 다음과 같이 표현할 수 있습니다.
여기에 이미지 설명 삽입

도달 가능성 분석 방법을 사용하면 node1, node2 및 node3을 모두 GC 루트로 사용할 수 있으며 세 번째부터 참조 관계를 탐색하기 시작합니다. 결국 메모리에 있는 Zhang San, Li Si, Zhao Liu라는 세 개의 노드는 통과되지 않았으므로 이 세 개는 쓰레기로 판단되어 재활용됩니다.
여기에 이미지 설명 삽입

참조 관계는 GC 루트 개체의 컬렉션에서 순회하므로 어떤 개체를 GC 루트로 사용할 수 있습니까? GC 루트는 네 가지 유형의 개체 모음입니다.

  • 스택의 지역 변수가 참조하는 개체
  • 메서드 영역의 정적 속성이 참조하는 개체
  • 메서드 영역의 상수가 참조하는 개체
  • 네이티브 메서드 스택 JNI에서 참조하는 객체

위의 예에서 GC 루트인 모든 개체는 유형 1입니다.

GC 루트에서 참조 관계가 있는 객체를 不是一定不会가비지로 순회하는 것은 당시의 메모리 상황과 참조 관계에 따라 다릅니다. 그러나 참조 관계가 없는 것은 一定会쓰레기로 취급됩니다.
GC 루트 개체의 참조 관계를 순회하여 어떤 것이 가비지인지 결정되며, 이는 참조된 관계 유형을 포함합니다.

4가지 유형의 참조 관계: 강함, 연성, 약함, 가상

객체 간의 참조 관계는 强引用, 软引用, 弱引用, 의 네 가지 유형으로 나눌 수 있습니다 虚引用. 우리가 일반적으로 사용하는 참조 관계는 모두 강한 참조이며 나머지 세 가지 방법은 특수 기능에서 사용됩니다.
여기에 이미지 설명 삽입


다시 채우다:

  • JVM은 Eden 영역의 공간이 부족하거나 old generation의 공간이 부족할 때 GC를 트리거하며 코드에서 수동으로 GC를 호출할 수도 있습니다. 이 방법을 사용하는 System.gc();것은 일반적이지 않으며 일반적으로 테스트에만 사용됩니다. 그리고: 이 방법을 사용하면 GC가 Full GC가 됩니다.
  • 기본적으로 힙 메모리의 최소 할당은 총 서버 메모리 六十四分之一, 최대 메모리는 총 서버 메모리를 차지 四分之一하며 JVM은 최소 메모리로 시작하며 메모리가 부족할 때까지 조정합니다. 메모리가 충분하지 않으면 OOM 오류가 발생합니다. .
    확인:
    16G의 기본 메모리는
    여기에 이미지 설명 삽입
    다음 코드를 통해서도 얻을 수 있습니다.
public class Application {
    
    
    public static void main(String[] args) {
    
    
        OperatingSystemMXBean mem = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
        // 获取内存总容量
        long totalMemorySize = mem.getTotalPhysicalMemorySize();
    }
}

JVM에서 최소 힙 메모리 및 최대 힙 크기 보기

public class Application {
    
    
    public static void main(String[] args) {
    
    
        System.out.println(""+Runtime.getRuntime().totalMemory()/1024/1024+"M");
        System.out.println(""+Runtime.getRuntime().maxMemory()/1024/1024+"M");
    }
}

여기에 이미지 설명 삽입
默认최대 힙 메모리 크기 = 총 메모리 크기 / 4, 默认최소 힙 메모리 크기 = 총 메모리 크기 / 64를 대략 만족하며,
이러한 기본 크기 매개변수는 Idea에서 조정할 수 있습니다(서버는 jar 패키지를 시작할 때 명령줄로 조정할 수 있음). 일반적으로 메모리 변동을 방지하기 위해 최대 힙 메모리 공간을 최소 힙 메모리 공간으로 조정합니다.
예: -Xms5m: 最小힙 메모리 공간을 5M으로 설정, -Xmx5m은 힙 메모리 最大공간을 5M으로 설정하고 Idea Code 검증 결과
에서 구성합니다 .
여기에 이미지 설명 삽입

여기에 이미지 설명 삽입


강한 참조

우리가 일반적으로 사용하는 참조는 강한 참조이며 다른 참조는 코드로 표시해야 합니다. 예를 들어:

public class Application {
    
    
    public static void main(String[] args) {
    
    
        Node node1 = new Node("张三");
        Node node2 = new Node("李四");
        Node node3=node1;
    }
}

Node1과 node3은 이름이 Zhang San인 노드를 참조하고 node2는 이름이 Li Si인 노드를 참조하며 세 참조는 모두 강한 참조입니다.
강력한 참조의 특징은 GC 루트에 도달할 수 있을 때 강력한 참조가 가비지 수집되지 않는다는 것입니다. 메모리가 부족하면 재활용되지 않고 OOM 예외가 직접 발생합니다.

소프트 참조 SoftReference

메모리가 부족하면 소프트 참조의 힙 공간 주소가 유효하지 않으며 가비지 수집됩니다.
코드 테스트 아이디어: 소프트 참조만 있는 힙 공간이 가비지 수집으로 처리되는지 여부에 따라 다른 환경(충분한 메모리 및 부족한 메모리)에서 동일한 코드를 테스트합니다.

public class Application {
    
    
    public static void main(String[] args) {
    
    
        User u1 = new User("张三"); //强引用方式
        SoftReference<User> softReferenceU1 = new SoftReference<>(u1); // 软引用方式 与强引用都是引用同一块地址

        System.out.println(u1);   // 以强引用的方式获取引用地址的数据(User (name=“张三”))
        System.out.println(softReferenceU1.get()); // 以软引用的方式获取引用地址的数据(User (name=“张三”))

        u1=null; //此时 User(“张三”)失去了强引用 ,只有一个软引用

        System.gc(); // 经历了一次 Full GC

        System.out.println(softReferenceU1.get()); // 测试 User("张三")是否被回收掉
    }
}
@Data
@AllArgsConstructor
class User{
    
    
    private String username;
}

여기에 이미지 설명 삽입

  • 충분한 메모리

결과:
여기에 이미지 설명 삽입
이로부터 다음을 알 수 있습니다.在内存充足时,由于堆内存中User(name=“张三”)仍有一个软引用,使得它没有被当做垃圾回收。

  • 메모리가 부족한 경우 JVM 파라미터를 설정하고 JVM 힙의 최대 및 최소 메모리를 5MB로 설정

메모리 부족으로 인해 오류가 바로 보고되는데, 여기서는 위 코드를 수정하여 메모리가 부족한 문장을 try for output으로 래핑하도록 수정합니다.

public class Application {
    
    
    public static void main(String[] args) {
    
    
        User u1 = new User("张三"); //强引用方式
        SoftReference<User> softReferenceU1 = new SoftReference<>(u1); // 软引用方式 与强引用都是引用同一块地址

        System.out.println(u1);   // 以强引用的方式获取引用地址的数据(User (name=“张三”))
        System.out.println(softReferenceU1.get()); // 以软引用的方式获取引用地址的数据(User (name=“张三”))

        u1=null; //此时 User(“张三”)失去了强引用 ,只有一个软引用


        try {
    
    
            Byte[] load = new Byte[1024 * 1024 * 10];
            // 直接开辟一个10M的内存空间 使得堆内存不足 这是检测是否会只有软引用是否会被当做垃圾回收
        }catch (Exception e){
    
    

        }
        finally {
    
    
            System.out.println(softReferenceU1.get()); // 测试 User("张三")是否被回收掉
        }
    }
}

결과:
여기에 이미지 설명 삽입

약한 참조 WeakReference

GC가 트리거되는 한 GC가 없으면 소프트 참조가 무효화됩니다(소프트하게 참조되는 공간만 쓰레기로 처리됨).

public class Application {
    
    
    public static void main(String[] args) {
    
    
        User u1 = new User("张三"); //强引用方式
        WeakReference<User> weakReferenceU1 = new WeakReference<>(u1); // 弱引用方式 与强引用都是引用同一块地址

        System.out.println(u1);   // 以强引用的方式获取引用地址的数据(User (name=“张三”))
        System.out.println(weakReferenceU1.get()); // 以弱引用的方式获取引用地址的数据(User (name=“张三”))

        u1=null; //此时 User(“张三”)失去了强引用 ,只有一个弱引用

        System.out.println(weakReferenceU1.get());

    }
}
@Data
@AllArgsConstructor
class User{
    
    
    private String username;
}

결과: GC가 없기 전에 참조 중인 개체를 계속 사용할 수 있는 경우
여기에 이미지 설명 삽입

GC에 대해
여기에 이미지 설명 삽입
생각해 보십시오 . 코드에서 u1을 null로 설정해야 하는 이유는 무엇입니까?
u1의 사용은 강한 참조입니다.강한 참조의 특징은 어떤 경우에도 강한 참조가 가리키는 객체를 쓰레기로 취급하지 않는다는 것입니다. 소프트 참조, 약한 참조, 팬텀 참조를 테스트할 때 강한 참조를 연결 해제하지 않으면 결과를 볼 수 없습니다.

WeakHashMap

HashMap을 사용할 때 Key는 강력한 참조 관계입니다. 예: User u1=User(name="Zhang San")를 생성하고 u1 개체에 연결된 값을 추가하고 싶었기 때문에 u1을 HashMap의 키로 전달했습니다. u1 객체가 다 사용되었을 때(첨부된 값도 사라져야 함) 해제를 원할 때 u1=null이지만 이 때 HashMap에는 여전히 User 객체를 가리키는 강력한 참조가 있습니다. 이 개체는 가비지로 해제할 수 없습니다.
여기에 이미지 설명 삽입
이렇게 하면 가비지 수집 중에 개체가 계속 존재하므로 메모리가 낭비되고 OOM 문제가 발생합니다
.WeakHashMap을 사용하는 것은 개체와 약한 참조 관계를 설정하는 것입니다.이 개체를 사용하려면 쓰레기가 아닌 개체. u1이 소진되고 강한 참조가 연결 해제되면 GC 중에 가비지가 수집되고 WeakHashMap의 참조는 무시됩니다.

public class Application {
    
    
    public static void main(String[] args) {
    
    
        WeakHashMap<User, String> weakHashMap = new WeakHashMap<>();
        User u1 = new User("张三");
        User u2 = new User("李四");
        weakHashMap.put(u1,"v1");
        weakHashMap.put(u2,"v2");
        u1=null;  //  User("张三")由于断开强引用,只有一个弱引用的WeakHashMap与之相连,在发生GC时会被回收
        System.out.println(weakHashMap);
        System.gc();
        System.out.println(weakHashMap); // 判断只有WeakHashMap引用下的对象是否被释放
    }
}

결과:
여기에 이미지 설명 삽입
그러나 개체가 값으로 weakHashMap으로 사용될 때 User 개체는 u1의 강한 참조를 깨고 개체는 GC 후에도 여전히 존재하므로 WeakHashMap의 값 부분이 강한 참조일 수 있습니다. 그림과 같이:
여기에 이미지 설명 삽입

여기에 이미지 설명 삽입

생각: 키가 가리키는 객체를 재활용할 때 WeakHashMap의 키를 통해 값을 얻으면 무엇이 반환될까?
여기에 이미지 설명 삽입

생각: WeakHashMap은 키가 약한 참조이기 때문에 재활용될 수 있습니다. 참조 개체에 강력한 참조가 없으면 WeakHashMap의 값은 재활용 후 어디로 갑니까?
여기에 이미지 설명 삽입
생각: WeakHashMap<Object,Object>是否类似于HashMap<Reference<Object>,Object>?

보충: 저는 ThreadLocalMap 소스 코드에서 ThreadLocal 메모리 오버플로 문제를 해결하기 위해 약한 참조를 사용하는 아이디어를 보았습니다
여기에 이미지 설명 삽입
. 출시된.

소프트 참조 및 팬텀 참조의 사용 시나리오

소프트 참조 및 약한 참조는 일반적으로 강한 참조와 함께 사용됩니다. 소프트 참조는 강한 참조가 연결 해제되고 메모리가 허용하는 경우 주소 공간을 계속 사용하려는 경우입니다. 소프트 참조는 강한 참조가 끊어지면 주소 공간을 사용할 필요가 없다는 것을 의미하며 일반적으로 컬렉션과 결합되며 힙 해제 방법이 번거롭고 GC에 넘겨 해제됩니다.

예: 소프트 참조를 사용하여 캐시 생성
이미지를 읽을 때 사용할 때마다 디스크에서 읽으면 성능에 영향을 미치고 한꺼번에 읽으면 메모리 오버플로가 발생할 수 있습니다. 메모리 오버플로를 일으키지 않고 효율성을 향상시키는 방법은 무엇입니까? 이때 메모리 활용도를 최대화할 필요가 있으며, 메모리 공간이 부족하여 OOM이 발생하려고 할 때 메모리에 로드된 사진을 해제해야 합니다. 작동 중 메모리가 충분하면 메모리에 있는 이미지를 계속 사용할 수 있습니다. HashMap<String, Reference<Byte>> hashMap = new HashMap<>()

팬텀 참조 및 참조 큐

팬텀 참조의 사용은 참조 큐와 함께 사용해야 하며 참조 큐 없이 팬텀 참조를 사용하는 것은 의미가 없습니다.

참조 큐

참조된 개체가 소멸되기 전에 가져오기 대기열에 넣습니다.다른 쪽 끝은 이 대기열을 듣고 소멸될 개체에 대해 일부 사후 처리를 수행할 수 있습니다.소프트 참조, 약한 참조 및 팬텀 참조는
모두 구성 중에 참조 대기열을 지정합니다. 팬텀 참조는 구성될 때 지정된 참조 대기열을 사용해야 합니다.
다음 팬텀 참조는 참조 대기열과 함께 사용됩니다.

public class Application {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        User u1 = new User("张三");
        WeakReference<Object> weakReference = new WeakReference<>(u1, referenceQueue);
        u1=null;
        new Thread(()->{
    
    
            while (referenceQueue.poll()==null){
    
    
            }
            System.out.println("对象被销毁");
        }).start();

        System.gc();
        TimeUnit.SECONDS.sleep(1);
    }
}

드물게 사용되는

가상 참조 PhantomReference

팬텀: 유령, 환상, 팬텀 참조는 참조 개체를 얻을 수 없으며 유일한 용도는 참조 큐와 결합하여 참조 개체가 파괴되기 전에 참조 큐에 추가할 수 있으므로 일부 여파가 나중에 수행될 수 있습니다. 소개 대기열의 다른 쪽 끝. 팬텀 참조의 사용은 클래스에 추가적인 영향을 미치지 않으며 거의 ​​사용되지 않습니다.

public class Application {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        User user = new User("张三");
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<User> reference = new PhantomReference<>(user, referenceQueue); // 在创虚引用对象时就要指定消息队列
        System.out.println(reference.get()); //尝试获取虚引用引用的对象
        user=null;
        new Thread(()->{
    
    
            while (referenceQueue.poll()==null){
    
    

            }
            System.out.println("虚引用引用的对象被销毁");
        }).start();
        System.gc();
        TimeUnit.SECONDS.sleep(1);
    }
}

결과:
여기에 이미지 설명 삽입

생각: 다른 참조 큐를 삭제하기 전에 참조 큐에 추가할 수 있습니까?
아주 드물게 사용할 수 있지만, 팬텀 레퍼런스는 이 기능밖에 없어서 특별히 소개합니다.


요약: 개체를 재활용할지 여부는 이 개체에 대한 참조 수와 참조 방법만 확인하면 됩니다.


이상으로 쓰레기 식별 방법을 소개했습니다. 다음 단계는 쓰레기를 발견한 후 처리하는 방법입니다.

가비지 컬렉션 알고리즘

참조 횟수

카운트가 0이면 쓰레기, 레퍼런스 카운트가 있으면 +1, 레퍼런스 무효화 카운트가 있으면 -1이 된다.
이 알고리즘은 가비지를 스캔하는 참조 카운팅 방식과 결합되지만 이 방식은 거의 사용되지 않는 순환 참조의 문제가 있으므로 참조 카운트 알고리즘 방식은 사용하지 않습니다.

복사 복사

영역 A의 정크가 아닌 부분을 영역 B에 복사한 다음 영역 A를 지웁니다. 이 방법은 힙의 Young Generation에 적합합니다. Young Generation은 쓰레기의 소수가 아니고 이동량이 상대적으로 적기 때문입니다. 에덴 영역과 생존자 영역 중 쓰레기가 아닌 부분을 생존자 to 영역으로 복사한 후 에덴 영역과 생존자 영역을 지웁니다.
이 방법이 더 효율적이고 to 영역이 모두 비어있는 곳부터 복사가 시작되며 clearing은 모든 Eden 영역과 생존자의 from 영역을 모두 지우는 것이므로 메모리 조각화가 없을 것입니다.
이 접근법의 단점

  • 생존자의 to 영역과 같은 크기여야 하는 추가 공간이 필요하고 생존자 영역의 메모리 절반은 언제든지 낭비됩니다.
  • 에덴 지역의 100%가 쓰레기가 아닌 극한 상황이 발생하면 유용한 부분을 복사하는 데 시간이 걸리고 생존자를 지역으로 폭발시킵니다. 그래서 생존율이 낮은 곳에 적합합니다.

마크 마크 스윕

표시 제거: 두 부분으로 나뉩니다. 1. 모든 GC 루트를 순회하여 어느 부분이 가비지인지 표시합니다. 2. 전체 힙을 순회하여 쓰레기를 지웁니다. 휴지통으로 표시하고 휴지통을 비울 때 전체 애플리케이션을 일시 중지하려면 STW(stop the world)가 필요합니다. 구세대에 있는 대부분의 객체가 15개의 GC를 경험했고 다시 쓰레기가 될 확률이 상대적으로 적기 때문에 구세대에서 발생합니다.

여기에 이미지 설명 삽입
두 가지 주요 단점이 있습니다.

  • 비효율적이며 전체 애플리케이션을 일시 중지해야 함
  • 지우면 메모리가 불연속화되어 과도한 메모리 조각화가 발생합니다. 그런 다음 JVM은 사용 가능한 메모리 목록을 유지해야 하며 이는 또 다른 오버헤드입니다. 또한 배열 객체를 할당할 때(큰 객체는 이전 영역에 있음) 연속적인 공간이 많이 필요하고 연속적인 메모리 공간을 찾기가 쉽지 않습니다.

마크-컴팩트

마무리 표시: 세 부분으로 나뉩니다. 1. 모든 GC 루트를 탐색하여 어느 부분이 쓰레기인지 표시합니다. 2. 전체 힙을 탐색하여 쓰레기를 제거합니다. 3. 조각화를 줄이기 위해 메모리를 구성합니다. 마킹 알고리즘과 비교할 때 메모리 조각화 문제를 해결하는 정렬 단계가 하나 더 있지만 새로운 문제도 발생합니다. 정렬하는 데 시간과 CPU가 필요합니다.
마크 클리어와 마크 클리어는 모두 구세대에서 발생하며, 둘은 종종 사용 중에 결합됩니다.여러 마크 클리어 후 한 번의 클리어가 수행됩니다. 이렇게 하면 시간과 성능이 많이 소모되는 잦은 표시 및 정렬을 피할 수 있을 뿐만 아니라 메모리 조각화를 합리적인 범위 내에서 유지할 수 있습니다.


이 세 가지 알고리즘은 각각 장점과 단점이 있습니다. 완벽한 알고리즘은 없으며 적절한 양쯔강에 따라 적절한 알고리즘을 선택해야 합니다.


위의 알고리즘은 가비지 수집을 해결하기 위한 아이디어이며 구체적인 구현 도구는 다음 가비지 수집기입니다.

가비지 컬렉터의 유형

실제로 알고리즘에 따라 구현된 도구와 가비지 수집기가 일반적으로 재활용되는 방식을 다섯 가지 범주로 나눕니다.

직렬 수집가

하나의 스레드만 수집 중이며 가비지가 수집되면 모든 사용자 스레드가 일시 중지됩니다. 단일 스레드 시나리오에 적합합니다. 예를 들어, 수업 시간에 한 명의 청소부 아줌마만 청소하러 들어옵니다. 수업을 계속하려면 청소 아줌마가 청소를 마칠 때까지만 기다리면 됩니다. 모든 사용자 스레드는 이 스레드가 GC를 완료할 때까지 일시 중지하고 기다려야 하므로 효율성이 떨어집니다.
구체적인 구현은 Serial(젊은 세대의 경우), Serial Old(구세대의 경우) 입니다.

병렬 수집기

직렬 수집기와 비교할 때 재활용할 때 더 이상 하나의 스레드만 있는 것이 아니라 여러 스레드가 함께 참여합니다. 이렇게 하면 더 이상 한 사람이 작업하기를 기다리지 않고 작업량도 변하지 않습니다.한 사람이 함께 작업하는 것과 비교하여 여러 사람이 사용자 스레드의 대기 시간을 줄일 수 있습니다.
구체적인 구현은 ParNew(젊은 세대를 위한), Parallel Scavenge(젊은 세대를 위한), Parallel Old(구세대를 위한) 입니다.

동시 수집기

사용자 스레드와 복구 스레드는 함께 작동할 수 있으며(비록 일시 중지가 있지만 시간이 짧음) 가장 큰 특징은 긴 일시 중지가 없으며 사용자가 상호 작용에 대한 강력한 요청이 있는 시나리오에 적합합니다. . 사용자는 확실히 상호 작용할 때 갑자기 긴 일시 중지를 원하지 않기 때문에 동시 가비지 수집기를 사용하면 응답 시간을 줄일 수 있습니다.
구체적인 구현은 CMS(구세대용)

G1

ZGC

생각: 什么是STW?
STW: Stop The Word는 가비지 수집 중에 모든 사용자 스레드가 일시 중단되어 동결 현상을 유발한다는 의미입니다.
생각: 复制算法也会到导致STW吗?为什么要GC时要STW?
모든 가비지 수집기는 STW를 유발합니다. 시간 문제일 뿐입니다. 쓰레기가 최종 확인되면 일관성이 보장되어야 합니다. 그렇지 않으면 프로그램이 계속 실행되고 참조 관계가 계속 변경되며 분석 결과가 부정확해집니다. 복사하는 동안 일시 중지가 없으면 이 기간 동안 생성된 개체는 생존 개체로 표시되지 않으며 생존자는 생존자 영역으로 이동되지 않고 지워지고 강한 참조가 지워지고 프로그램이 잘못됩니다. . 또한 복사 알고리즘과 마킹 알고리즘에서 원본 참조 개체의 주소가 변경됩니다. 인용 혼동을 방지합니다.

일반적인 가비지 컬렉터의 예

새로운 세대

연속물

스레드 만이 一个Young Generation에서 복사 알고리즘을 사용하여 에덴동산과 생존자 From 영역에서 생존자 To 영역에 모든 생존 개체를 복사합니다. STW는 GC 중에 필요합니다. Serial Old 가비지 수집기로 작업할 수 있습니다.

병렬 청소

스레드를 시작 多个하고 Young Generation에서 복사 알고리즘을 사용하여 에덴동산의 모든 생존 개체와 생존자 From 영역을 Survivor To 영역으로 복사합니다. STW는 GC 중에 필요하며 여러 스레드를 함께 사용하기 때문에 STW 시간이 더 짧을 수 있습니다.
처리량은 XX:MaxGCPauseMillis 매개변수와 처리량을 직접 제어하는 ​​-XX:GCTimeRatio 매개변수로 설정할 수 있습니다. 처리량 = 프로그램 실행 시간 / (프로그램 실행 시간 + GC 시간). 처리량을 개선하면 CPU 사용률을 높일 수 있지만 응답 속도와 관련이 없으며 반드시 사용자 경험을 개선하는 것은 아닙니다.

와 함께 작업 할 수 있습니다 CMS垃圾回收器.Parallel Old垃圾回收器

파뉴

또한 Parallel(처리량을 설정할 수 없음)과 약간 다른 여러 스레드가 있는 GC입니다. CMS垃圾回收器함께 일할 수 있습니다

구세대

시리얼 올드

가비지는 단일 스레드 방식으로 수집되며 모든 사용자 스레드는 가비지 수집 중에 일시 중단됩니다. 위의 Youth Generation의 Serial과 비슷하지만 Old Generation에서는 Mark-and-Organize 알고리즘을 사용하므로 메모리 조각화가 발생하지 않습니다.

병렬 이전

동시 스레드 복구는 Young Generation의 Parallel과 유사하지만 Old Generation에서 채택한 마킹 알고리즘은 메모리 조각화를 일으키지 않습니다.

CMS

CMS의 전체 이름: concurrent mark sweep, 번역은 동시 청산입니다.

CMS 4단계
  • 初始标记: old 영역에 있는 모든 GC 루트, 즉 루트 노드를 스캔합니다. 이 기간 동안 루트노드의 변화가 정지되어 STW가 되지만 상대적으로 GC 루트가 적기 때문에 STW 시간이 상대적으로 짧다.
  • 동시 마킹: 초기 단계에서 모든 루트 노드가 스캔되었으므로 이 단계에서는 루트 노드를 따라 참조할 수 없는 객체를 스캔하고 쓰레기로 표시하는 것만 필요합니다. 이 기간 동안 사용자 스레드와 병렬로 실행됩니다. 일반적으로 루트 노드보다 루트 노드 아래에 노드가 더 많기 때문에 상대적으로 시간이 많이 걸리지만 사용자 스레드와 병렬로 실행되기 때문에 STW가 없습니다. 단점은 이 노드가 더 많은 성능을 소비한다는 것입니다.
  • 重新标记: 사용자 스레드가 실행 중에 가비지를 스캔하여 가비지가 추가될 수 있습니다. 이는 최종 대청소를 위해 모든 사용자 스레드를 일시 중단하기 위한 것입니다. 이 과정에서 새로 추가된 가비지를 다시 표시하여 청소가 철저하고 동시성으로 인해 생성된 가비지를 알 수 없도록 합니다. 이 단계는 STW이지만 추가되는 쓰레기가 적기 때문에 시간이 짧습니다.
  • 동시 지우기: 여기에 모든 가비지가 표시되고 나머지는 지워집니다(지우기 알고리즘은 표시 지우기 사용). 정리된 스레드는 사용자 스레드와 함께 실행할 수 있습니다.

CMS의 가장 큰 특징은 STW가 짧고 인터랙션 도중 시스템이 잠시 멈추는 현상으로 사용자와 인터랙션할 때 사용하기에 적합하다.
그러나 CMS에는 두 가지 주요 단점이 있습니다.

  • 동시 삭제 과정에서 사용자 스레드가 일시 중단되지 않았기 때문에 이전 세대에 새로운 데이터가 추가될 가능성이 있으므로(예: 이전 세대에 직접 배치된 큰 개체) 꽉 차도 지워지지 않습니다. 그러나 일정량의 공간이 예약되어 있습니다. 예를 들어 10%, 단, 동시 청산 과정에서 구세대에 새로 들어오는 데이터가 유보된 값의 10%를 초과하면 이때 구세대를 위한 공간이 없게 되어 트리거되고, CMS는 Concurrent Mode FailureSerial Old 가비지 수집기로 변질됩니다. 모든 사용자 스레드를 일시 중단하고 단일 스레드 방식으로 재활용합니다. 심각한 지연을 유발합니다. 예약된 공간이 크면 Full GC가 자주 발생하고 예약된 공간이 작으면 CMS 재활용 실패가 발생합니다.
  • CMS에서 채택한 알고리즘은 동시 삭제 알고리즘이므로 메모리 조각화가 발생합니다. 프래그먼트가 너무 많으면 Serial GC가 트리거되고 단일 스레드 마크 구성 알고리즘을 사용하여 지우고 조각 모음합니다.

생각: 왜 CMS는 조각화 문제를 해결하기 위해 마크업 알고리즘을 사용하지 않습니까?
데이터 정렬 알고리즘을 사용하는 경우 개체의 주소는 동시 정리 중에 변경되어 모든 사용자 스레드를 일시 중단해야 합니다.

젊은 세대 + 구세대

G1

이 블로그를 읽는 것이 좋습니다

GC 가비지 컬렉터의 구성 및 사용과 JVM의 일부 매개변수 및 명령에 대해서는 Linux와 결합된 JVM 튜닝에 대해 다음 블로그에서 소개할 예정입니다. 오류를 발견하면 함께 논의하는 것을 환영합니다.

추천

출처blog.csdn.net/m0_52889702/article/details/128900768