자바의 메모리 구조
스레드와 프로세스를 배우기 앞서 자바의 메모리 구조를 살펴보자.
크게 3가지의 영역으로 나눌 수 있고, 각각의 영역에 대해 설명과 예시 코드를 들어보려고 한다.
메서드 영역: class 정보, static 변수가 저장된다.
Stack 영역: 지역 변수, 매개변수, 객체의 참조값이 저장된다.
Heap 영역: 객체, 배열, 인스턴스 변수(필드값)가 저장된다. GC의 대상이다.
공부를 해본 사람들은 알겠지만, 모르는 사람들도 있을 수 있기에 코드와 그림을 통해 조금 더 자세하게 설명하고자 한다.
public class Data {
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public class Example {
public static void main(String[] args) {
method1();
}
static void method1() {
Data data1 = new Data(10);
method2(data1);
}
static void method2(Data data2) {
System.out.println(data2.getValue());
}
}
이 예시 코드를 기반으로 어떤 순서대로 메모리에 저장되는지 그림을 통해 살펴보자.
1) Stack 영역에 메인 메서드가 저장된다.
2) method1() 이 Stack 영역에 쌓이고, 지역변수 또한 Stack 영역에 저장된다.
3) method2()가 Stack 영역에 쌓이고, 지역 변수 또한 Stack 영역에 저장된다.
4. print문 출력 후 method2()가 종료된다.
System.out.println(data2.getValue());
// 10
5) method1()이 종료된다.
이때 Heap 영역에 있는 Data는 참조값이 전부 사라져 혼자 남았다. 이런 객체 및 필드값들은 GC의 대상이므로, 삭제된다.
6) main() 메서드 종료 후 Stack에 남아있는 작업이 없어 프로그램이 종료된다.
위와 같은 작업 흐름을 따른다. 이 예시에서는 메서드 영역에 대해서는 다루지 않았기에 간단하게 설명하고 넘어가자면,
public class Data {
public static int count;
public String name;
public Data(String name) {
this.name = name;
count++;
}
}
public class MainExample {
public static void main(String[] args) {
Data data1 = new Data("A");
System.out.println(data1.count);
}
}
static 변수인 count는 메서드 영역에서 관리되기에 위와 같은 그림이 된다. 만약에 data2 = x002를 하나 더 만들었다고 해도, 메서드 영역에서 관리가 되기에 값을 공유할 수 있는 것이다.
프로세스와 스레드
프로세스:
운영체제 안에서 실행 중인 프로그램
프로세스는 실행 환경과 자원을 제공하는 컨테이너 역할을 한다. 예를 들자면, 음악 듣기라는 작업단위를 프로세스가 제공해 주고, 스레드가 음악 실행, 가사 가져오기와 같은 세부적인 작업을 진행한다. 프로세스는 별도의 메모리 공간을 가지고 있어, 프로세스끼리 전혀 무관하다. 즉, 하나의 프로세스가 오작동을 한다면, 다른 프로세스에 영향을 미치지 않는다는 말이다.
스레드:
프로세스 내부에 있는 작은 작업 단위
하나의 프로세스의 여러 스레드가 존재할 수 있다. 스레드는 프로세스가 제공하는 메모리 공간을 공유한다. 실제 CPU를 사용하여 프로그램을 실행시키는 것은 스레드의 역할이다.
그림을 통해 스레드와 프로세스의 메모리 공간에 대해 살펴보자
프로세스의 메모리 구조다. JVM과 유사하다. Stack은 스레드에 포함이 된다. 아래 그림을 통해 멀티프로세스 환경에 대해 살펴보자.
각각의 프로세스들은 독립된 공간에 있는 것을 확인할 수 있다. 즉, 프로세스 별로 메모리가 할당되어있는것을 확인할 수 있는 반면, 스레드들은 프로세스 내부에서 공통된 자원을 사용하고 있는것을 확인할 수 있다.
멀티스레드 및 사용 이유
멀티스레드란 어떤 것일까?
하나의 프로세스도 그 안에서 여러 작업이 필요하다. 유튜브를 본다고 하면, 영상을 보는 동시에 댓글을 달 수 있어야 한다.
이걸 어떻게 같이 하냐? 영상을 보는 것과 댓글을 다는 것은 별도의 스레드이기 때문이다. 만약 단일 스레드를 사용한다면, 영상이 끝나고 나서, 다음 작업인 댓글 달기를 수행할 수 있는 것이다.
멀티스레드를 사용하는 간단한 예시를 하나 보자.
public class Example {
public static void main(String[] args) {
int sum = 0;
for (int i=1; i<=100; i++) {
sum += i;
}
System.out.println(sum);
}
}
//5050
1~100까지 더하고 출력하는 아주 간단한 코드다. 우리는 하나의 스레드가 1~100까지 전부 더한다는 사실을 알 수 있다.
이걸 반으로 나눠서 하나의 스레드가 1~50 계산을 진행하고, 하나의 스레드가 51~500 계산을 진행하고 두 개의 값을 더하면 어떨까?? 이론상 속도가 절반으로 빨라지지 않을까? (Context Switching 시간, 두 개의 값을 더하는 시간은 우선 고려하지 않겠다.)
import java.util.concurrent.*;
public class Example {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> result1 = es.submit(new ExampleTask(1, 50));
Future<Integer> result2 = es.submit(new ExampleTask(51, 100));
Integer calResult1 = result1.get();
Integer calResult2 = result2.get();
System.out.println(calResult1 + calResult2);
es.close();
}
static class ExampleTask implements Callable<Integer> {
private final int startNum;
private final int endNum;
private int sum = 0;
public ExampleTask(int startNum, int endNum) {
this.startNum = startNum;
this.endNum = endNum;
}
@Override
public Integer call() {
for (int i=startNum; i<=endNum; i++) {
sum +=i;
}
return sum;
}
}
}
해당 코드가 이해 안 되겠지만 간단히 설명하자면, Thread 2개를 생성해서 하나의 스레드는 1~50까지, 다른 하나의 스레드는 51~100까지 작업을 시킨 코드다. 멀티스레드의 활용도를 느끼기만 하면 좋을 것 같다.
다음 포스팅에서는 스레드의 상태, 문제점에 대해 살펴볼 계획이다. 참고로 필자는 인프런 김영한 선생님의 java 고급 1편을 보고 공부했다.
'java' 카테고리의 다른 글
Thread(3) - volatile, synchronize, lock (8) | 2025.01.19 |
---|---|
Thread(2) - 스레드의 상태 (6) | 2025.01.15 |
Java 컬렉션 정리 for 코딩테스트 - Queue (9) | 2024.12.25 |
Java 컬렉션 정리 for 코딩테스트 - Map (7) | 2024.12.23 |
Java 컬렉션 정리 for 코딩테스트 - Set (1) | 2024.12.22 |