JVM 시리즈-섹션 1 : JVM 소개, 런타임 데이터 영역, 메모리 생성 모델

1. JVM이란?

JVM은 Java Virtual Machine (Java Virtual Machine)의 약자입니다. JVM은 컴퓨팅 장치의 사양으로 실제 컴퓨터에서 다양한 컴퓨터 기능을 시뮬레이션하여 구현 된 가상 컴퓨터입니다.

JVM은 사양이며 Oracle / Sun JDK, OpenJDK 등과 같은 많은 구현이 있으며 모두 동일한 JVM ( HotSpot VM)을 사용합니다 . IBM에서 개발 한 고도로 모듈화 된 JVM : J9 . 또한 다른 많은 JVM 구현이 있습니다. 일반적으로 사람들이 "자바 성능은 어떻습니까", "자바에 얼마나 많은 GC가 있는지", "JVM을 조정하는 방법"및 기타 문제에 대해 이야기 할 때 기본값은 HotSpot VM이므로 HotSpot VM이 절대 주류입니다. 아래 언급 된 JVM은 HotSpot을 의미합니다.

Java 가상 머신은 기본적으로 프로그램이며 명령 줄에서 시작되면 바이트 코드 파일에 저장된 명령을 실행하기 시작합니다. JVM에는 두 가지 중요한 기능이 있습니다. 1. 기계 코드 번역. JVM은 "한 번 컴파일하고 여러 번 실행"을 보장합니다. 플랫폼마다 JVM이 다르기 때문입니다. 예를 들어 HotSpot에는 Windows와 Linux 버전이 있고 플랫폼마다 다른 버전을 사용합니다. 프로그래머의 경우 일부 코드에만주의를 기울이면됩니다. 다른 플랫폼의 JVM이 시스템의 차이점을 보호했기 때문에 코드의 이식성. 2. 메모리 관리. 프로그래머는 객체를 사용해야하며 새 것이어야하며 새 것이 어떻게 나오는지 신경 쓰지 않고 객체의 수명주기에 신경 쓰지 않으며 객체를 언제 재활용해야하는지 신경 쓰지 않습니다.

JVM 파티션

Java 가상 머신은 주로 클래스 로더, 런타임 데이터 영역, 실행 엔진, 기본 메소드 인터페이스 및 가비지 콜렉션 모듈의 5 개 모듈로 나뉩니다. 다음 내용은 주로 런타임 데이터 영역과 가비지 수집기의 두 가지에 대해 설명합니다.

2. 런타임 데이터 영역

스레드가 실행되기 전에 실행해야하는 코드의 다른 부분이 런타임 데이터 영역의 다른 영역에 배치되고 스레드가 실행 중일 때 다른 위치에서 데이터를 가져옵니다.

2.1 프로그램 카운터

프로그램 카운터는 현재 스레드에서 실행중인 바이트 코드 명령어의 주소와 줄 번호를 저장합니다. 스레드에 의해 실행되는 바이트 코드 명령의 주소와 줄 번호를 기록하는 이유는 무엇입니까? Thread는 Java의 가장 작은 실행 단위입니다. CUP가 동시에 여러 스레드를 실행하면 스레드 전환이 수반되기 때문입니다. CUP가 스레드를 전환 할 때 CUP가 현재 스레드로 다시 전환 할 때 스레드가 알 수 있도록이 정보를 기록해야합니다. 위치가 실행을 계속하기 시작합니다. 각 스레드에는 자체 프로그램 카운터가 있습니다.

2.1 스택

가상 머신 스택 은 현재 스레드에서 실행중인 메소드에 필요한 데이터, 명령어 및 반환 주소를 저장 합니다.

간단한 코드 예제를 제공하십시오.

package com.wuxiaolong.jvm;

/**
 * Description:
 *
 * @author 诸葛小猿
 * @date 2020-09-06
 */
public class TestJVM {
    
    

    public static final int AGE = 30;

    public static void test () {
    
    
        int a = 1;
        int b = 2;
        int c = a + b;
        Object objc= new Object();
    }
}

위의 TestJVM.java에 의해 컴파일 된 TestJVM.class 파일을 찾고, javap명령을 통해 바이트 코드의 각 명령어를보고, 명령어를 TestJVM.txt 파일에 저장합니다.

$ javap -c -v ./TestJVM.class > TestJVM.txt

명령 파일 TestJVM.txt :

Classfile /C:/Users/WuXiaoLong/Desktop/java-summary/target/classes/com/wuxiaolong/jvm/TestJVM.class
  Last modified 2020-9-6; size 497 bytes
  MD5 checksum e2bee1c0136645a123ea37b4c6aba4a2
  Compiled from "TestJVM.java"
public class com.wuxiaolong.jvm.TestJVM
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
// 这里是常量池的描述      
Constant pool:
   #1 = Methodref          #2.#23         // java/lang/Object."<init>":()V
   #2 = Class              #24            // java/lang/Object
   #3 = Class              #25            // com/wuxiaolong/jvm/TestJVM
   #4 = Utf8               AGE
   #5 = Utf8               I
   #6 = Utf8               ConstantValue
   #7 = Integer            30
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/wuxiaolong/jvm/TestJVM;
  #15 = Utf8               test
  #16 = Utf8               a
  #17 = Utf8               b
  #18 = Utf8               c
  #19 = Utf8               objc
  #20 = Utf8               Ljava/lang/Object;
  #21 = Utf8               SourceFile
  #22 = Utf8               TestJVM.java
  #23 = NameAndType        #8:#9          // "<init>":()V
  #24 = Utf8               java/lang/Object
  #25 = Utf8               com/wuxiaolong/jvm/TestJVM
{
    
    
  // 静态常量AGE的描述    
  public static final int AGE;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 30
  // 这里是TestJVM类的描述
  public com.wuxiaolong.jvm.TestJVM();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wuxiaolong/jvm/TestJVM;
  // 这里是test方法的描述
  public static void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code: // test方法的指令
      stack=2, locals=4, args_size=0
         0: iconst_1
         1: istore_0
         2: iconst_2
         3: istore_1
         4: iload_0
         5: iload_1
         6: iadd
         7: istore_2
         8: new           #2                  // class java/lang/Object //创建一个对象 在堆上分配了内存并在栈顶压入了指向这段内存的地址
        11: dup
        12: invokespecial #1                  // Method java/lang/Object."<init>":()V  //调用构造函数、实例化方法
        15: astore_3
        16: return
      LineNumberTable:  // test方法在java代码中的行号 
        line 14: 0
        line 15: 2
        line 16: 4
        line 17: 8
        line 18: 16
      LocalVariableTable: // test方法的本地局部变量表
        Start  Length  Slot  Name   Signature
            2      15     0     a   I
            4      13     1     b   I
            8       9     2     c   I
           16       1     3  objc   Ljava/lang/Object;
}
SourceFile: "TestJVM.java"

위의 명령어 설명 파일을 통해 TestJVM 클래스의 모든 명령어에 대한 설명을 볼 수 있습니다.

JVM의 각 스레드에는 런타임 스택이 있습니다. JVM은 각 스레드에 의해 실행되는 메소드를 위해 런타임 스택에 공간을 엽니 다.이 공간을 스택 프레임 이라고합니다 . 각 스택 프레임은 메서드의 로컬 변수 테이블, 피연산자 스택, 동적 링크, 메서드 종료 (반환 주소) 등과 같은 여러 블록으로 나뉩니다.

가상 머신 스택

테스트 메소드를 기록하는 스택 프레임 에는 로컬 변수 테이블 의 테스트 메소드에 네 개의 로컬 변수가 있습니다. a / b / c / objc, TestJVM.txt 명령어 설명 파일의 81-86 행에 해당합니다.

62-74 행 TestJVM.txt 명령어 파일과 같은 피연산자 해당 지역 변수의 메모리에있는 피연산자 스택 은 명령어를 설명하고, 첫 번째 명령어 아이콘 t_1 : int 유형 상수 값 1을 스택에 넣고, int a = 1피연산자 1이 피연산자 스택 (현재 스택에 숫자 1이 하나만 있음), 두 번째 명령어 istore_0 : 스택 맨 위에있는 int 유형 값을 0 번째 지역 변수에 저장하고 0 번째 지역 변수는 Who? TestJVM.txt 명령어는 파일의 83 행을 설명하여 0 번째 로컬 변수 a를 나타내고 피연산자 1은 로컬 변수 a 아웃 바운드에 저장됩니다. 실제로 iconst_1 및 istore_0 int a = 1은이 코드에 의해 실행되는 명령입니다. 다른 명령어에 대한 분석에 대해서는 자세히 설명하지 않으며 각 명령어의 의미를 인터넷에서 확인할 수 있습니다.

TestJVM.java 테스트 메소드의 17 번째 줄에주의를 기울여야합니다 Object objc= new Object();. 이것은 위의 로컬 변수 a / b / c와 다른 객체입니다.이 문장에 포함 된 명령어는 new, dup, invokespecial 및 astore_3. 그중 new는 특히 힙에 메모리를 할당하고 스택 맨 위에이 메모리를 가리키는 주소를 푸시하는 객체 생성을 의미합니다. 객체는 힙에 저장되며 힙에있는 객체의 주소 만 스택에 저장됩니다 .

동적 연결 은 호출 된 메서드 또는 개체를 컴파일 타임에 확인할 수없는 경우를 의미합니다. 즉, 호출 메서드의 기호 참조는 프로그램 런타임 동안에 만 직접 참조로 변환 될 수 있습니다. dynamic, 그래서 동적 링크라고합니다. TestJVM.txt의 라인 70 및 72에있는 # 2 및 # 1과 유사하게, TestJVM.txt의 라인 12 및 11에있는 상수 풀에 해당하는 동적 링크 기능은 이러한 기호 (#)를 참조하는 것입니다. 호출 방법에 대한 참조. 간단한 예를 들자면, 일반적으로 Controller 계층에서 Service 계층이 호출 될 때 @Autowired를 통해 Service가 주입됩니다. 일반적으로 구현 클래스 대신 Service 인터페이스가 사용됩니다. Controller의 메서드에서 특정 메서드는 다음과 같습니다. Service 인터페이스의 메소드를 통해 호출됩니다. 서비스 구현시 Service 인터페이스의 구현이 여러 개있는 경우 프로그램은 컴파일시 사용할 구현 클래스를 알지 못합니다. 이때 동적 링크 (#) 가 생성됩니다. 바이트 코드 상수 풀 부분. 결국 호출 메서드의 직접 참조로 변환됩니다. 바이트 코드 명령어 파일을 통해 상수 풀을 "상수 풀"로 변환하는 것이 정확하지 않음을 알 수 있습니다. 상수 외에도 상수에는 심볼 참조 (클래스, 메서드, 필드 등에 대한 설명자 포함)가 있습니다. 풀.

메소드 종료 (반환 주소)는 메소드가 실행될 때 스택에서 팝되어야하며 스택이 팝된 후 어디로 가야하는지, 정상적으로 실행되는 메서드의 팝핑이 비정상적으로 실행되는 것과 다릅니다. 방법.

스레드에서 메서드가 재귀 적으로 호출되면 각 메서드 호출에 대한 스택 프레임이 있으므로 스레드가 요청한 스택 깊이가 가상 머신에서 허용하는 스택 깊이보다 크고 StackOverflowError가 발생합니다. 스택의 크기는 자동으로 확장 될 수 있지만 동적 확장 중에 더 큰 공간을 적용 할 수없는 경우에도 OutOfMemory를보고합니다.

스택 프레임 크기로 결정되는 시간은 컴파일 시간에 있으며 런타임 데이터의 영향을받지 않습니다. 따라서 로컬 변수 테이블이 필요로하는 메모리 공간은 컴파일 과정에서 할당되며, 메소드 입력시이 메소드가 스택에 할당해야하는 로컬 변수 공간의 양이 완전히 결정되며 로컬 변수 테이블의 크기는 변경되지 않습니다. 메서드를 실행하는 동안.

2.3 로컬 메서드 스택

네이티브 메서드 스택은 네이티브 메서드 실행을 설명한다는 점을 제외하면 가상 머신 스택과 유사합니다. 로컬 방법은 무엇입니까? native 메소드는 native 키워드에 의해 수정 된 메소드를 의미합니다. JDK에는 특정 구현 클래스가 없습니다. 특정 구현은 JVM 코드에 있습니다 . 여기 에서 다양한 버전의 Hotspot 소스 코드를 찾을 수 있습니다. 소스 코드 C 또는 C ++로 작성되었습니다. 이전 기사에서는 CAS를 분석 할 때 Hotspot 소스 코드를 사용했습니다.

2.4 분석법 영역

메소드 영역은 클래스 정보, 상수, 정적 변수, JIT 및 클래스 바이트 코드의 기타 정보를 저장합니다.

TestJVM.txt 명령 파일의 10-35 행은 상수 풀이며이 부분의 내용은 메서드 영역의 상수 풀에 배치됩니다.

방법 영역

여기에서 생각해 볼 수 있습니다. 왜 정적 변수와 상수를 힙에 넣지 않습니까? 상수와 정적 변수는 일반적으로 변경되지 않기 때문에 하나의 복사본을 저장하는 한 동일한 데이터가 힙의 모든 새 개체에 존재하여 공간 낭비를 유발한다고 생각합니다.

2.5 힙

대부분의 애플리케이션에서 힙은 JVM (Java Virtual Machine)에서 관리하는 가장 큰 메모리 영역입니다. 힙에 저장된 객체는 스레드가 공유하므로 멀티 스레딩시에도 동기화 메커니즘이 필요합니다 . 그러므로 우리는 이해에 초점을 맞출 필요가 있습니다.

힙에 저장된 모든 개체는 새 개체이며 스택에 저장된 개체에 대한 참조는 힙에있는 개체의 메모리 주소를 가리 킵니다. 모든 개체 인스턴스와 배열은 힙에 메모리를 할당해야하지만 JIT 컴파일러의 개발과 이스케이프 분석 기술의 성숙도 ( HotSpot 이스케이프 분석에 대해 이야기)로 인해이 진술은 그렇게 절대적이지는 않지만 대부분의 경우는 다음과 같습니다. .

셋, JVM 메모리 생성 모델

JDK1.8 이전에는 JVM의 메모리가 신세대, 구세대 및 영구 세대의 세 가지 주요 블록으로 나뉩니다. 처음 두 조각은 힙에 있고 후자는 메서드 영역에 있습니다. JDK1.8 이상에서 Meta Space는 영구 생성에서 제거되었습니다.

JVM 메모리 모델

세대를 생성해야하는 이유는 무엇 입니까? 다른 개체의 수명주기가 다르기 때문에 수명주기가 다른 개체는 다른 세대에 배치되고 다른 가비지 수집 알고리즘이 재활용에 사용됩니다.

3.1 신생대

일반적으로 신세대의 물체는 신생대에 배치되며, 신생대의 물체는 일반적으로 상대적으로 수명이 짧은 물체이며, 98 % 이상의 물체를 단일 컬렉션으로 재활용 할 수 있습니다 (Minor GC).

신생대 기억은 에덴 영역, s0 영역, s1 영역 으로 나뉘는데 왜 3 영역으로 나뉘는가 ?

신생대는 세 부분으로 나뉘는데, 신생대가 사용하는 가비지 콜렉션 알고리즘은 복제 알고리즘을 사용하기 때문입니다.

마이너 GC

S0과 S1은 크기와 기능이 같은 두 영역이지만 GC에서는 하나의 영역 만 작업 할 수 있습니다. 에덴 영역이 처음으로 가득 차면 첫 번째 마이너 gc가 트리거되고 에덴 영역의 객체가 복구됩니다. gc 이후에 도달 할 수있는 다른 객체가 있고 남아있는 객체에 속합니다. 이 개체는 s0 영역에 배치됩니다. 두 번째로 에덴 영역이 가득 차면 두 번째 마이너 gc가 트리거됩니다. 이때 에덴 영역과 s0 영역이 회수됩니다.이 시점에서 객체 b에 여전히 도달 할 수 있고 객체 j가 이 두 개체는 s1 영역에 들어갑니다. 각 gc에 남아있는 객체가 s0과 s1 영역 사이에서 앞뒤로 복사되는 것을 볼 수 있는데 이것이 복제 알고리즘입니다. gc 이후에 살아남는 개체의 수명은 1 씩 증가합니다. 여러 gc 수명이 고정 된 임계 값 (기본값 15)에 도달하면 개체가 수명에 들어갑니다.

신세대 메모리는 3 개의 Eden 영역, s0 영역, s1 영역으로 나뉘며 비율은 8 : 1 : 1입니다. 비율이 8 : 1 : 1 인 이유는 무엇 입니까? 복제 알고리즘에서는 s0 및 s1의 한 영역 만 작동 할 수 있고 다른 영역은 비어 있으므로 모든 신세대가 유효한 저장 공간이 아닙니다. s0 및 s1이 너무 많으면 사용 가능한 메모리가 작아지고 에덴 영역이 줄어 듭니다. s0과 s1이 너무 작아서 gc 시간이 줄어들면 s0과 s1이 가득 차서 어린 개체가 노년기에 접어 들게됩니다. 8 : 1 : 1은 28 개의 원칙으로 볼 수 있습니다.

3.2 노년

신세대와 구세대의 메모리 비율은 1 : 2입니다.

새로운 세대의 여러 컬렉션 후에도 여전히 존재하는 개체는 이전 세대로 들어갑니다.

3.3 영구 생성 및 메타 공간

영구 생성의 오버플로를 방지하기 위해 JDK1.8 이상에서는 영구 생성을 제거하고 Meta Space를 사용합니다. Meta Space의 메모리는 세대 외부에 할당 된 메모리에 속하며 기계의 직접 메모리를 사용합니다. Meta Space는 자동으로 확장 될 수 있지만 자동으로 확장 할 수 있지만 Meta Space는 가능한 한 크지 않습니다. 머신의 전체 메모리가 고정되어 있기 때문에 Meta Space는 다른 메모리 공간을 사용하게됩니다.

후속 작업에서는 Java 메모리 모델, 네 가지 참조, GC 복구 알고리즘, GC 복구 장치, JVM 최적화 등을 계속 소개합니다.

공식 계정에 따라 " java-summary "를 입력 하여 소스 코드를 받으십시오.

완료되었습니다.

[ 지식의 보급, 가치 공유 ] 여러분의 관심과 성원에 감사드립니다. 저는 망설이는 인터넷 이주 노동자 [ Zhuge Xiaoyuan ]입니다.

추천

출처blog.csdn.net/wuxiaolongah/article/details/109323029