스레드(Thread)
- 프로세스
- 실행 중인 어플리케이션
- 즉, OS에게 메모리를 할당 받은 어플리케이션
- 어플리케이션을 실행하면 OS가 실행에 필요한 메모리를 할당한다 -> 이를 할당 받아 프로세스가 된다
- 데이터, 컴퓨터 자원, 스레드로 구성된다
- 스레드
- 프로세스 내에서 실행되는 소스 코드의 실행 흐름
- 스레드는 데이터와 어플리케이션이 확보한 자원을 활용하여 소스 코드를 실행한다
메인 스레드(Main Thread)
- main 메서드를 실행시켜준다
- main 메서드 : 자바 어플리케이션을 실행하면 가장 먼저 실행되는 메서드
- main 메서드의 코드를 처음부터 끝까지 순차적으로 실행시키며, 코드의 끝을 만나거나 return을 만나면 실행을 종료한다
- 자바 어플리케이션이 싱글 스레드로 작성되었다면, 어플리케이션이 실행되어 프로세스가 될 때 오로지 메인 스레드만 가지는 싱글 스레드 프로세스가 된다
멀티 스레드(Multi-Thread)
- 멀티 스레드 프로세스
- 하나의 프로세스가 여러 개의 스레드를 가지는 프로세스
- 멀티 스레딩
- 여러 개의 스레드가 동시에 작업을 수행
- 하나의 어플리케이션 내에서 여러 작업을 동시에 수행하는 멀티 태스킹을 구현하는 데에 핵심적인 역할을 수행한다
스레드 생성과 실행
작업 스레드 생성과 실행
- 메인 스레드 외의 별도의 작업 스레드를 활용한다는 것은, 작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행시키는 것을 의미한다
- 자바에서는 코드는 클래스 안에 작성되어야 한다
- 따라서, 스레드가 수행할 코드도 클래스 내부에 작성해주어야 하며, run() 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어져 있다
- run() 메서드
- Runnable 인터페이스와 Thread 클래스에 정의되어져 있다
- 작업 스레드를 생성하고 실행하는 방법에는 두 가지가 있다
- Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
- Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
- 임의의 클래스를 만들고, Runnable 인터페이스를 구현하도록 한다
- Runnable에는 run()이 정의되어 있으므로 반드시 run()을 구현해야 한다
public class ThreadExample {
public static void main(String[] args) {
}
}
class Thread1 implements Runnable {
public void run() {
}
}
- run() 메서드 바디에 새롭게 생성된 작업 스레드가 수행할 코드를 적는다
public class ThreadExample {
public static void main(String[] args) {
}
}
class Thread1 implements Runnable {
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 = new Thread1();
Thread thread = new Thread(runnable);
}
}
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);
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)
- 임계 영역
- 하나의 스레드만 코드를 실행할 수 있는 코드 영역
- 락
- 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다
- 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다
- 임의의 스레드가 임계 영역 내의 코드를 모두 실행하면 락을 반납한다
- 이 때부터 다른 스레드들 중 하나가 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다!
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) 상태로 전환된다
- 이 때, 다음과 같은 상황에서 실행 대기 상태로 복귀한다
- 인자로 전달한 시간만큼의 시간이 경과한 경우
- interrupt()를 호출한 경우
- interrupt()가 호출되면 기본적으로 예외가 발생하므로 반드시 예외 처리를 해줘야 한다!
interrupt()
- 일시 중지 상태인 스레드를 실행 대기 상태로 복귀시킨다
void interrupt()
- 일시 정지 상태로 보내는 메서드들
- sleep(), wait(), join()
- 이렇게 일시 정지가 되면 코드의 흐름은 sleep(), wait(), join()에서 멈춰있다
- 멈춰있지 않은 스레드에서 일시 정지 상태로 보내는 메서드들에 의해 멈춰있는 스레드로 interrupt()를 호출하면 sleep(), wait(), join() 메서드들에서 예외가 발생하며, 그에 따라 일시 정지가 풀린다!
yield()
- 다른 스레드에게 자신의 실행 시간을 양보하는 메서드
static void yield()
- 스레드에게 반복적인 작업을 실행시키는 경우가 많은데 반복문의 순회가 필요하지 않을 때가 존재한다
join()
- 다른 스레드의 작업이 끝날 때까지 자신을 일시 중지 상태로 만드는 상태 제어 메서드
void join()
void join(long milliSecond)
- 실행 대기 상태로 복귀하는 조건
- 인자로 전달한만큼의 시간이 경과
- interrupt()가 호출됨
- join() 호출 시 지정했던 다른 스레드가 모든 작업을 마침
- join() vs sleep()
- 유사점
- 해당 스레드는 일시 중지 상태가 된다
- try-catch문을 통해 예외 처리가 필요하다
- interrupt()에 의해 실행 대기 상태로 복귀할 수 있다
- 차이점
- sleep() : Thread의 static 메서드
- join() : 특정 스레드에 대해 동작하는 인스턴스 메서드
wait(), notify()
- 두 스레드가 교대로 작업을 처리해야 할 경우가 존재한다
- 이 때 wait(), notify() 상태 제어 메서드를 사용한다
- Ex. 스레드1과 스레드2가 공유 객체를 두고 협업한다
- 먼저 스레드1이 공유 객체에 대해 작업을 완료한다
- 스레드2와 교대하기 위해 notify()를 호출한다
- notify()가 호출되면 스레드2가 실행 대기 상태가 되고, 곧 실행된다
- 이어서 스레드1은 wait()을 호출하여 일시 정지 상태가 된다
- 스레드2가 작업을 완료하면 notify()를 호출하여 작업을 중단하고 스레드1이 실행 대기 상태로 복귀한다
- 스레드2가 wait()을 호출하여 일시 정지 상태가 된다
- 위 과정이 반복되며 공유 객체에 대해 서로 배타적으로 접근하면서 효과적으로 협업할 수 있다
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 구조
- 자바로 소스 코드를 작성하고 실행하면 일어나는 일
- 소스 코드를 작성하고 실행하면, 컴파일러가 먼저 실행되면서 컴파일을 진행한다
- 컴파일 결과로 .java 확장자의 소스 코드가 .class 확장자의 바이트 코드 파일로 변환된다
- JVM은 OS로부터 소스 코드 실행에 필요한 메모리를 할당받는다
- 위 그림 상의 Runtime Data Area
- Class Loader가 바이트 코드 파일을 JVM 내부로 불러들여 Runtime Data Area에 적재시킨다
- 로드가 완료되면 실행 엔진(Execution Engine)이 Runtime Data Area에 적재된 바이트 코드를 실행시킨다
- 이 때, 실행 엔진은 두 가지 방식으로 바이트 코드를 실행시킨다
- 인터프리터(Interpreter)를 통해 바이트 코드 전체를 기계어로 번역하고 실행시킨다
- 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 영역이 존재한다
Car car = new Car();
- new Car();가 실행되면 Heap 영역에 인스턴스가 생성된다
- 인스턴스가 생성된 위치의 주소값을 참조 변수 car에 할당한다
- 즉, Stack 영역에 저장되어 있는 참조 변수를 통해 Heap 영역에 존재하는 객체를 다룬다!
- Heap 영역 -> 실제 객체의 값이 저장되는 공간
Garbage Collection
Garbage Collection이란?
- 메모리를 자동으로 관리하는 프로세스를 말한다
- 프로그램에서 더이상 사용하지 않는 객체를 찾아 삭제 / 제거하여 메모리를 확보하는 것을 말한다
동작 방식
- JVM의 Heap 영역은 객체는 대부분 일회성이며, 메모리에 남아있는 기간이 대부분 짧다는 전제로 설계되어 있다
- 그러므로 객체가 얼마나 살아있느냐에 따라 Young, Old 2개의 영역으로 나뉜다
- Young 영역
- 새롭게 생성된 객체가 할당되는 곳
- 많은 객체가 생성되었다 사라지는 것을 반복한다
- Minor GC
- Old 영역
- Young 영역에서 상태를 유지하고 살아남은 객체들이 복사되는 곳
- 보통 Young 영역보다 크게 할당되고, 크기가 큰 만큼 가비지는 적게 발생한다
- Major GC
- 기본적으로 Garbage Collection이 실행될 때 다음의 2가지 단계를 따른다
- Stop The World
- Garbage Collection를 실행시키기 위해 JVM이 어플리케이션의 실행을 멈추는 작업
- Garbage Collection이 실행될 때, Garbage Collection을 실행하는 스레드를 제외한 모든 스레드들의 작업은 중단되고, Garbage 정리가 완료되면 재개된다
- Mark and Sweep
- Mark
- 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
- Sweep
- Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
- Stop The World를 통해 모든 작업이 중단되면, Garbage Collection이 모든 변수와 객체를 탐색해서 어떤 객체를 참조하고 있는지 확인한다
- 이후, 사용되고 있는 메모리를 식별해서(Mark) 사용되지 않는 메모리는 제거(Sweep)하는 과정을 진행한다