17. [Java] Thread, JVM, Garbage Collection / 20230508

2023. 5. 9. 01:24

🧑🏻‍💻 TIL(Today I Learned)


✔️ Thread, JVM(Java Virtual Machine), Garbage Collection

 

1. Thread(스레드)

➡️ 프로세스 내에서 실행되는 소스 코드의 실행 흐름 
➡️ 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행

📍 프로세스(Process)?
→  어떤 애플리케이션이 실행되면 운영체제가 해당 애플리케이션에 메모리를 할당해 주며 애플리케이션이 실행되는데 이처럼 실행 중인 애플리케이션을 '프로세스' 라고 부름
→ 데이터, 컴퓨터 자원, 스레드 이 세 가지로 구성됨 

 

🔎 메인 스레드(Main Thread)

➡️ 자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 main() 메서드, 메인 스레드가 main() 메서드의 코드를 처음부터 끝까지 차례대로 실행시키며 코드의 끝을 만나거나 return 문을 만나면 실행 종료 

public static void main(String[] args) { // 위에서 아래로 흐름
	String data = null;
    if(...) {
    }
    while(...) {
    }
    System.out.println("...");
    }
   }

 

✍🏻 싱글 스레드(Single-Thread)

  • 어떤 자바 애플리케이션 소스 코드가 싱글 스레드로 작성되었다면 그 애플리케이션이 실행되어 프로세스가 될 때 오로지 메인 스레드만 가지는 싱글 스레드 프로세스가 됨 
  • 싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료됨 

 

✍🏻 멀티 스레드(Multi-Thread)

➡️ 하나의 애플리케이션 내에서 여러 작업을 동시에 수행하는 멀티 태스킹을 구현하는 데  핵심적인 역할 

(ex. 메신저 프로그램 사용 시 보낼 사진을 업로드하면서 동시에 메세지 주고받기 가능) 

➡️ 메인 스레드에서 또 다른 스레드를 생성하여 실행시키면 해당 애플리케이션은 멀티 프로세스로 동작하게 됨 

➡️ 메인 스레드가 작업 스레드보다 먼저 종료되더라도 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않음 

➡️ 하지만, 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미침

 

 

🔎 스레드의 생성과 실행 

➡️ 작업 스레드를 활용한다는 것은 작업 스레드가 수행할 코드를 작성하고 작업 스레드를 생성하여 실행시키는 것을 의미 

➡️ 자바는 작업 스레드도 객체로 관리하기 때문에 클래스 필요 
➡️ run() 메서드 내부에 스레드가 처리할 작업을 작성 

➡️ 생성하고 실행하는 두 가지 방법

 

✍🏻 Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
       (일반적으로 많이 사용 --> 다중 상속 가능)

Runnable 인터페이스 구현

  • 결과는 @ 과 # 이 섞여서 출력됨 
    → 메인 스레드와 작업 스레드가 동시에 병렬로 실행되면서 각각 main() 메서드와 run() 메서드 코드 실행시켰기 때문

 

✍🏻 Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

하위 클래스

  • 첫 번째 방법과는 달리 Thread 클래스를 직접 인스턴스화하지 않음 
  • 하지만 두 가지 다 작업 스레드를 만들고 run() 메서드에 작성된 코드를 처리하는 동일한 내부 동작 수행 
    → 여기서 첫 번째와 두 번째의 run() 은 다르다 첫 번째 run() 은 인터페이스, 두 번째는 Thread 클래스

 

🔎 스레드의 이름

➡️ 메인 스레드는 "main"이라는 이름 가지며 그 외에 추가로 생성한 스레드는 기본적으로 "Thread-0"이라는 이름 가짐

// Thread 이름 조회하기
public class ThreadExample3 {
    public static void main(String[] args) {

        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Get Thread Name");
            }
        });

        thread3.start();

        System.out.println("thread3.getName()  =  " + thread3.getName());
        // 출력값 : Get Thread Name thread3.getName()  =  Thread-0
    }
}
// Thread 이름 설정하기
public class ThreadExample4 {
    public static void main(String[] args) {

        Thread thread4 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Set And Get Thread Name");
            }
        });

        thread4.start();

        System.out.println("thread4.getName() = " + thread4.getName());

        thread4.setName("ChangeName!!!");

        System.out.println("thread4.getName() = " + thread4.getName());
        
        // 출력 결과
        // Set And Get Thread Name
        // thread4.getName() = Thread-0
        // thread4.getName() = ChangeName!!!
    }
}
// Thread 인스턴스의 주소값 얻기
public class ThreadExample5 {
    public static void main(String[] args) {
        // 두 메서드는 모두 Thread 클래스로부터 인스턴스화된 인스턴스의 메서드이므로 호출할 때 스레드 객체의 참조 필요
        // 실행 중인 스레드의 주소값을 사용해야 하는 상황이 발생한다면 Thread 클래스의 정적 메서드인 currentThread() 사용


        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()); // main
            }
        });

        thread1.start();
        System.out.println(Thread.currentThread().getName()); // Thread-0
    }
}

 

🔎 스레드의 동기화 

➡️ 멀티 스레드 프로세스의 경우, 두 스레드가 같은 데이터를 공유하게 되어 문제가 발생할 수 있음 

  • 임계 영역(Critical section)
    : 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역 
  • 락(Lock)
    : 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한 의미 

➡️ 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드 A 는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있음 

➡️ 스레드 A가 임계 영역 내의 코드를 모두 실행하면 락 반압, 이때 또 다른 스레드 중 하나가 락을 획득하여 임계 영역 내의 코드 실행할 수 있음

➡️ 특정 코드 구간을 임계 영역으로 설정할 때는 synchronized 키워드를 사용

 

2. JVM(Java Virtual Machine)

➡️ 자바 프로그램을 실행시키는 도구 
➡️ 즉, 자바로 작성한 소스 코드를 해석해 실행하는 별도의 프로그램 

📍 일반적인 프로그램 실행 과정
1. 프로그램이 실행되기 위해서는 CPU, 메모리, 각종 입출력 장치 등과 같은 컴퓨터 자원을 프로그램이 할당받아야 함 
2. 프로그램이 자신이 필요한 컴퓨터 자원을 운영체제에게 주문하면 운영체제는 가용한 자원을 확인하고 프로그램이 실행되는 데에 필요한 컴퓨터 자원을 프로그램에게 할당 
→ 이때 프로그램이 운영체제에게 필요한 컴퓨터 자원을 요청하는 방식이 운영체제마다 다름 
⭐️ 프로그래밍 언어가 운영체제에 대해 종속성을 가지는 이유!

➡️ 하지만 자바는 JVM을 매개해서 운영체제와 소통 → JVM이 자바와 운영체제 사이의 일종의 통역가 역할 

➡️ JVM은 각 운영체제에 맞게 개발되어 있으며 자바 소스 코드를 운영체제에 맞게 변환해 줌(→ 자바가 운영체제로부터 독립적인 이유)

 

🔎 JVM의 구조 

JVM의 구조

➡️ 자바 소스 코드를 작성하고 실행하면 일어나는 일 

  • 제일 먼저 컴파일러가 실행되면서 컴파일 진행, 그 결과로 .java 확장자를 가졌던 자바 소스 코드가 .class 확장자를 가진 바이트 코드 파일로 변환
  • JVM은 운영체제로부터 소스 코드 실행에 필요한 메모리 할당받음 → Runtime Data Area
  • Class Loader가 바이트 코드 파일을 JVM 내부로 불러들여 Runtime Data Area에 넣음 
    → 자바 소스코드를 메모리에 로드
  • 로드가 완료되면 실행 엔진(Execution Engine)이 Runtime Data Area에 있는 바이트 코드 파일 실행 
    • 인터프리터를 통해 코드를 한 줄씩 기계어로 번역하고 실행시키기
    • JIT Compiler(Just-In-Time)를 통해 바이트 코드 전체를 기계어로 번역하고 실행시키기
📍 JIT Cimpiler?
- JVM에서 바이트 코드를 실행하기 위해서 바이트 코드를 기계어로 변환하는 단계를 하나 더 거쳐야 함
    → 이때 기계어로 번역해 주는 것 JIT Compiler
- 실행 시점 전에 기계어로 변환하는 컴파일러 : 정적 컴파일러(오래 걸리지만 런타임 성능 좋음)
- 실행 중 기계어로 변환하는 컴파일러 : 동적 컴파일러(프로그램 성능 떨어짐)
- 정적 컴파일러와 동적 컴파일러의 한계점을 극복하기 위해 설계된 컴파일러 
➡️ 실행 엔진은 기본 적으로 인터프리터 통해 바이트 코드 실행시키다가 특정 바이트 코드가 자주 실행되면 JIT Compiler 통해 실행
➡️ 어떤 바이트 코드가 등장할 때 인터프리터는 해당 바이트 코드를 해석하고 실행하지만 JIT Compiler가 동작하면 한번에 바이트 코드 해석하고 실행해줌 

 

🔎 Stack 과 Heap

➡️ JVM 메모리 구조 

 

[Java] 메모리 사용 영역

🧑🏻‍💻 자바에서 사용하는 메모리 영역에 대해 간단히 정리 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받고, 그 메모리를 용도에 따라 여러 영역으로 나누어 관리한다. 🔎 JVM(Java Vir

reeeemind.tistory.com

➡️ JVM에 자바 프로그램이 로드되어 실행될 때 특정 값 및 바이트코드, 객체, 변수 등과 같은 데이터들이 메모리에 저장되어야 함 
    이러한 정보를 담는 메모리 영역이  Runtime Data Area

 

✍🏻 Stack

➡️ 일종의 자료구조(※ 자료구조 : 프로그램이 데이터를 저장하는 방식)

➡️ LIFO(Last In First Out) : 마지막에 들어간 데이터가 가장 먼저 나온다
    💡즉, 맨 마지막에 들어온 데이터가 가장 먼저 나가는 구조, LIFO는 스택의 데이터 입출력 순서를 나타내는 원칙 

 

✍🏻 Heap

➡️ JVM이 작동되면 영역 자동 설정

➡️ 객체나 인스턴스 변수, 배열이 저장됨

➡️ 객체는 대부부분 일회성이며 메모리에 남아있는 기간이 짧다는 전제로 설계되어 있음 

Person person = new Person();
  • new Person() 실행되면 Heap 영역에 인스턴스 생성되며 인스턴스가 생성된 위치의 주소값을 person에게 할당해주는데 이 person 은 Strack 영역에 선언된 변수
  • 우리가 객체를 다룬다는 것은 Stack 영역에 저장되어 있는 참조 변수를 통해 Heap 영역에 존재하는 객체를 다룬다는 의미
  • Heap 영역은 실제 객체의 값이 저장되는 공간

 

3. Garbage Collection

➡️ 메모리를 자동으로 관리하는 프로세스

➡️ 프로그램에서 더이상 사용하지 않는 객체를 찾아 삭제하거나 제거하여 메모리 확보함

➡️ 아무한테도 참조되지 않고 있지 않은 객체 및 변수들을 검색하여 메모리에서 점유를 해제하며 메모리 공간을 확보하여 효율적으로 메모리를 사용할 수 있게 해줌 

➡️ 객체가 얼마나 살아있냐에 따라서 힙 영역 안에서 영역을 나눔 Young/ Old

  • Young 영역 
    : 새롭게 생성된 객체가 할당되는 곳, 많은 객체가 생성되었다 사라짐
      이 영역에서 활동하는 가비지 컬렉터 → Minor GC
  • Old 영역
    : Young 영역에서 상태를 유지하고 살아남은 객체들이 복사되는 곳, Young 영역보다 크게 할당되고 크기가 큰 만큼 가비지는 적게 발생 
      이 영역에서 활동하는 가비지 컬렉터 → Major GC
  • 기본적으로 가비지 컬렉션이 실행될 때의 단계
    1. Stop The World
      : 가비지 컬렉션을 실행시키기 위해 JVM이 애플리케이션의 실행을 멈추는 작업 
        가비지 컬렉션이 실행될 때 가비지 컬렉션을 실행하는 스레드를 제외한 모든 스레드의 작업은 중단되고 가비지 정리가 완료되면 재개 
    2. Mark and Sweep
      : Mark 는 사용되는 메모리와 사용하지 않는 메모리를 식별하는 작업 
        Sweap은 Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업(제거)

들어보긴 했었는데 잘 몰랐던 개념들을 다시 알 수 있는 시간이었다. JVM 구조는 저번에 공부해 놓은 게 있어서 이해가 쉬웠고 스레드와 가비지 컬렉터는 아직은 낯선 것 같다. 그래도 앞에서 람다식이랑 스트림 하는 것보다 재밌었다......😂  오늘의 정리 끝! 

BELATED ARTICLES

more