[코드스테이츠 백엔드 44기 SEB BE] 19일차

오태호·2023년 3월 11일
0

코드스테이츠

목록 보기
17/22
post-thumbnail

스레드(Thread)

  • 프로세스
    • 실행 중인 어플리케이션
    • 즉, OS에게 메모리를 할당 받은 어플리케이션
      • 어플리케이션을 실행하면 OS가 실행에 필요한 메모리를 할당한다 -> 이를 할당 받아 프로세스가 된다
    • 데이터, 컴퓨터 자원, 스레드로 구성된다
  • 스레드
    • 프로세스 내에서 실행되는 소스 코드의 실행 흐름
    • 스레드는 데이터와 어플리케이션이 확보한 자원을 활용하여 소스 코드를 실행한다

메인 스레드(Main Thread)

  • main 메서드를 실행시켜준다
    • main 메서드 : 자바 어플리케이션을 실행하면 가장 먼저 실행되는 메서드
  • main 메서드의 코드를 처음부터 끝까지 순차적으로 실행시키며, 코드의 끝을 만나거나 return을 만나면 실행을 종료한다
  • 자바 어플리케이션이 싱글 스레드로 작성되었다면, 어플리케이션이 실행되어 프로세스가 될 때 오로지 메인 스레드만 가지는 싱글 스레드 프로세스가 된다

멀티 스레드(Multi-Thread)

  • 멀티 스레드 프로세스
    • 하나의 프로세스가 여러 개의 스레드를 가지는 프로세스
  • 멀티 스레딩
    • 여러 개의 스레드가 동시에 작업을 수행
    • 하나의 어플리케이션 내에서 여러 작업을 동시에 수행하는 멀티 태스킹을 구현하는 데에 핵심적인 역할을 수행한다

스레드 생성과 실행

작업 스레드 생성과 실행

  • 메인 스레드 외의 별도의 작업 스레드를 활용한다는 것은, 작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행시키는 것을 의미한다
  • 자바에서는 코드는 클래스 안에 작성되어야 한다
    • 따라서, 스레드가 수행할 코드도 클래스 내부에 작성해주어야 하며, run() 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어져 있다
  • run() 메서드
    • Runnable 인터페이스와 Thread 클래스에 정의되어져 있다
    • 작업 스레드를 생성하고 실행하는 방법에는 두 가지가 있다
      1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
      2. Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

  • 임의의 클래스를 만들고, Runnable 인터페이스를 구현하도록 한다
    • Runnable에는 run()이 정의되어 있으므로 반드시 run()을 구현해야 한다
public class ThreadExample {
    public static void main(String[] args) {

    }
}

// Runnable 인터페이스를 구현하는 클래스
class Thread1 implements Runnable {
    public void run() {

    }
}
  • run() 메서드 바디에 새롭게 생성된 작업 스레드가 수행할 코드를 적는다
public class ThreadExample {
    public static void main(String[] args) {

    }
}

// Runnable 인터페이스를 구현하는 클래스
class Thread1 implements Runnable {
    // run() 메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.println(count);
        }
    }
}
  • Runnable 인터페이스를 구현한 객체를 활용하여 스레드를 생성할 때, Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화한다
public class ThreadExample {
    public static void main(String[] args) {
        // Runnable 인터페이스를 구현한 객체 생성
        Runnable runnable = new Thread1();

        // Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화하여 스레드를 생성
        Thread thread = new Thread(runnable);

        // Thread thread = new Thread(new Thread1());
    }
}

class Thread1 implements Runnable {
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.println(count);
        }
    }
}
  • run() 메서드 내부 코드를 실행하기 위해 start() 메서드를 호출하여 스레드를 실행시켜준다
public class ThreadExample {
    public static void main(String[] args) {
        Runnable runnable = new Thread1();
        Thread thread = new Thread(runnable);

        // 작업 스레드를 실행시켜, run() 내부 코드를 처리하도록 한다
        thread.start();
    }
}

class Thread1 implements Runnable {
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.println(count);
        }
    }
}
  • main 메서드에 반복문을 추가한 후 코드를 실행해보면 아래와 같은 결과가 나타난다
public class ThreadExample {
    public static void main(String[] args) {
        Runnable runnable = new Thread1();
        Thread thread = new Thread(runnable);

        thread.start();

        for(int idx = 0; idx < 100; idx++) {
            System.out.print('*');
        }
    }
}

class Thread1 implements Runnable {
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.print(count);
        }
    }
}
// 결과값
*01234567**************************************************************8*************************************9101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  • 결과값을 보면 *과 숫자가 번갈아가며 나오고 있다
    • 이 결과는 실행할 때마다 다를 수 있다
    • *은 메인 스레드의 반복문 코드 실행에 의해 출력되고 숫자는 run() 메서드의 반복문에 의해, 즉 작업 스레드의 반복문 코드 실행에 의해 출력된다
    • 메인 스레드와 작업 스레드가 동시에 병렬로 실행되면서 각각의 코드를 실행시켰기 때문에 두 가지가 섞여 나온다

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

  • Thread 클래스를 상속 받는 하위 클래스를 만든다
    • Thread 클래스에는 run() 메서드가 정의되어져 있어 run()을 오버라이딩해준다
public class ThreadExample {
    public static void main(String[] args) {

    }
}

class Thread2 extends Thread {
    public void run() {

    }
}
  • run() 메서드 바디에 생성될 스레드가 수행할 작업을 작성한다
public class ThreadExample {
    public static void main(String[] args) {

    }
}

class Thread2 extends Thread {
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.print(count);
        }
    }
}
  • 스레드를 생성한다
    • Thread 클래스를 직접 인스턴스화하지 않는다
    • Thread 클래스를 상속받은 하위 클래스를 인스턴스화하여 스레드를 생성한다
public class ThreadExample {
    public static void main(String[] args) {
        Thread2 thread = new Thread2();
    }
}

class Thread2 extends Thread {
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.print(count);
        }
    }
}
  • main 메서드에 반복문을 추가한 후에 스레드를 start() 메서드를 통해 실행시킨다
public class ThreadExample {
    public static void main(String[] args) {
        Thread2 thread = new Thread2();

        thread.start();

        for(int idx = 0; idx < 100; idx++) {
            System.out.print('*');
        }
    }
}

class Thread2 extends Thread {
    public void run() {
        for(int count = 0; count < 100; count++) {
            System.out.print(count);
        }
    }
}
  • 위 두 방법 모두 작업 스레드를 만들고, run() 메서드에 작성된 코드를 처리하는 동일한 내부 동작을 수행한다

익명 객체를 사용하여 스레드 생성하고 실행하기

  • 스레드가 수행할 동작은 run() 메서드의 바디에 작성하며, 자바는 객체지향 언어이므로 클래스 안에 코드를 작성해야 한다
  • 그러나, 클래스를 따로 정의하지 않고 익명 객체를 이용해 스레드를 생성하고 실행시킬 수 있다

Runnable 익명 구현 객체를 활용한 스레드 생성 및 실행

public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                for(int count = 0; count < 100; count++) {
                    System.out.print(count);
                }
            }
        });

        thread.start();

        for(int idx = 0; idx < 100; idx++) {
            System.out.print('*');
        }
    }
}

Thread 익명 하위 객체를 활용한 스레드 생성 및 실행

public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            public void run() {
                for(int count = 0; count < 100; count++) {
                    System.out.print(count);
                }
            }
        };

        thread.start();

        for(int idx = 0; idx < 100; idx++) {
            System.out.print('*');
        }
    }
}

스레드의 이름

  • 메인 스레드 : "main"이라는 이름을 가진다
  • 추가적으로 생성한 스레드 : 기본적으로 "Thread-n"이라는 이름을 가진다

스레드의 이름 조회

  • {스레드의참조값}.getName()으로 스레드의 이름을 조회할 수 있다
public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println("Get Thread Name Example");
            }
        });

        thread.start();

        System.out.println("thread.getName(): " + thread.getName());
    }
}
// 결과값
Get Thread Name Example
thread.getName(): Thread-0

스레드의 이름 설정

  • {스레드의참조값}.setName()으로 스레드의 이름을 설정할 수 있다
public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println("Set Thread Name Example");
            }
        });

        thread.start();

        System.out.println("thread.getName(): " + thread.getName());

        thread.setName("newThread");

        System.out.println("thread.getName(): " + thread.getName());
    }
}
// 결과값
Set Thread Name Example
thread.getName(): Thread-0
thread.getName(): newThread

스레드 인스턴스의 주소값 얻기

  • 실행 중인 스레드의 주소값을 사용해야 하는 상황이 발생한다면 Thread 클래스의 정적 메서드 currentThread()를 사용한다
    • 스레드의 이름을 조회하거나 설정할 때, 두 메서드 모두 Thread 클래스로부터 인스턴스화된 인스턴스의 메서드이므로, 호출할 때에 스레드 객체의 참조가 필요하다
public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });

        thread.start();
        System.out.println(Thread.currentThread().getName());
    }
}
// 결과값
main
Thread-0

스레드의 동기화

  • 프로세스
    • 자원, 데이터, 스레드로 구성되어 있다
    • 프로세스는 스레드가 OS로부터 자원을 할당받아 소스 코드를 실행하여 데이터를 처리한다
  • 멀티 스레드 프로세스는 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생할 수 있다!
class Acount {
    private int balance = 5000;

    public int getBalance() {
        return balance;
    }

    public boolean withdraw(int money) {
        if(balance >= money) {
            try { Thread.sleep(1000); }
            catch(Exception e) { e.printStackTrace(); }

            balance -= money;
            return true;
        }
        return false;
    }
}

class Thread implements Runnable {
    Account account = new Account();

    public void run() {
        while(account.getBalance() > 0) {
            int money = (int)(Math.random() * 3 + 1) * 100;

            boolean isDeny = !account.withdraw(money);

            System.out.println(String.format("Withdraw %d₩ By %s, Balance : %d %s", money, Thread.currentThread().getName(), account.getBalance(), isDeny ? "-> DENIED" : ""));
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        Runnable runnable = new Thread();
        Thread thread1 = new Thread(runnable);
        THread thread2 = new Thread(runnable);

        thread1.setName("홍길동");
        thread1.setName("김철수");

        thread1.start();
        thread2.start();
    }
}
  • 위 코드를 실행하면 두 개의 작업 스레드가 생성되며, 두 작업 스레드는 Account 객체를 공유한다
  • 그래서 위 코드를 실행하면 인출금 및 잔액이 제대로 출력되지 않는다
    • 두 스레드 사이에 같은 객체를 공유하고 있기 때문에 오류가 발생한다
  • withdraw()에서 잔액이 인출하고자 하는 금액보다 작은 경우에만 인출하도록 작성하였지만 음수의 잔액이 발생한다
    • 두 스레드가 Account 객체를 공유하는 상황에서, 한 스레드가 if문의 조건식을 true로 평가하여 if문의 실행부로 코드의 흐름이 이동하는 시점에 다른 스레드가 끼어들어 balance를 인출했기 때문에 음수 잔액이 발생한다!
  • 위와 같은 문제가 발생하지 않도록 스레드 동기화를 진행한다!

임계 영역(Critical section)과 락(Lock)

  • 임계 영역
    • 하나의 스레드만 코드를 실행할 수 있는 코드 영역
    • 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다
  1. 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다
  2. 임의의 스레드가 임계 영역 내의 코드를 모두 실행하면 락을 반납한다
    • 이 때부터 다른 스레드들 중 하나가 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다!
class Acount {
    private int balance = 5000;

    public int getBalance() {
        return balance;
    }

    public boolean withdraw(int money) {
        if(balance >= money) {
            try { Thread.sleep(1000); }
            catch(Exception e) { e.printStackTrace(); }

            balance -= money;
            return true;
        }
        return false;
    }
}

class Thread implements Runnable {
    Account account = new Account();

    public void run() {
        while(account.getBalance() > 0) {
            int money = (int)(Math.random() * 3 + 1) * 100;

            boolean isDeny = !account.withdraw(money);

            System.out.println(String.format("Withdraw %d₩ By %s, Balance : %d %s", money, Thread.currentThread().getName(), account.getBalance(), isDeny ? "-> DENIED" : ""));
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        Runnable runnable = new Thread();
        Thread thread1 = new Thread(runnable);
        THread thread2 = new Thread(runnable);

        thread1.setName("홍길동");
        thread1.setName("김철수");

        thread1.start();
        thread2.start();
    }
}
  • 이 예제를 생각해보면 우리에게 필요한 것은 두 스레드가 동시에 실행하면 안되는 부분을 설정해야 한다(withdraw() 메서드)
    • withdraw() 메서드 부분을 임계 영역으로 설정해야 한다
    • 특정 코드 구간을 임계 영역으로 설정할 때에는 synchronized 키워드를 사용한다

1. 메서드 전체를 임계 영역으로 지정

  • 메서드 전체를 임계 영역으로 지정하면 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락을 얻는다
public synchronized boolean withdraw(int money) {
    if(balance >= money) {
        try { Thread.sleep(1000); }
        catch(Exception e) { e.printStackTrace(); }

        balance -= money;
        return true;
    }
    return false;
}

2. 특정한 영역을 임계 영역으로 지정하기

  • 임계 영역으로 설정한 블럭의 코드로 코드 실행 흐름이 진입할 때, 해당 코드를 실행하고 있는 스레드가 this에 해당하는 객체의 락을 얻고, 배타적으로 임계 영역 내의 코드를 실행한다
public boolean withdraw(int money) {
    synchronized(this) {
        if(balance >= money) {
            try { Thread.sleep(1000); }
            catch(Exception e) { e.printStackTrace(); }

            balance -= money;
            return true;
        }
        return false;
    }
}

스레드의 상태 및 실행 제어

  • start() : 스레드의 상태를 실행 대기 상태로 만들어주는 메서드
    • 스레드가 실행 대기 상태가 되면 OS가 적절한 때에 스레드를 실행시켜준다
  • 즉, 스레드는 상태라는 것이 존재하고, 스레드의 상태를 바꿔주는 메서드가 존재한다!

스레드의 상태 및 실행 제어 메서드 요약

sleep(long milliSecond)

  • milliSecond 동안 스레드를 잠시 멈춘다
static void sleep(long milliSecond)
  • 우리가 지정한 시간만큼 정확히 스레드가 중지되는 것은 아니고 약간의 오차를 가진다
  • sleep()을 호출하면 해당 스레드의 상태가 일시 정지(TIMED_WAITING) 상태로 전환된다
  • 이 때, 다음과 같은 상황에서 실행 대기 상태로 복귀한다
    1. 인자로 전달한 시간만큼의 시간이 경과한 경우
    2. interrupt()를 호출한 경우
      • interrupt()가 호출되면 기본적으로 예외가 발생하므로 반드시 예외 처리를 해줘야 한다!

interrupt()

  • 일시 중지 상태인 스레드를 실행 대기 상태로 복귀시킨다
void interrupt()
  • 일시 정지 상태로 보내는 메서드들
    • sleep(), wait(), join()
    • 이렇게 일시 정지가 되면 코드의 흐름은 sleep(), wait(), join()에서 멈춰있다
  • 멈춰있지 않은 스레드에서 일시 정지 상태로 보내는 메서드들에 의해 멈춰있는 스레드로 interrupt()를 호출하면 sleep(), wait(), join() 메서드들에서 예외가 발생하며, 그에 따라 일시 정지가 풀린다!

yield()

  • 다른 스레드에게 자신의 실행 시간을 양보하는 메서드
static void yield()
  • 스레드에게 반복적인 작업을 실행시키는 경우가 많은데 반복문의 순회가 필요하지 않을 때가 존재한다
    • 이럴 때 yield()를 사용할 수 있다

join()

  • 다른 스레드의 작업이 끝날 때까지 자신을 일시 중지 상태로 만드는 상태 제어 메서드
void join()
void join(long milliSecond)
  • 실행 대기 상태로 복귀하는 조건
    1. 인자로 전달한만큼의 시간이 경과
    2. interrupt()가 호출됨
    3. join() 호출 시 지정했던 다른 스레드가 모든 작업을 마침
  • join() vs sleep()
    • 유사점
      • 해당 스레드는 일시 중지 상태가 된다
      • try-catch문을 통해 예외 처리가 필요하다
      • interrupt()에 의해 실행 대기 상태로 복귀할 수 있다
    • 차이점
      • sleep() : Thread의 static 메서드
      • join() : 특정 스레드에 대해 동작하는 인스턴스 메서드

wait(), notify()

  • 두 스레드가 교대로 작업을 처리해야 할 경우가 존재한다
    • 이 때 wait(), notify() 상태 제어 메서드를 사용한다
  • Ex. 스레드1과 스레드2가 공유 객체를 두고 협업한다
    1. 먼저 스레드1이 공유 객체에 대해 작업을 완료한다
    2. 스레드2와 교대하기 위해 notify()를 호출한다
    3. notify()가 호출되면 스레드2가 실행 대기 상태가 되고, 곧 실행된다
    4. 이어서 스레드1은 wait()을 호출하여 일시 정지 상태가 된다
    5. 스레드2가 작업을 완료하면 notify()를 호출하여 작업을 중단하고 스레드1이 실행 대기 상태로 복귀한다
    6. 스레드2가 wait()을 호출하여 일시 정지 상태가 된다
    7. 위 과정이 반복되며 공유 객체에 대해 서로 배타적으로 접근하면서 효과적으로 협업할 수 있다
public class ThreadExample {
    public static void main(String[] args) {
        SharedObject sharedObject = new SharedObject();

        Thread1 thread1 = new Thread1(sharedObject);
        Thread2 thread2 = new Thread2(sharedObject);

        thread1.start();
        thread2.start();
    }
}

class SharedObject {
    public synchronized void method1() {
        System.out.println("Thread1이 작동 중...");
        notify();
        try { wait(); } catch(Exception e) { e.printStackTrace(); }
    }

    public synchronized void method2() {
        System.out.println("Thread2가 작동 중...");
        notify();
        try { wait(); } catch(Exception e) { e.printStackTrace(); }
    }
}

class Thread1 extends Thread {
    private SharedObject sharedObject;

    public Thread1(SharedObject sharedObject) {
        this.sharedObject = sharedObject;
    }

    public void run() {
        for(int count = 0; count < 10; count++)
            sharedObject.method1();
    }
}

class Thread2 extends Thread {
    private SharedObject sharedObject;

    public Thread2(SharedObject sharedObject) {
        this.sharedObject = sharedObject;
    }

    public void run() {
        for(int count = 0; count < 10; count++)
            sharedObject.method2();
    }
}

JVM

JVM이란?

  • JVM(Java Virtual Machine)
    • 자바 프로그램을 실행시키는 도구이다
    • 즉, 자바로 작성한 소스 코드를 해석해서 실행하는 별도의 프로그램이다!
  • 자바는 JVM을 통해 OS와 소통한다
    • 프로그램이 실행될 때, OS에게 필요한 컴퓨팅 자원을 할당받는다
    • 그런데, 프로그램이 OS에게 컴퓨팅 자원을 요청하는 방식이 OS마다 다르다!
      • 이로 인해 프로그래밍 언어가 OS에 대해 종속성을 가지게 된다
    • 그러나 JVM이 자바 프로그램과 OS 사이를 매개해주기 때문에 자바는 운영체제에 독립적이다!
      • JVM은 각 운영체제에 적합한 버전이 존재한다 (Ex. Windows용, MacOS 용, Linux용 JVM)
      • OS에 맞는 JVM을 설치해주면 자바 소스 코드를 OS에 맞게 변환해 실행시켜준다

JVM 구조

  • 자바로 소스 코드를 작성하고 실행하면 일어나는 일
    1. 소스 코드를 작성하고 실행하면, 컴파일러가 먼저 실행되면서 컴파일을 진행한다
    2. 컴파일 결과로 .java 확장자의 소스 코드가 .class 확장자의 바이트 코드 파일로 변환된다
    3. JVM은 OS로부터 소스 코드 실행에 필요한 메모리를 할당받는다
      • 위 그림 상의 Runtime Data Area
    4. Class Loader가 바이트 코드 파일을 JVM 내부로 불러들여 Runtime Data Area에 적재시킨다
      • 자바 소스 코드를 메모리에 로드시키는 것
    5. 로드가 완료되면 실행 엔진(Execution Engine)이 Runtime Data Area에 적재된 바이트 코드를 실행시킨다
      • 이 때, 실행 엔진은 두 가지 방식으로 바이트 코드를 실행시킨다
        1. 인터프리터(Interpreter)를 통해 바이트 코드 전체를 기계어로 번역하고 실행시킨다
        2. JIT(Just-In-Time) Compiler를 통해 바이트 코드 전체를 기계어로 번역하고 실행시킨다
      • 실행 엔진은 기본적으로 1번 방식으로 통해 바이트 코드를 실행시키다가, 특정 바이트 코드가 자주 실행된다면 해당 바이트 코드를 JIT Compiler를 통해 실행시킨다
    • 중복적으로 어떤 바이트 코드가 등장할 때
      • 인터프리터는 매번 해당 바이트 코드를 해석하고 실행
      • JIT Compiler가 동작하면 한 번에 바이트 코드를 해석하고 실행

Stack과 Heap

  • JVM에 Java 프로그램이 로드되어 실행될 때, 특정 값 및 바이트 코드, 객체, 변수 등 여러 데이터들이 메모리에 저장되어야 한다
    • Runtime Data Area가 이러한 정보를 담는 메모리 영역이다!
    • 크게 5가지 영역으로 구분되어 있다

Stack 영역이란?

  • 스택(Stack)
    • LIFO(Last In First Out)의 특징을 가진다
      • 마지막에 들어간 데이터가 가장 먼저 나온다
  • JVM 내부에서 Stack의 작동
    • 메서드가 호출되면 메서드를 위한 공간인 Method Frame이 생성된다
      • 메서드 내부에는 참조 변수, 매개 변수, 지역 변수, 반환값 및 연산 시 일어나는 값들 등 다양한 값들이 임시로 저장된다
      • 이러한 것들이 Method Frame에 저장된다
    • Method Frame이 Stack에 호출되는 순서대로 쌓이게 되는데, Method 동작이 완료되면 역순으로 제거된다

Heap 영역이란?

  • 객체나 인스턴스 변수, 배열이 저장되는 영역
  • JVM에는 단 하나의 Heap 영역이 존재한다
    • JVM이 작동되면 자동으로 생성된다
Car car = new Car();
  • new Car();가 실행되면 Heap 영역에 인스턴스가 생성된다
  • 인스턴스가 생성된 위치의 주소값을 참조 변수 car에 할당한다
    • 참조 변수는 Stack 영역에 선언된다
  • 즉, Stack 영역에 저장되어 있는 참조 변수를 통해 Heap 영역에 존재하는 객체를 다룬다!
    • Heap 영역 -> 실제 객체의 값이 저장되는 공간

Garbage Collection

Garbage Collection이란?

  • 메모리를 자동으로 관리하는 프로세스를 말한다
    • 프로그램에서 더이상 사용하지 않는 객체를 찾아 삭제 / 제거하여 메모리를 확보하는 것을 말한다

동작 방식

  • JVM의 Heap 영역은 객체는 대부분 일회성이며, 메모리에 남아있는 기간이 대부분 짧다는 전제로 설계되어 있다
    • 그러므로 객체가 얼마나 살아있느냐에 따라 Young, Old 2개의 영역으로 나뉜다

  • Young 영역
    • 새롭게 생성된 객체가 할당되는 곳
    • 많은 객체가 생성되었다 사라지는 것을 반복한다
    • Minor GC
      • Young 영역에서 활동하는 가비지 컬렉터
  • Old 영역
    • Young 영역에서 상태를 유지하고 살아남은 객체들이 복사되는 곳
    • 보통 Young 영역보다 크게 할당되고, 크기가 큰 만큼 가비지는 적게 발생한다
    • Major GC
      • Old 영역에서 활동하는 가비지 컬렉터
  • 기본적으로 Garbage Collection이 실행될 때 다음의 2가지 단계를 따른다
    1. Stop The World
      • Garbage Collection를 실행시키기 위해 JVM이 어플리케이션의 실행을 멈추는 작업
      • Garbage Collection이 실행될 때, Garbage Collection을 실행하는 스레드를 제외한 모든 스레드들의 작업은 중단되고, Garbage 정리가 완료되면 재개된다
    2. Mark and Sweep
      • Mark
        • 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
      • Sweep
        • Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
    • Stop The World를 통해 모든 작업이 중단되면, Garbage Collection이 모든 변수와 객체를 탐색해서 어떤 객체를 참조하고 있는지 확인한다
    • 이후, 사용되고 있는 메모리를 식별해서(Mark) 사용되지 않는 메모리는 제거(Sweep)하는 과정을 진행한다
profile
자바, 웹 개발을 열심히 공부하고 있습니다!

0개의 댓글