본문 바로가기
[Development]/DailyGoogle

【D.D.S JAVA】Java 메모리 구조 (JVM) 및 런타임 메모리 구조

by Inkim 2021. 4. 20.

메모리 구조 이해의 필요성

  • 메모리 관리 부족 시, 서비스 속도 저하 및 서버 불능 상태 도달
  • 문제 예방 및 한정된 자원 하 효율적 메모리 사용을 통한 성능 향상

JVM(Java Virtual Machine)

(개념)

자바 가상 머신

자바 바이트 코드 실행 주체

JAVA (언어) + Virtual Machine (물리적인 형태가 아닌 소프트웨어로서의 개념)

→ 소프트웨어적으로 Java 언어를 실행시키기 위한 Machine

→ JVM은 정의된 스펙(표준은 존재하나 벤더사마다 다른 JVM 구현) 을 구현한 독자적인 프로세스 형태로 구동되는 Runtime Instance

(like) 개발자들이 작성한 Java 프로그램을 실행시키는 하나의 데몬

 

(특징)

CPU나 OS(플랫폼)와 독립적인 실행 가능

OS 위에서 동작하는 프로세스

자바 코드를 컴파일해서 얻은 바이트코드를 사용 중인 운영체제가 이해할 수있는 기계어로 변환하여 실행시켜주는 역할

 

(구성)

Class Loader

Execution Engine

Garbage Collector

Runtime Data Area

 

JVM 실행 과정

《자바에서 프로그램을 실행한다》

컴파일 과정을 통하여 생성된 Class 파일을 JVM으로 로딩하고 ByteCode를 해석(interpret)하는 과정을 거쳐 메모리 등의 리소스를 할당하고 관리하며 정보를 처리하는 일련의 작업

JVM은 Thread Synchronization 및 Garbage Collection과 같은 메모리 정리 작업도 수행

  • 실행될 클래스 파일을 메모리 로드 후 초기화 작업 수행
  • 메소드와 클래스변수들을 해당 메모리 영역 배치
  • 클래스 로드 종료 후 JVM은 main 메소드를 찾아 지역변수, 객체변수, 참조변수를 스택에 쌓음
  • 다음 라인을 진행하면서 상황에 맞는 작업 수행(함수 호출, 객체 할당 등)

 

(0) 선작업

자바 소스 작성 후, .java파일 생성

컴파일러 : Java Source 파일을 JVM이 해석할 수 있는 Java Byte Code로 변경

.java 소스를 자바 컴파일러가 컴파일하면 .class파일(바이트코드) 생성

자바 바이트 코드 : Java Compiler에 의해 수행된 결과물(확장자 .class 파일)

클래스 로더로 해당 클래스 파일들을 로드

로딩된 바이트코드에 대해 실행엔진으로 해석(Interpret)하여 런타임 데이터 영역에 배치

매번 인터프리터하여 한줄 단위 컴파일에 시간 소요 → JIT(Just In Time) 컴파일러 이용

(cf) JIT 컴파일러 미이용시 바이트코드를 사용할 때마다 인터프리터해서 사용하므로 실행시간 느림

적절한 시점에 JIT 컴파일러가 바이트 코드를 컴파일하여 네이티브 코드로 캐싱

컴파일한다 = 실행하는 전체 코드를 컴파일하고 캐시→ 시간소요

1회 컴파일 후 캐싱되면 그 이후로는 빠른 실행 보장

1회 실행되는 대상은 인터프리터 하고 주기적으로 수행되는 것들은 체킹으로 일정 횟수가 넘으면 JIT 컴파일러로 전체 컴파일

Runtime Data Areas 배치

 

(1) Class Loader

JVM 내로 클래스를 로드하고 링크로 배치하는 작업을 수행하는 모듈

런타임시 동적으로 클래스 로드

생성된 클래스파일을 엮고 JVM이 OS로부터 할당받은 메모리영역인 Runtime Data Area로 적재

자바 애플리케이션이 실행중일 때 해당 작업 수행

 

(2) Execution Engine

Loading된 클래스의 Bytecode 해석(interpret)

Class Loader에 의해 JVM 내의 런타임 데이터 영역에 배치된 바이트 코드 실행.

메모리에 적재된 클래스(자바 바이트 코드)를 기계어로 변경 후, 명령어 단위로 읽어서 실행

 

※ 실행 방식 2가지

① 인터프리터(Interpreter) 방식

명령어를 하나 하나 읽어들여 실행

② JIT(Just-In-Time) 방식

컴파일러 이용하여 적절한 시간에 전체 바이트 코드를 네이티브 코드로 변경해서

Execution Engine이 네이티브로 컴파일된 코드를 실행하는 것

성능 향상 방식

 

(3) Garbage Collector

Garbage Collector(GC)는 Heap 메모리 영역에 생성(적재)된 객체들 중에 참조되지 않는 객체들을 탐색 후 제거하는 역할

JVM은 Garbage Collector로 메모리 관리 기능자동 수행

애플리케이션이 생성한 객체의 생존 여부 판단 후, 사용되지 않는 객체를 해제하는 방식으로 메모리 자동 관리

(특징)

GC가 역할을 하는 시간 미정 → 참조가 없어지자마자 해제되는 것을 보장하지 않음

GC가 수행되는 동안 GC를 수행하는 쓰레드가 아닌 다른 모든 쓰레드 일시정지

Full GC 발생하여 수 초간 모든 쓰레드가 정지한다면 장애로 이어지는 치명적인 문제 발생 가능

 

(4) Runtime Data Areas

JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역.

Class Loader에서 준비한 데이터를 보관하는 저장소

JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터를 적재하는 영역

Method Area, Heap Area, Stack Area, PC Register, Native Method Stack 분류

 

Runtime Data Areas

(1) Method (Static) Area

(개념)

JVM이 읽어들인 클래스와 인터페이스 대한 런타임 상수 풀, 멤버 변수(필드), 클래스 변수(Static 변수), 생성자와 메소드 저장 공간

 

(생성대상)

클래스 멤버 변수명, 데이터 타입, 접근 제어자 정보 등 필드 정보와 메소드명, 리턴 타입, 파라미터, 접근 제어자 정보 등의 메소드 정보, Type정보(Interface인지 class인지), Constant Pool, static 변수, final class 변수 등 생성 영역

 

(특징)

Class Constructor와 Method Code가 저장

클래스의 인스턴스가 생성된 후 메소드 실행 시 모든 정보는 Method Area에 저장

 

  • Runtime Constant Pool→ 클래스 파일 constant_pool 테이블에 해당하는 영역→ 이미 있는 메소드나 필드의 참조를 통해 중복을 막음
  • → 문자 상수, 타입, 필드, 객체 참조가 저장
  • → 클래스, 인터페이스 상수, 메소드, 필드에 대한 모든 레퍼런스 저장
  • → 메소드 영역에 포함되지만 독자적 중요성
  • JVM은 런타임 상수 풀로 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조

메소드 영역/런타임 상수 풀의 사용기간 및 스레드 공유 범위》

  • JVM 시작시 생성되며 프로그램 종료 시까지 유지
  • 명시적 null 선언 시
  • 구성 방식이나 GC 방법은 JVM 벤더마다 다르다
  • 모든 스레드가 공유

(2) Heap Area

(개념)

자바 클래스의 인스턴스(instance)와 배열(array)이 할당되는 곳. 런타임(run time) 데이터 저장 영역

new 키워드로 생성된 객체와 배열이 생성되는 영역

Thread 간 공유 정보는 Stack에 저장이 되고 Class나 Method 정보, Bytecode는 Method Areas 저장. Heap은 Instance, Array 객체 두 가지 종류만 저장→모든 Thread에 의해 공유되는 영역

동일한 애플리케이션을 사용하는 Thread 사이에서는 공유된 Heap Data 이용 시, 동기화 이슈 발생

JVM은 Java Heap에 메모리를 할당하는 메소드(ex.new 연산)만 존재. 메모리 해제를 위한 코드를 직접 프로그래머가 명시하지 않음 → Heap의 메모리 해제는 Garbage Collection을 통해서만 수행

메소드 영역에 로드된 클래스만 생성이 가능하고 Garbage Collector가 참조되지 않는 메모리를 확인하고 제거하는 영역

힙 영역의 크기는 -Xms VM option으로 지정하며 가비지 컬렉션의 전략에 따라 고정된 크기 또는 유동적 변경 가능 → 디폴트로 설정된 힙 영역 크기 64MB

힙 영역의 최대 크기는 -Xmx option으로 설정

 

  • JVM이 관리. 프로그램 상에서 데이터 저장을 위해 런타임 시 동적으로 할당하여 사용하는 영역
  • → JVM이 시작 시 생성되어, 애플리케이션 실행 동안 크기가 커지거나 작아짐.
  • New 연산자로 생성된 객체 또는 객체(인스턴스)와 배열 저장되는 영역
  • 힙 영역에 생성된 객체와 배열은 스택 영역 변수나 다른 객체의 필드에서 참조
  • 참조하는 변수나 필드가 없다면 의미 없는 객체가 되어 GC 대상

《힙 영역의 사용기간 및 스레드 공유 범위》

  • 객체가 더 이상 사용되지 않거나 명시적으로 null 선언 시
  • GC(Garbage Collection) 대상
  • 구성 방식이나 GC 방법은 JVM 벤더마다 다르다
  • 모든 스레드에서 공유

《HotSpot JVM Heap 구조 변화》

 

Perm 영역 → Metaspace 영역 대체

※ Method Area, Heap Area는 모든 쓰레드가 공유


※ Stack Area는 각 쓰레드 개별 할당

 

(3) Stack Area

  • 각 스레드마다 하나씩 존재하며, 스레드가 시작될 때 할당
  • 메소드 호출마다 프레임(Frame) 추가(push) 후, 메소드 종료 시 해당 프레임 제거(pop) 동작 수행
  • → 메소드 호출마다 개별 스택 생성
  • 선입후출(FILO, First In Last Out) 구조로 push와 pop 기능 사용
  • 메소드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame 저장
  • 메소드 정보, 지역변수, 매개변수 파라미터, 리턴 값, 연산 중 발생하는 임시 데이터 생성 및 저장
  • 기본(원시)타입 변수는 스택 영역에 직접 값을 가짐
  • 참조타임 변수는 힙 영역이나 메소드 영역의 객체 주소를 가진다.

(ex)

int a = 10; 소스 작성 시, 정수값이 할당될 수 있는 메모리 공간을 a 로 잡고 해당 메모리 영역에 값 10이 들어간다 → 스택 메모리에 이름을 a로 붙이고 값이 10인 메모리 공간 생성

Person p = new Person(); 소스 작성 시, Person p는 스택 영역에 생성되고, new로 생성된 Person 클래스의 인스턴스는 힙 영역 생성

스택 영역에 생성된 p의 값으로 힙 영역의 주소값을 가지고 있음. → 스택 영역에 생성된 p가 힙 영역에 생성된 객체를 가리키고(참조하고) 있음

 

(4) PC Register

(개념)

Thread(쓰레드)가 생성될 때마다 생성되는 영역

스레드의 명령어 실행 기록 → JVM의 명령어 주소를 가짐

Program Counter : 현재 쓰레드가 실행되는 부분의 주소와 명령을 저장하는 영역

(단, CPU의 레지스터와 다름)

쓰레드를 돌아가면서 수행하도록 보장하는 역할

 

  • 현재 수행 중인 JVM 명령 주소를 가짐
  • 각 Thread별로 하나씩 생성
  • 프로그램 실행은 CPU에서 인스트럭션(Instruction)을 수행.
  • CPU는 인스트럭션 수행 동안 필요 정보를 CPU 내 기억장치인 레지스터에 저장
  • 연산 결과값을 메모리에 전달하기 전 저장하는 CPU 내의 기억장치

(5) Native Method Stack Area

  • 바이트 코드가 아닌 Binary Code 실행 영역
  • 자바 이외 언어로 작성된 네이티브 코드를 위한 Stack 메모리 영역
  • JNI(Java Native Interface)로 호출되는 C/C++ 등의 코드 수행 목적의 스택
  • (ex) I/O작업을 위한 C 라이브러리 함수 등
  • 네이티브 메소드의 매개변수, 지역변수를 바이트 코드로 저장

(종합)

쓰레드가 생성되었을 때 기준으로 메소드 영역과 힙 영역을 모든 쓰레드가 공유하고,

스택 영역 , PC 레지스터, Native method stack 는 각 쓰레드마다 생성되고 공유안함

 

Heap Area

  • Young Generation: 자바 객체 생성 직후 저장되고, 생긴지 얼마 안된 객체 저장 공간. 시간이 지나 우선순위가 낮아지면 Old 영역으로 이동. 이 영역에서 객체가 사라질 때 Minor GC 발생
  • Old(Tenured) Generation: Young Generation 영역에 저장된 객체 중 오래된 객체가 이동되어 저장되는 영역. 이 영역에서 객체가 사라질 때 Major GC(Full GC) 발생

nursery(혹은 young space/young generation) 파트 = Eden + Survivor

  • 새로운 객체를 할당하기 위해 힙에 확보된 공간
    • new를 통해 새로 생성된 객체 위치
    • 정기적 GC 후 살아남은 객체들은 Survivor 이동
    ② Survivor1, Survivor2
    • 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 이동
    • 참조가 없는 객체들은 Minor GC로 수집됨
    • 항상 Survivor1과 2중 한 곳은 비어있는 상태
  • ① Eden
    • nursery가 가득 차면 young collection이라는 것을 실행하여 쓰레기(garbage) 수집② Major GC : Old, Permanent 영역에서 발생하는 GC
    • ① Minor GC : Eden, Survivor에서 발생하는 GC
    • young collection은 nursery에 오래 머문 객체를 old space로 이동시켜 nursery가 더 많은 객체를 할당할 수 있도록함
  • nursery는 3가지 부분. - Eden Momory와 2개의 Survivor Memory 공간(spaces)

old space(혹은 old generation) : 참조가 거의 없음

  • Youn Generation에서 마지막까지 살아남은 객체가 이동
  • Major GC가 이루어지며, Minor GC보다 횟수가 적음

perm : 프로그램 종료 시까지 살아있어야 하는 메타데이터 집합

  • Class Loader에 의해 Load된 클래스들이 저장
  • JDK 8부터는 Metaspace 영역으로 교체

None Heap Area

(특징)

JVM이 시작할 때 생성

런타임 상수 풀, 필드 및 메소드 데이터와 같은 클래스별 구조, 메소드 및 생성자에 대한 코드, 내부 문자열 저장

디폴트로 지정된 Non-Heap 영역의 크기는 64MB

XX:MaxPermSize VM option 로 변경

 

  • Permanent Generation(Java 7 이전): 클래스 로더에 의해 로드되는 클래스, 메소드 등에 대한 메타 정보 저장 영역으로 JVM에 의해 사용. 리플렉션으로 동적 클래스 로딩 때 사용. 내부적으로 리플렉션 기능을 자주 사용하는 Spring Framework 이용 시, 이 영역에 대한 고려가 필요. 런타임시 사이즈 조절이 불가하며 OutOfMemoryError: PermGen Space error 가 발생하는 메모리 영역. JVM 실행시 PermSize와 MaxPermSize 옵션 사용.
  • Metaspace(Java 8 이후): Permanent Generation이 Metaspace로 변경. 동적 사이즈 변경 가능. JVM 옵션도 PermGem관련 소멸하고, Metaspace 관련 MetaspaceSize 및 MaxMEtaspaceSize 옵션 신설.

Method Area(Static Area)

(개념)

JVM 구동 시, 생성되며 모든 스레드 공유 영역

JVM 구동 중 사용될 클래스 파일을 읽고 클래스별 runtime constant pool(런타임 상수 풀), field data(필드 데이터), method data(메서드 데이터), constructor(생성자) 등 저장

 

(저장대상)

  1. 필드 정보 : 멤버변수명, 데이터 타입, 접근 제어자 등의 정보
  2. 메서드 정보 : 메서드명, 리턴타입, 매개변수, 접근제어자 등의 정보
  3. 상수 풀(String Constant Pool)
  • Type에서 사용된 상수를 저장하는 곳 (중복 시, 기존 상수 사용)
  • 문자 상수, 타입, 필드, Method reference도 상수 풀 저장
  • final class 변수의 경우, 상수 풀에 값 복사
  1. Static 변수
  • 모든 객체가 공유 할 수 있고, 객체 생성없이 접근 가능

(특징)

프로그램 종료까지 메모리에 상주

Static 영역 데이터는 프로그램 시작 ~ 종료시까지 메모리에 남아있음

 

(장점 및 단점)

장점 : 프로그램이 종료될 때까지 어디서든 사용 가능

단점 : 무분별한 사용으로 인한 메모리 부족 → 메모리 가득 찰 시 java.lang.StackOverFlowError 발생

힙 (heap)

(개념)

ㆍ객체 배열 생성 영역.

ㆍ해당 영역에 생성된 객체와 배열은 JVM 스택 영역의 변수, 다른 객체의 필드에서 참조

ㆍ참조하는 변수, 필드가 없으면 JVM이 Garbage Collector를 실행하여 해당 객체 제거

 

(특징)

  1. 객체(인스턴스 - new 연산자로 생성된 객체)가 저장되는 영역
  2. 프로그램 실행 중 생성된 객체는 Heap영역에 동적 할당 → Garbage Collector로 메모리 반환
  3. 참조형(Reference Type) 데이터타입 객체(인스턴스), 배열 등은 Heap 영역에 데이터 저장.

( 실제 데이터가 저장된 Heap 영역의 참조값(reference value, 해시코드 / 메모리에 저장된 주소를 연결해주는 값)을 저장)

 

  1. 런타임에 Object와 JRE 클래스에 대한 할당에 사용
  2. 객체의 위치를 개발자가 알 수 없으며 참조를 Stack에게 반환
  3. stack 값이 null이라면 참조할 수 있는 값이 없으므로 nullPointerException

(cf) nullPointerException

Calling the instance method of a null object.

(빈 객체의 메소드를 호출하는 경우)

Accessing or modifying the field of a null object.

(빈 객체의 필드에 접근하거나 수정하려는 경우)

Taking the length of null as if it were an array.

(빈 배열의 길이를 가져오려는 경우)

Accessing or modifying the slots of null as if it were an array.

(빈 배열의 인덱스를 접근하거나 수정하려는 경우)

Throwing null as if it were a Throwable value.

(예외처리를 위해 널 값을 던지는 경우)

 

JVM 스택 영역

ㆍJVM 스택 영역은 스레드가 실행될 때 할당 (스레드마다 하나씩 존재)

ㆍJVM 스택은 메서드 호출 시 프레임(Frame)을 추가(Push)하고 메서드 종료 시, 해당 프레임(Frame) 제거(pop)

ㆍ실행 순서에 따라 생성되고 소멸 (Last In First Out (LIFO))

차이점

ㆍ힙(heap) 과 스택(Stack Area)은 프로세스의 모든 스레드에 공유되는 공통영역

ㆍCallStack은 쓰레드 생성시 쓰레드별로 할당되는 쓰레드별 영역

 

▣ 결과

전역변수 호출 : GodDaeHee

현재 스피드 : 10

전역변수 어디든 호출 가능 : GodDaeHee

현재 스피드 : 35

mycar 객체의 주소 : aaa.Mycar@6d0690c

yourcar 객체의 주소 : aaa.Mycar@783722

str1 == str2 결과? true

str1 == str3 결과? false

 

소스원문

public class MyCar { // 객체 int speed = 0; static String makerName = "GodDaeHee"; // 전역변수 public void speedUp(int speedUp){ // 메서드 정보 (메서드 이름, 리턴타입, 매개변수, 접근제어자) int defaultSpeedUp = 5; // 지역변수 speed = speed + defaultSpeedUp + speedUp; System.out.println("3. 전역변수 어디서든 호출 가능 : " + makerName); //전역변수 호출 } public void speedDown(int speedDown){ // 1-2. 메서드 정보 (메서드 이름, 리턴타입, 매개변수, 접근제어자) int defaultSpeedDown = 5; // 지역변수 speed = speed - defaultSpeedDown - speedDown; } public static void main(String[] args) { // Static영역은 처음부터 메모리에 띄운다. MyCar myCar = new MyCar(); //클래스를 객체(인스턴스)화, 메모리에 MyCar를 띄운다.(Heap에 객체 생성) MyCar yourCar = new MyCar(); //클래스를 객체(인스턴스)화, 메모리에 MyCar를 띄운다.(Heap에 객체 생성) System.out.println("1. 전역변수 호출 : " + makerName); //전역변수 호출 myCar.speed = 10; System.out.println("2. 현재 스피드 : " + myCar.speed); myCar.speedUp(20); System.out.println("4. 현재 스피드 : " + myCar.speed); System.out.println("5. myCar 객체의 주소 :" + myCar); // Heap영역에 저장 된 myCar 객체의 주소 System.out.println("6. yourCar 객체의 주소 :" + yourCar); // Heap영역에 저장 된 yourCar 객체의 주소 /* 상수풀 힙 이해 */ String str1 = "GodDaeHee"; // GodDaeHee 상수풀에 저장 String str2 = "GodDaeHee"; // GodDaeHee 상수풀에 저장 String str3 = new String("GodDaeHee"); // 힙영역에 저장 // str1 == str2 => 상수풀에 저장되어있고 True System.out.print("7. str1 == str2 결과? "); System.out.println(str1 == str2); // str1 == str3 => str3은 힙영역에 저장되어 주소가 다르다. False System.out.print("8. str1 == str3 결과? "); System.out.println(str1 == str3); } }

 

GC의 주요 대상

《GC의 주요 대상》

힙, Stack, Method 영역

 

▣ 힙 영역

5개의 영역 : eden, survivor1, survivor2, old, permanent

 

(특징)

JDK7까지 permanent영역이 heap에 존재

JDK8부터 permanent 영역 소멸 후, 일부가 "meta space 영역" 변경

meta space 영역은 Native stack 영역에 포함 되었다.

survivor영역의 숫자는 의미없고 두 개로 나뉜다는 것이 중요

 

(cf) 힙 영역을 5가지로 나눈 이유

효율적 GC 목적

 

《GC 종류》

Minor GC vs Major GC

 

(1) Minor GC : New 영역에서 일어나는 GC

  1. 최초 객체 생성 시, Eden영역 생성
  2. Eden영역에 객체가 가득차게 되면 첫 번째 CG 발생
  3. survivor1 영역에 Eden영역 메모리 그대로 복사 → survivor1 영역 제외 타 영역의 객체 제거
  4. Eden영역도 가득차고 survivor1영역도 가득차면, Eden영역에 생성된 객체와 survivor1영역에 생성된 객체 중 참조되고 있는 객체가 있는지 여부 검사
  5. 참조 되지 않은 객체는 내버려두고 참조되는 객체만 survivor2 영역으로 복사
  6. survivor2 영역을 제외한 다른 영역의 객체 제거
  7. 일정 횟수이상 참조 중인 객체를 survivor2에서 Old영역 이동

⇒ 1~7 과정 반복 : survivor2 영역까지 꽉차기 전에 계속해서 Old로 비움

 

(2) Major GC(Full GC) : Old 영역에서 일어나는 GC

  1. Old 영역에 있는 모든 객체 검사 후 참조되고 있는지 여부 확인
  2. 참조되지 않은 객체를 모아 한 번에 제거
  • Minor GC보다 시간이 많이 걸리고 실행 중에 GC를 제외한 모든 쓰레드 중지
  • Major GC(Full GC) 발생 시, Old영역에 있는 참조가 없는 객체를 표시하고 해당 객체 모두 제거

Heap 메모리 영역에 중간중간 제거된 이후의 빈 메모리 공간이 발생하며 해당 부분 제거를 위해 재구성 (like 디스크 조각모음처럼 조각난 메모리 정리)

메모리 이동 중, 타 쓰레드에 의한 메모리 사용 방지를 위해 모든 쓰레드 정지

 

⇒ Youn Generation의 Minor GC, Old의 Major GC를 거치며 최종적으로 모든 쓰레기가 수집


참고블로그

https://goddaehee.tistory.com/149 [갓대희의 작은공간]

https://hoonmaro.tistory.com/19

https://jeong-pro.tistory.com/148

https://shinjekim.github.io/java/2020/01/06/자바의-메모리-구조/

https://velog.io/@agugu95/자바와-JVM-그리고-메모리-구조

https://coding-start.tistory.com/205