
컴퓨터에서 프로그램이 실행되기까지 과정을 정리해보고자 한다. 개발자 입장에서 직접 작성한 고급언어 코드 프로그램이 어떻게 작동하게 되는지 알아보자. 운영체제별 차이점까지 깊게 다루진 않았다. 특히 Java 개발자 측면에서 찾아보며 정리해보았다.
C/C++ 실행 과정
소스 코드(.c, .cpp) -> 컴파일 -> 링킹 -> 실행 파일 -> 로딩 -> 프로세스 생성 -> 실행 -> 종료
Java 실행 과정
소스코드(.java) → javac → 바이트코드(.class) → JVM 로딩 → JVM 프로세스 → 바이트코드 실행
개발자는 C, C++, Java, Python 등 **고급 프로그래밍 언어로 사람이 이해할 수 있는 형태의 소스 코드를 작성한다.
이 소스코드는 컴파일러 에 의해 기계어 or 중간 언어로 번역된다. 컴파일 과정에서 코드 최적화, 오류 검사 등이 수행된다. 컴파일 오류가 발생하지 않으면 실행 가능한 프로그램 (실행 파일) 이 생성된다.
Java의 경우 바이트코드와 같은 중간 형태로 컴파일되어, 가상 머신에서 실행될 수 있다.
컴파일된 코드는 필요한 라이브러리나 다른 모듈과 함께 링킹된다.
최종적으로, 링킹 과정을 거쳐 완전한 실행 파일이 완성된다.
사용자가 프로그램을 실행시키면, 운영 시스템은 실행 파일을 메모리로 로드한다.
이 과정에서 운영 시스템은 파일 시스템에서 실행 파일을 찾아 메모리에 적재한다.
메모리에 로드 된 실행 파일은 프로세스(실행중인 프로그램의 인스턴스)로 생성된다.
프로세스는 고유 메모리 공간(code, data, stack 등) 과 운영 시스템 자원(파일 핸들, 스레드 등) 을 할당받는다.
운영 시스템은 프로세스를 관리하고, CPU 스케줄링을 통해 프로세스가 실행될 수 있도록 한다.
프로세스가 CPU 시간을 할당받으면, 프로세스의 코드가 실행된다. 이때 명령어 실행 사이클 (Fetch, Decode, Execute 등) 이 반복되며, 프로그램의 로직에 따라 처리가 수행된다.
프로그램 실행중에는 메모리 접근, 입출력 작업, 네트워크 통신 등 다양한 시스템 호출이 이루어질 수 있다.
프로그램이 완료되면 운영 시스템은 프로세스를 종료시키고 사용했던 자원을 회수한다. 프로세스 종료는 정상 종료, 사용자에 의한 강제 종료, 오류로 인한 비정상 종료 등 여러 방식이 있다.
int res = a + b; 라는 java 코드가 어떻게 실행되는지 살펴보자.
public class APlusB {
public static void main(String[] args) {
int a = 5;
int b = 3;
int res = a + b;
System.out.println(res);
}
}
javac 컴파일러가 .java 파일을 .class 파일(바이트코드)로 변환한다.
이 바이트코드는 특정 OS에 종속되지 않는 중간 코드로, JVM만 있으면 어디서든 실행 가능하다.
javac APlusB.java # APlusB.class 생성
java APlusB
java 명령어를 실행하면 JVM이라는 프로세스가 OS에서 시작된다.
JVM 자체는 C/C++로 만들어진 네이티브 프로그램이므로 일반적인 프로세스 생성 과정을 거친다.
JVM 내부에서 클래스 로더가 3단계 작업
로딩(Loading)
링크(Linking)
- 상수 풀:
클래스 파일 내부에 있는 상수들을 모아 놓은 것
클래스 파일 내부에서 사용되는 모든 상수들이 저장되어 있다.
- 심볼릭 참조:
클래스나 인터페이스의 이름, 필드의 이름, 메서드의 이름 등을 나타내는 것입니다.
일종의 자바 코드 상의 식별자 (예시 APlusB 같은것)
초기화
초기화 단계는 클래스의 정적변수(static variable)와 클래스의 정적블록(static block)이 초기화 되는 단계다.
정적변수는 클래스가 로딩되는 과정에서 메모리에 할당된다. 이 변수들은 초기화 전에 기본값으로 초기화 된다.
정적블록은 클래스가 로딩될 때 실행되는 코드 블록이다. 이 블록에서는 클래스의 정적 변수를 초기화하거나, 클래스의 정적 메소드를 호출하거나, 예외 처리 등의 작업을 수행할 수 있다.
JVM 실행엔진이 바이트코드를 실행하는 두 가지 방식:
int res = a + b; → iload_1, iload_2, iadd, istore_3
// 반복문 같은 자주 실행되는 코드
for(int i = 0; i < 1000000; i++) {
res = a + b; // JIT 컴파일 대상
}
JVM 프로세스 내부의 여러 메모리 영역:
JVM 프로세스
├── Method Area: 클래스 정보, static 변수
├── Heap: new로 생성한 객체들
├── Stack: 지역변수, 메서드 호출 정보
└── PC Register: 현재 실행 중인 명령어 위치
@RestController
public class UserController { // Method Area에 클래스 정보
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { // Stack에 id 변수
User user = new User(id); // Heap에 User 객체 생성
return user;
}
}
Java의 가장 큰 특징은 자동 메모리 관리
C/C++
char* buffer = malloc(1024); // 수동 할당(size_t size; 입력 인자로 필요한 형식의 메모리 크기)
...
free(buffer); // 수동 해제 (없으면 메모리 누수)
Java
List<String> list = new ArrayList<>(); // 자동 할당
...
// 자동으로 GC가 메모리 해제 (개발자가 신경 쓸 필요 없음)