[jvm series-06] 객체 인스턴스화, 메모리 레이아웃 및 액세스 포지셔닝에 대한 심층적 이해

JVM 시리즈 전체 칼럼


콘텐츠 링크 주소
[1] 가상 머신과 자바 가상 머신 알아보기 https://blog.csdn.net/zhenghuishengq/article/details/129544460
[2] jvm의 클래스 로딩 서브시스템과 jclasslib의 기본 사용법 https://blog.csdn.net/zhenghuishengq/article/details/129610963
[3] 런타임 시 프라이빗 영역의 가상 머신 스택, 프로그램 카운터 및 로컬 메서드 스택 https://blog.csdn.net/zhenghuishengq/article/details/129684076
[4] 런타임 시 데이터 영역의 공유 영역에 대한 힙 및 이스케이프 분석 https://blog.csdn.net/zhenghuishengq/article/details/129796509
[5] 런타임 데이터 영역 공유 영역의 메서드 영역 및 상수 풀 https://blog.csdn.net/zhenghuishengq/article/details/129958466
[6] 개체 인스턴스화, 메모리 레이아웃 및 액세스 위치 지정 https://blog.csdn.net/zhenghuishengq/article/details/130057210

1. 개체 인스턴스화, 메모리 레이아웃 및 액세스 위치 지정

1. 개체 인스턴스화

주로 객체를 생성하는 다음과 같은 방법과 객체를 생성하는 단계가 있습니다.
여기에 이미지 설명 삽입

1.1, 객체를 생성하는 여러 가지 방법

일상적인 개발에는 주로 다음과 같은 방법으로 객체를 생성합니다.

  • 가장 일반적인 방법: new plus 생성자, 생성자가 비공개인 경우 싱글톤 모드와 같이 정적으로 액세스하거나 팩토리를 통해 로드할 수 있습니다.
//new 构造器 创建对象
Object object = new Object();
//构造器静态私有,如典型的单例模式
Object object = Object.getObject()//工厂加载,SpringBean,SqlSessionBean
Object object = ObjectFactory.getObject();
  • 리플렉션 방법: 클래스의 newInstance 또는 생성자의 newInstance
public class Invoke {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            Class<?> clazz1 = Class.forName("com.tky.jvm.Invoke");
            //通过类构造器获取对象
            Constructor<?> constructor = clazz1.getConstructor();
            Invoke invoke1 = (Invoke)constructor.newInstance();
            //通过类名获取
            Class<Invoke> clazz2 = Invoke.class;
            Invoke invoke2 = clazz2.newInstance();
            //通过对象获取
            Invoke in = new Invoke();
            Class<? extends Invoke> clazz3 = in.getClass();
            Invoke invoke3 = clazz3.newInstance();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}
  • 복제 방법: 복제 방법, 생성자를 호출하지 않고 현재 클래스는 Cloneable 인터페이스와 복제 방법을 구현해야 합니다.
/**
 * @author zhenghuisheng
 * @date : 2023/4/10
 */
@Data
public class Clone implements Cloneable {
    
    
    private Long id;
    private String username;
    private String password;

    @Override
    protected Clone clone() throws CloneNotSupportedException {
    
    
        return (Clone)super.clone();
    }
}

class TestClone{
    
    
    public static void main(String[] args) {
    
    
        Clone clone1 = new Clone();
        clone1.setId(1L);
        clone1.setUsername("zhenghuisheng");
        clone1.setUsername("123456");
        try {
    
    
            Clone clone2 = clone1.clone();
            System.out.println(clone2.getId());
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}
  • 역직렬화 방법: 파일 또는 네트워크에서 바이너리 스트림 가져오기, 바이너리 스트림을 객체로 변환
//对象序列化
Student s = new Student("1","zhenghuisheng","18");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/a.txt"));
objectOutputStream.writeObject(s);
objectOutputStream.close();

//对象反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
Student student = (Student) inputStream.readObject();
  • 타사 라이브러리 Objenesis
//构建 Objenesis 对象  Objenesis需要对应的pom依赖对象
Objenesis objenesis = new ObjenesisStd();
ObjectInstantiator<Student> instantiator = objenesis.getInstantiatorOf(Student.class);
Student student = instantiator.newInstance();

1.2 객체 생성 단계

여기서는 주로 실행의 관점에서 객체 생성 단계를 분석하는데 위의 그림과 같이 객체를 생성하는 단계는 크게 6단계로 나누어진다.
여기에 이미지 설명 삽입

1.2.1, 개체에 해당하는 클래스가 로드, 확인, 준비, 구문 분석 및 초기화되었는지 확인

가상 머신이 새로운 명령어를 만나면 먼저 이 명령어의 매개변수가 메타스페이스의 상수 풀에서 클래스 기호를 찾을 수 있는지 여부를 확인하고 클래스가 로드, 확인, 준비 및 구문 분석을 거쳤는지 확인하고 이를 초기화합니다. 몇 단계. 그렇지 않은 경우 클래스 로더는 여전히 상위 위임 모드에 있으며 현재 클래스 로더의 키를 사용 ClassLoader + package + class하여 해당 .class 파일을 검색합니다. 해당 파일이 없으면 ClassNotFoundException예외가 발생합니다. 찾은 경우, 클래스 로딩 및 해당 클래스 객체 생성

1.2.2, 객체를 위한 공간 열기 및 메모리 할당

먼저 객체가 차지하는 공간의 크기를 계산한 후 새로운 객체에 대한 힙의 메모리 조각을 나누어 인스턴스 멤버 변수가 참조 변수인 경우 참조 변수 공간만 할당하면 된다. 크기는 4바이트입니다. 예를 들어, 서로 다른 기본 데이터 유형이 차지하는 바이트 수에 따라 각 변수가 차지하는 공간을 알 수 있으며 마지막으로 이러한 변수에 필요한 모든 공간을 더하여 총 공간의 바이트 수를 얻을 수 있습니다.

그리고 메모리가 정상이면 가상 머신은 포인터 개체에 대한 메모리를 할당합니다. 아래 그림과 같이 한 쪽에 사용된 메모리를 놓고 다른 쪽에 여유 메모리를 배치합니다 중간에 구분점 표시가 있습니다 메모리 할당은 포인터를 여유 있는 쪽으로 이동하는 것 입니다 객체가 필요로 하는 이동거리는 크기 , 포인터 충돌방식은 가상머신의 가비지컬렉션 알고리즘에 압축기능이 있는지 여부에 따라 달라진다.

[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있습니다. 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-mPmoI7G5-1681101135139)(img/1680846919641.png)]

내부 메모리가 일정하지 않은 경우 가상 머신 내부에 사용된 메모리와 사용되지 않은 메모리를 관리하기 위한 목록이 유지되어야 하며, 이를 여유 . 아래 그림과 같이 가상 머신 내부에 테이블이 유지되며, 어떤 메모리가 사용 가능한지, 어떤 메모리가 사용 불가능한지 기록하고 할당할 때 목록에서 충분히 큰 공간을 찾아 객체 인스턴스에 할당합니다. 테이블의 내용을 업데이트합니다.

[외부 링크 사진 전송 실패, 소스 사이트에 도난 방지 링크 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-enJCEOZ8-1681101135140)(img/1680847513344.png)]

1.2.3, 동시성 문제 다루기

객체는 힙에서 생성되고 힙은 공유 영역이므로 이러한 동시성 문제를 피할 수 없습니다.힙 내부에서는 인스턴스의 보안을 보장하기 위해 주로 두 가지 방법이 사용됩니다.

하나는 CAS 비교 및 ​​교환 방법을 사용하여 실패하면 재시도하고 업데이트의 원자성을 보장하기 위해 영역을 잠그는 것이고 다른 하나는 각 스레드에 대해 TLAB를 미리 할당하는 것입니다 . 동시성 보안 문제를 해결하는 것은 주로 이 두 가지 방법을 통해서입니다.

1.2.4, 개체 초기 할당

여기에서 기본 초기화가 수행됩니다 . 이러한 방식으로 모든 속성에는 개체 인스턴스 필드가 할당되지 않은 경우 사용할 수 있도록 하는 기본값이 있습니다. 따라서 메소드 내부에서는 준비 단계에서 정적 변수를 초기에 할당하고, 인스턴스 변수도 공간 할당 시 초기에 할당하기 때문에 이 두 변수를 그대로 사용할 수 있다. 직접 컴파일 실패가 발생합니다.

1.2.5, 개체의 개체 헤더 설정

객체가 속한 클래스, 객체의 hashCode, GC 정보, age, lock 정보 등을 객체의 객체 헤더에 저장합니다.

1.2.6, init 메서드를 실행하여 초기화

여기에 디스플레이 초기화가 있으며 초기화 작업이 정식으로 시작됩니다. 멤버 변수를 초기화하고, 인스턴스화 코드 블록을 실행하고, 클래스의 생성자를 호출하고, 힙 개체의 첫 번째 주소를 참조 개체에 할당합니다. 따라서 일반적으로 새로운 명령 뒤에는 프로그래머의 희망에 따라 개체를 초기화하는 실행 방법이 뒤따르므로 진정으로 사용 가능한 개체가 완전히 생성됩니다.

2. 개체의 메모리 레이아웃

개체의 메모리 레이아웃에는 주로 개체 헤더, 인스턴스 데이터 및 채우기가 포함됩니다.

[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-ehB4t1wc-1681101135141)(img/1680853091624.png)]

2.1, 객체 헤더(Header)

개체 헤더에서 두 부분으로 나눌 수 있습니다. 한 부분은 런타임 메타데이터이고 다른 부분은 유형 포인터입니다.

런타임 메타데이터에는 해시 코드, GC 연령 생성, 스레드가 보유한 잠금, 잠금 보유 플래그, 스레드 ID 및 스레드 타임스탬프가 포함됩니다 .

아래 그림에서 알 수 있듯이 객체의 나이를 4비트로 나누었으므로 최대값은 1111, 즉 15이고 0부터 시작하므로 최대 나이는 15이므로 이 나이를 설정하면 더 작은 값으로만 ​​설정할 수 있습니다 . 잠금의 플래그 비트에 해당하는 바이트 코드는 01, 10 및 11로 표시되며 해당 값은 각각 1, 2 및 3에 해당하며 잠금 업그레이드 프로세스는 비가역적입니다.

[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-LgRgzEzW-1681101135141)(img/1680854392412.png)]

유형 포인터는 객체의 유형을 결정하는 메타데이터 InstanceKlass를 가리킵니다. 배열인 경우 배열의 길이도 기록해야 합니다.

2.2, 인스턴스 데이터(Instance Data)

객체가 실제로 저장하는 유효 정보에는 코드에 정의된 다양한 유형의 필드와 부모 클래스에서 상속되어 자체적으로 소유한 필드가 포함됩니다 . 그리고 이러한 객체들에서 부모 클래스에 의해 정의된 변수는 하위 클래스보다 먼저 나타나며 항상 같은 필드가 함께 할당됩니다. 클래스 변수.

2.3, 코드 예제

다음으로 다음 코드를 분석합니다.

/**
 * @author zhenghuisheng
 * @date : 2023/4/7
 */
public class Customer {
    
    
    Integer id = 1001;
    String name = "zhenghuisheng";
    public Customer(){
    
    
        Account account = new Account();
    }
}
public class Test{
    
    
    public static void main(String[] args){
    
    
        Customer cust = new Customer(); 
    }
}

그러면 그에 해당하는 메모리 구조는 아래 그림과 같다.이 메인 메소드는 정적 메소드이기 때문에 로컬 변수 테이블의 첫 번째 슬롯은 this가 아니며, 로컬 변수 테이블의 두 번째 cust는 다음에서 new를 참조한다. 인스턴스 객체인 Customer()의 인스턴스 주소는 주로 위의 런타임 메타데이터, 유형 포인터 및 그 채우기로 구성됩니다. 런타임 데이터 영역에는 고유 주소의 해시 값, 여러 GC 후 결과의 나이, 잠금 획득 여부 등이 포함되며, 유형 포인터는 Customer의 Klass 클래스 메타 정보에 해당하고, 인스턴스 데이터에는 자신의 속성 및 상위 클래스 속성

[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있으므로 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-wdzf03vJ-1681101135142)(img/1680855082757.png)]

3. 객체 접근 위치

객체를 생성하는 주요 목적은 그것을 더 잘 사용하는 것입니다.JVM 내에서 내부 객체에 대한 객체 참조 액세스를 실현하는 방법에는 주로 두 가지가 있습니다. 하나는 직접 포인터이고 다른 하나는 핸들 액세스 입니다 .

다음 코드와 같이 객체 생성에는 힙, 스택 및 메서드 영역이 포함되어야 하므로 객체의 위치 및 액세스도 이 세 위치를 설계해야 합니다.

//第一个User存在方法区,主要是存储类信息和运行时常量池
//第二个user在栈中,作为变量存储
//最后的 new User存储在堆中
User user = new User();

핸들 접근 방식은 다음과 같다. 자바 힙에 핸들 풀이 있고, 핸들 풀은 인스턴스를 가리키는 주소를 힙에 저장하고 클래스 정보를 가리키는 주소를 메서드 영역에 저장한다. 핸들 풀의 주소를 스택에 저장할 수 있습니다.

[외부 링크 사진 전송 실패, 소스 사이트에 거머리 방지 메커니즘이 있을 수 있습니다. 사진을 저장하고 직접 업로드하는 것이 좋습니다(img-TRULI2d8-1681101135142)(img/1680857098011.png)]

직접 포인터는 핸들 풀을 사용할 필요가 없으며 힙의 인스턴스 주소가 스택의 로컬 변수 테이블에 직접 저장되고 힙의 주소를 가리키는 포인터가 있음을 의미합니다. 메서드 영역의 예약된 클래스 정보. Hotspot 가상머신에서는 이 방식을 주로 사용

여기에 이미지 설명 삽입

핸들 포인터는 핸들 풀을 저장하기 위해 힙 공간에서 공간을 열어야 하므로 일정량의 공간 낭비가 발생하고 효율성은 상대적으로 낮지만 가비지 수집과 같이 객체의 위치가 변경되면 또는 마크업 알고리즘을 사용할 때 이 스택의 힙에 있는 핸들 풀에 대한 포인터는 변경할 필요가 없으며 핸들 풀의 인스턴스 데이터 및 메서드 영역에 대한 포인터만 변경하면 됩니다. 이 다이렉트 포인터의 장점과 단점은 핸들 포인터와 정반대입니다.

4. 직접 기억(이해)의 첫 경험

JDK8에서는 메서드 영역의 구체적인 구현이 영구 생성에서 메타스페이스로 변경되었으며 메타스페이스는 직접 메모리라고도 하는 로컬 메모리를 사용합니다. JVM(Java Virtual Machine) 사양 "에 정의된 메모리 영역

여기에 이미지 설명 삽입

Java 코드에서는 ByteBuffer.allocateDirect()this 를 즉, 이 NIO를 통해 직접 작동하며 일반적으로 직접 메모리의 속도는 Java 힙보다 직접적으로 낫고 읽기 및 쓰기 성능이 상대적으로 높습니다. 따라서 성능을 고려하여 빈번한 읽기와 쓰기에는 다이렉트 메모리를 사용할 수 있으며 Java의 NIO 라이브러리도 Java 프로그램에서 다이렉트 메모리를 사용할 수 있도록 합니다.

OutOfMemoryError다이렉트 메모리가 Java 힙 외부에 있기 때문에 예외가 발생할 수도 있으므로 그 크기는 -Xmx로 지정된 최대 힙 크기에 의해 제한되지 않지만 시스템의 메모리는 항상 제한되므로 힙과 다이렉트 메모리의 합은 운영 체제에서 제공하는 최대 메모리에 의해 제한되지만 직접 메모리에는 할당 및 복구 비용이 높고 JVM 메모리 복구로 관리되지 않는다는 단점도 있습니다 .

따라서 직접 메모리의 크기는 MaxDirectMemorySize를 통해 , 지정하지 않으면 기본값은 힙의 -Xmx최대 매개 변수 값과 일치합니다.

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100 * 1024);

추천

출처blog.csdn.net/zhenghuishengq/article/details/130057210