[Java] Java 기초 - 자바 코드가 실행되는 원리

Hyunjun Kim·2025년 3월 28일
0

Data_Engineering

목록 보기
11/153

2. 자바 코드가 실행되는 원리

우선 자바 코드 실행하는 원리 배우기 전에 일반적으로 프로그래밍 언어가 어떤 과정으로 실행되는지 배워보자.

2.1. 프로그래밍 언어의 실행 원리

(bottom-up 방식의 설명을 할 것.)왜냐? top-down으로 하는 건 우리가 익숙히 접하기 쉬운데 bottom-up으로 하는 건 접하기 어려운 점이 한가지고, 또 이제 기계로부터 시작한 것을 배우는 게 실제 구현체에 더 가깝기 때문에 bottom-up으로 배워볼 것이다.

2.1.1 기계어(Machine Code)

기계어는 컴퓨터가 이해하는 언어이다. 모든 컴퓨터는 CPU, 메모리, 디스크, 버스 등의 하드웨어를 이용하여 작업을 수행한다. 이러한 하드웨어 구성은폰 노이만 아키텍쳐하에서 이런 구조를 가진다.

기계어의 특징

  1. 하드웨어 의존성
    기계어는 하드웨어 종류에 따라 달라진다. 즉, 각 컴퓨터 시스템의 CPU 종류, 메모리 크기, 디스크 사양 등에 따라 기계어가 달라진다. 같은 프로그램을 실행하더라도 다른 하드웨어에서 실행되면 그에 맞는 기계어로 해석되어 실행된다.

  2. 프로그램 실행
    프로그램을 실행한다는 것은 컴퓨터의 CPU가 기계어 명령어를 해석하고 실행하는 과정이다.

  3. 어렵고 시간이 많이 걸리는 프로그래밍
    기계어로 직접 코딩하는 것은 프로그래밍이 매우 어렵고 시간도 많이 걸린다. 또한 기계어는 읽기 어렵고 유지보수가 힘들다.

  4. 하드웨어에 종속적인 제약
    기계어로 작성된 프로그램은 같은 하드웨어를 사용하는 장비에서만 실행될 수 있다. 이는 하드웨어의 종류와 사양에 따라 달라지는 기계어 명령어가 있기 때문이다.

결론
기계어는 컴퓨터가 직접 이해하고 실행하는 언어이지만, 프로그래밍이 어려우며 하드웨어에 종속적이다. 이러한 특성 때문에, 우리는 고수준 언어(예: C, Java)를 사용하여 기계어로 컴파일되는 프로그램을 작성한다.

2.1.2 어셈블리어(Assembly Langauge)

어셈블리어는 저급언어(Low Level Language)의 일종으로. 컴퓨터CPU가 실행할 수 있는 operation(동작) 단위로 코딩할 수 있는 언어이다.

예를들면 이런 식이다. "레지스터 1번 에서 값을 가져와", "메모리 주소 00번에서 값을 가져와", "CPU에서 더하기를 해". 형태는 다음과 같이 생겼다.

이미지 출처 : https://commons.wikimedia.org/wiki/File:Motorola_6800_Assembly_Language.png

기계어보다 그래도 사람이 알아볼 수 있는 의미 단위로 코딩을 할 수 있다. 하지만, 여전히 하드웨어에 종속적이다. 예를 들어서 인텔칩의 어셈블리어로 코딩을 했다면, 애플 실리콘 칩에서 그 프로그램은 동작하지 않는다. 또한 어셈블리어의 명령어도 하드웨어의 스펙과 기능을 알아야 코딩을 할 수 있기 때문에 여전히 코딩을 하기도 어렵고 시간도 오래걸린다. (아까보다 나아진 건 사람이 이해할 수 있는 거, 동작이라는 게 하나의 덩어리로 생겼다는 것. 정도)

2.1.3 고급어(High-Level Language)

고급어는 하드웨어의 동작을 고도로 추상화 해서 프로그래밍 할 수 있는 언어를 말한다. 주요 특징으로는 동작이나 데이터, 데이터 구조 등에 대해서 사람이 이해 할수 있는 이름을 붙일 수 있다는 게 가장 큰 특징이다. 컴퓨터 시스템을 사용하는 것이 저급언어보다 더 쉽고 자동화 되어있다. 고급언어의 목표는 사람이 이해하고 작성하기 쉬운 코드를 작성하는 것을 지원 하는 것이다.

Shell script, C, Python, Java 등 프로그래밍 언어라고 배우는 대부분의 것들이 High Level Language에 해당함.

2.1.4 고급어, 어셈블리어, 기계어


이미지 출처 : http://www.btechsmartclass.com/c_programming/C-Computer-Languages.html

고급어, 어셈블리어, 기계어 각각의 간단한 예제와 추상화 수준은 다음과 같다.
하지만, 고급어가 어셈블리어가 되고, 어셈블리어가 기계어가 되어야 하드웨어를 구동할 수 있다는 뜻은 아니다. 최종 결과물이 기계어 이기만 하면 컴퓨터에서 구동할 수 있다.

헷갈리면 안되는 게 하이레벨 랭귀지는 어셈블리어로 변환되고 어셈브리어는 머신랭기지로 변환되고 머신랭기지가 하드웨어에 동작할 수 있다! 이 말이 아니다. 그래서 결국에 머신랭귀지여야 하드에워가 이해할 수 있다는 건 기본이고, 어셈블리어를 어떻게 머신랭귀지로 바꿔줄지, 하이레벨랭귀지를 어떻게 머신랭귀지로 바꿔줄지는 각 언어나 특징마다 다르다.

2.1.5 고급어, 어셈블리어가 기계어가 되는 과정


이미지 출처 : https://www.texno.blog/asagi-orta-ve-yuksek-seviyyeli-proqramlasdirma-dilleri?36

Assembly LanguageAssembler라는 중간에 동작하는 도구에 의해 Machine Code로 변환이 되고
High Level LanguageCompiler라는 걸 통해서 Machine Code로 변환될 수도 있고 Interpreter라는 걸 통해서도 Machine Code로 변환될 수 있다.

그래서 Compiler 쓰는 대표적인 언어가 C, Java 같은 거고, Interpreter를 쓰는 언어가 대표적으로 Python 같은 언어가 Interpreter로 Machine Code로 변환이 된다.

위 그림은 간단히 도식화 한 것이고 모든 언어가 이 공식을 따르지 않는다. 각 언어마다 어떤 과정이나 도구로 기계어로 바꿀지는 각각 다르니까 꼭 각 자기가 쓰는 언어에 대한 이 부분은 따로 공부를 하고 코딩을 한다면 더 도움이 많이 될 것이이다.

2.2. 자바 코드의 컴파일과 실행 원리

2.2.1. 자바 코드의 컴파일


이미지 출처 : https://medium.com/@PrayagBhakar/lesson-2-behind-the-scenes-4df6a461f31f

우리가 코드로 짠 자바 파일(.java 확장자)은 컴퓨터가 이해할 수 없는 언어다. 자바파일은 자바 컴파일러(javac)에 의해서 자바 바이트 코드(.class 확장자)로 변환된다. 이 자바 바이트 코드는 JVM(Java Virtual Machine)이 이해할 수 있는 언어다.

자바 바이트 코드는 JVM이 이해할 수 있는 machine instruction으로 구성되어있는데, JVM자바 바이트코드를 각 운영체제나 하드웨어에 맞춰 수행할 수 있는 기계어로 바꾸어서 실행한다. 즉, 하드웨어 또는 운영체제마다 달라지는 기계어 코드는 JVM이 호환해준다고 이해하면 됨. Java의 목적인 “Write Once, Run Everywhere” 가 이런 원리에 의해서 가능하다.

우리가 .java 확장자로 Java 파일을 작성하면, Java 컴파일러를 통해 Java 바이트코드(.class 확장자를 가짐)로 변환된다. (바로 머신 코드로 변환되는 것은 아니다.)

앞에서 JVM이 Java 프로그램을 어디서든 실행할 수 있도록 해준다고 설명했다. 따라서 JVM이 직접 읽는 것은 .java 파일이 아니라, 컴파일된 .class 파일(Java 바이트코드)이다. JVM은 실행 환경(OSX, Windows, Linux 등)에 상관없이 동작하며, 운영체제와 하드웨어에 맞게 프로그램을 실행할 수 있도록 지원한다.

즉, Java 소스 코드(.java)는 사람이 이해할 수 있는 코드이고, Java 바이트코드(.class)는 JVM이 이해할 수 있는 명령어의 조합이다. Java 컴파일러는 사람이 작성한 소스 코드를 JVM이 실행할 수 있는 바이트코드로 변환하는 역할을 한다. 그래서 운영체제마다 달라지는 거는 JVM이 동작하면서 호환해준다.

글면 JVM이 어떤 과정을 통해서 그걸 호환해주는지 알아보자!!!

2.2.2. JVM이 Java 바이트코드를 실행하는 원리


이미지 출처 : https://www.geeksforgeeks.org/compilation-execution-java-program/

그림을 보면 맨 왼쪽 소스코드컴파일을 통해 바이트코드(Bytecode)가 되었다. 그러면 JVM이 실행됐다는 것은 컴퓨터에서 프로세스가 실행됐다는 의미이다. 즉, 런타임(실행 타임; 프로그램이 실행된 후 프로세스로 실행된 상태.)에 클래스 로더(Class Loader)가 이 클래스를 메모리에 로드한다.
이때, 바이트코드 검증(Bytecode Verification)이 수행되는데, Bytecode가 제대로 된 bytecode인지 (예를 들어 JVM이 실행 중인 버전과 바이트코드의 버전이 호환 가능한지, 내 명령어가 JVM이 이해할 수 있는 머신 인스트럭션인지, 등의 것) 확인한다.
만약 바이트코드 검증을 통과하면, JIT(Just-In-Time) Compiler라는 걸 통해서 최종 Machine Code로 변경된 뒤에 이 Machine Code를 컴퓨터가 실행하게 된다.


자바의 클래스 로더(Class Loader)와 바이트코드 검증(Bytecode Verifier)

자바의 클래스 로더(Class Loader)자바 클래스를 런타임(Run Time, 프로그램 실행 후 프로세스로 실행된 상태)에 메모리에 로드한다.
이때, 바이트코드 검증(Bytecode Verifier)이 수행되어 해당 바이트코드가 실행 가능한지 검사한다.

  • 대표적인 검증 과정
    • JVM 버전과 바이트코드의 호환성
    • JVM이 이해할 수 없는 명령어 포함 여부
    • 기타 보안 및 유효성 검사

만약 JVM이 이해할 수 없는 버전의 자바 바이트코드가 로드되면, 바이트코드 검증에서 실패하게 된다.


JIT(Just In Time) Compiler란?

  • Java 파일(.java)을 Java Compiler.class 파일(바이트코드)로 변환한다.
  • 그러나 바이트코드 → 머신 코드 변환은 실행 중(런타임) 이루어진다.
  • 이 변환을 수행하는 것이 JIT 컴파일러이다.

JIT 컴파일 방식

  • 최초에 Java Compiler로 변환한 바이트코드 전체를 한 번에 기계어로 변환하여 실행하는 것이 아니다.
  • 클래스가 로딩될 때마다 JIT 컴파일러가 바이트코드를 기계어로 변환하고 실행한다.
  • 즉, JIT Compiler는 로드되는 자바 바이트코드를 즉시 컴파일하여 실행 가능한 기계어로 변환하는 역할을 한다.

우선, Java 파일을 .class 파일로 바꿀 때는 컴파일러를 통해서 한 번에 다 바꿔야 된다. 그리고 byte 코드가 만들어지면 해당 byte코드는 로드 될 때마다 컴파일해서 기계코드로 바뀐다. 그래서 이 과정은 사전에 한 번에 이루어지는 과정이고

Byte code(.class)에서 머신 코드로 바뀌는 건 실시간으로 이루어지는 과정이다.
그래서 클래스가 로딩될 때마다 JIT컴파일러가 머신 코드를 생성하고, 실행한다.
그래서 이 특징 때문에 자바 코드 개발자는 사전에 클래스 파일만 뽑아내면 운영체제와 상관 없이 실제로 운영체제에 맞춰서 기계 코드를 바꾸는 건 실행 시간(run time)의 JIT 컴파일러를 통해서 가능하다. 그래서 자바 코드 개발자는 컴파일 할 때 기계별로 컴파일하고 검증할 필요가 없는 장점이 있다.

장점

  • 운영체제 독립성:
    • .class 파일만 배포하면 되며, 운영체제에 맞는 기계어 변환은 JIT 컴파일러가 처리
  • 최적화된 실행 속도:
    • JIT 컴파일러는 오랜 기간 최적화가 이루어져 매우 빠르게 동작
    • 변환 속도가 1마이크로초(µs) 이하로 매우 짧아, 성능 저하를 거의 체감할 수 없음

대신에 이제 무조건 좋냐? 단점도 있다. 보면 세 단계가 추가되어 있다. class loader, bytdecode verifier, JIT Compiler. 그 말은 애초에 맨 처음에 컴파일해서 기계어가 만들어졌으면 이 중관 과정을 안 거쳐도 되는데

실행시간에 이 중간 과정을 매번 거치면서 기계어가 실행되다 보니까 조금 더 시간을 여기서 쓴다.. 대신 JIT 컴파일러가 오랜 시간동안 최적화가 많이 돼서 굉장히 빠르다. 눈치를 못 챌 정도로 빠르고 예를 들면 1마이크로세컨드도 안 되는 시간 동안에 변환이 되기 때문에 큰 문제가 되는 경우는 적다. 대신 JIT 컴파일러가 소모하는 시간까지 중요하다면 (우리 프로그램의 성능이 너무너무 중요하고 더 이상 개선하거나 튜닝할 게 없어. 라면 ) 이런 경우에는 사전에 기계어를 생성해버리는 그런 언어로 프로그래밍 해야 그 목표를 달성할 수 있다.

단점

  • 추가적인 실행 단계 필요:
    • 클래스 로더 → 바이트코드 검증 → JIT 컴파일 → 실행 과정이 포함되어 실행 시점의 오버헤드 발생
  • JIT 컴파일 시간이 중요한 경우:
    • 기계어 자체를 실행하는 것보다 시간이 더 걸린다.
    • 그러나 JIT 컴파일러는 최적화가 잘 되어 있어서 대부분의 경우 성능 저하가 크지 않음
    • 하지만 프로그램의 성능이 극도로 중요한 경우(더 이상 최적화할 수 없는 경우),
      사전에 기계어를 생성하는 언어(C, C++ 등)를 사용하는 것이 더 적합

Java vs. C/C++ 실행 방식 비교

대표적으로 C나 C++같은 언어로 프로그래밍 하면 실제로 컴파일을 할 때 컴파일과 링킹이라는 단계로 이루어지는데 그 과정을 구동 환경에 맞추어서 만든 다음에 실제로 구동 환경별로 다른 바이너리를 배포해서 실행을 시켜야 된다.

그렇기 때문에 이런 단점이 있긴 있지만 대부분의 경우에 이게 문제가 될 정도로 성능의 저하를 가져오는 부분은 아니고, 호환성이 좋기 때문에 자바 언어를 사용한다고 이해하면 된다.

언어컴파일 단계실행 단계특징
Java.java.class(바이트코드)JIT 컴파일 후 실행운영체제 독립적, 실행 시 변환
C/C++.c/.cpp → 기계어(.exe, .out)바로 실행운영체제별로 별도 컴파일 필요, 실행 속도 빠름
  • C/C++은 사전 컴파일(Precompiled) 방식이므로 실행 시간이 빠르지만, OS별로 다른 바이너리를 배포해야 한다.
  • Java는 JVM을 통해 실행되는 방식이라 OS 독립성이 뛰어나지만, 실행 시 JIT 컴파일 과정이 추가된다.
mkdir java_compile
cd java_compile
vi Hello.java

Hello.java

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

자바 컴파일러로 Hello.java 컴파일

javac Hello.java
ls

결과 : Hello.class Hello.java
(컴파일된 Hello.class 파일이 생겨난 모습.)

Hello.class 를 vi로 확인해 보았을 때, 이해할 수 없는 문자들이 굉장히 많다. 자바 바이트 코드여서 그렇다.


java Hello

java + 클래스 메인이 위치한 클래스 이름을 .class 확장자 없이 바로 입력
실제로 Hello World! 가 출력되는 모습을 볼 수 있따.

이렇게 자바 파일을 컴파일 해서 클래스 파일을 만든 다음에 자바 프로그램으로 실행시키는 실습을 마친다.

profile
Data Analytics Engineer 가 되

0개의 댓글