JAVA의 컴파일과 실행

Updated:

1. 크로스플랫폼과 간단한 컴파일 과정

1.1 크로스플랫폼

Java란 1991년 James Gosling, Mike Sheridan, and Patrick Naughton이 당시 프로그램을 작성하는데 특정 운영체제, 디바이스마다 다른 규격에 어려움을 느끼고 “Write once, run any where(WORA), 한 번 작성하면 어디서든 실행” 된다는 가치관을 가지고 (크로스플랫폼) 시작되었다.

크로스 플랫폼 : 특정 언어의 같은 소스코드를 여러 운영체제, 플랫폼에서 실행 가능

1.2 C언어 컴파일 과정과 크로스플랫폼

  1. 소스코드 작성(.h, .c)
  2. 전처리 & 컴파일
    • input : 소스코드 (.h, .c) ⇒ 전처리기 ⇒ output : 트랜스레이션 유닛 (.pre)
    • input : 트랜스레이션 유닛(.pre) ⇒ 컴파일러 ⇒ output : 어셈블리어 코드 (.s)
    • input : 어셈블리어 코드(.s) ⇒ 어셈블러 ⇒ output : 오브젝트 코드 (.o)
    • input : 오브젝트 코드(.o) ⇒ 링커 ⇒ output : 실행파일 (.out .exe)
  3. 실행파일 실행 (.out .exe)

C언어의 컴파일 최종 결과는 실행파일이 나온다. 이 실행파일은 특정 운영체제(Linux, Window, MacOS) 전용 실행파일이기 때문에 Linux에서 만들 실행 파일(.out)을 Window에서 실행할 수 없다. 하지만 Linux에서 작성한 C언어 소스 코드를 Window 전용 컴파일러로 컴파일 한다면 Window에서 실행할 수 있다. 이처럼 C언어는 소스코드에 대해선 크로스 플랫폼이라 할 수 있고 컴파일 결과에 대해선 그렇지 않다 라고 할 수 있다.

1.3 크로스플랫폼, Java 컴파일 과정

  1. 소스코드 작성 (.java)
  2. 컴파일 - input : 소스코드(.java) ⇒ 컴파일(javac) ⇒ output : .class(bytecode)
  3. JVM에서 바이트코드 실행

자바의 컴파일 최종 결과는 실행파일이 아닌 바이트코드(bytecode)이다. 이는 특정 운영체제가 이해할 수 있는 기계어가 아니며 JVM(Java Virtual Machine)이라는 프로그램이 이해할 수 있는 명령어이다. C언어 같은 경우에는 운영체제가 실행할 수 있는 결과로 컴파일 되어 각 운영체제에 종속되어 바로 실행 할 수 있는 반면 Java는 바이트코드로 컴파일 되면 운영체제와 상관없이 실행할 운영체제에서 JVM이란 프로그램을 설치하면 그 위에서 실행할 수 있다. 이는 C와는 다르게 소스코드와 컴파일된 후의 바이트코드에서 크로스 플랫폼이라 할 수 있는데 문제는 JVM이 설치되어 있지 않다면 실행할 수 없다는 문제가 있다. 이를 두고 완벽한 크로스 플랫폼이라 할 수 있는가 의문이 남는다.

  • 컴파일러를 프로그래밍 언어 → 기계어로 바꾸는 과정이라 말한다면 javac는 100 컴파일러라고 할 순 없지만 사람이 이해할 수 없는 코드로 변환하는 과정이 있으므로 컴파일러라 하자. 실제 기계어로 바꾸는 과정은 뒤쪽의 JIT 컴파일 과정이 있다.

Bytecode : JVM의 인터프리터가 효율적으로 실행할 수 있는 형태(기계어가 아니다.)

java_compile

2. Hello World! (컴파일 & 실행)

  • 아래 코드는 Hello, World! 를 커맨드 창에 출력하는 코드이다.
    • 소스코드 내용은 다음에 이야기하기로 하고 이곳에선 컴파일(빌드)와 실행을 살펴본다.
// Main.java

package com.kukim;

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

2.1 컴파일

# javac -d <컴파일 결과 저장할 경로> <컴파일할 .java 파일>
# -d : .class 파일 저장할 경로
# class란 폴더가 없을 땐 자동으로 생성된다.

javac -d class/ srcs/com/kukim/*.java
  • javap 를 활용한 bytecode opcode check!
javap -c Main.class
  • bytecode 내용을 살펴보면 각 코드 번호와 명령어 aload_0, invokevirtual, return 등이 있는데 이것이 JAVA에서 사용하는 opcode 이다. 이 opcode가 1바이트로 되어 있어서 bytecode라 불리며 8비트이기 때문에 2^8 = 256개의 바이트코드를 가지고 있고 모두 사용되고 있진 않다.

2.2 RUN

컴파일한 .class(bytecode) 파일을 JVM에서 실행된다.

2.2.1. java -classpath를 활용한 실행

# java -classpath <class 파일 위치> <클래스이름>
java -classpath class/ com.kukim.Main

2.2.2 jar 파일을 활용한 실행

  • 소스코드가 적을 때는 위의 실행처럼 단일 .class 파일만 실행하면 된다. 하지만 많은 양의 소스코드를 넘겨줄 땐 C나 C#의 .dll 이나 lib으로 만들어 배포하듯이 JAVA에서는 .jar 압축 파일형태로 묶어 배포한다. jar은 단순히 .zip 파일과 유사하지만 그 안에 META-INF라는 폴더에 MANIFEST 파일에 압축 하일에 대한 상세 내용이 들어 있고 JVM은 이 데이터를 가지고 실행한다.
  • 따라서 jar로 압축할 때 Manifest.txt 파일을 활용해 내 소스코드의 main 함수 위치(entry point)를 알려주어야 한다.
# 1. Manifest.txt 파일 만들기
# 2. jar로 압축하기
jar -cfm <jar 저장할 디렉토리> <Manifest.txt 위치> <최상단 .class 디렉토리 위치>
## -c : create / -f : .jar 파일 이름 설정 / -m : manifest 파일 경로
# 3. java로 실행하기
java -jar ./lib/deploy.jar

2.2.3 IDE를 활용한 실행

  • 항상 자바를 실행할 때 위의 복잡한 과정을 거쳐야 하는가? NO!
  • 실제로는 IDE를 활용해 컴파일, 실행한다.
  • Intellij & Eclipse을 활용하자.

+a) 서로 다른 JAVA 버전 간의 컴파일과 실행

자바는 버전 1부터 현재 15까지 있다.

  • java 14의 javac(컴파일 옵션 X) 컴파일하여 java 8 버전에서 실행이 가능할까?
    • 정답 : X
  • java 8의 javac(컴파일 옵션 X) 컴파일하여 java 14 버전에서 실행이 가능할까?
    • 정답 : O
  • java 14의 javac(컴파일 옵션 O) 컴파일하여 java 8 버전에서 실행이 가능할까?
    • 정답 : O

하위버전에서 컴파일한 자바 소스코드는 상위버전에서 문제없이 돌아가지만 상위버전애서 컴파일한 소스코드는 어떤 버전에서 실행할 지 컴파일 할 때 옵션을 주어야 하위 버전에서 실행할 수 있다.

# javac version = JAVA 14
## 컴파일 옵션 : -source, -tartget 을 주어 1.8(java 8 명시)
javac main.java -source 1.8 -target 1.8

# java 14에서 버전8 옵션을 주고 컴파일한 결과는 java 8 버전에서 문제없이 실행 가능
java main.class

JVM 구성 요소

🔎 JVM(Java Virtual Machine) : javac를 통해 컴파일 된 .class(Bytecode)를 각각의 운영체제에 맞게 기계어로 번역하여 실행할 수 있게 만들어주는 프로그램이다.

1. Class Loader Sub System

  • 앞에서 컴파일된 .class file(bytecode)들을 로딩, 링킹, 초기화 단계를 거쳐 실제 메모리를 할당하는 역할을 한다.

2. Runtime Data Area

  • Class Loader sub System을 통해 할당된 메모리 공간이다.
    1. Method Area
    2. Heap Area
    3. Stack Area
    4. PC register
    5. Native Method Stack

3. Execution Engine

3.1 Interpreter

  • 컴파일된 .class의 바이트 코드를 기계어(0101010…)으로 변환하며 실행한다.
  • 최초의 JVM은 인터프리터 방식이었다. 하지만 동일한 메서드를 매번 해석하여 실행하는 성능의 이슈가 발생하였다.

3.2 JIT(Just In Time) Compiler

  • 인터프리터의 성능 이슈를 해결하기 위해 생긴 방법이다.
  • 자주 사용되는 메서드를 체크한 뒤 기계어로 변환한 뒤 캐싱하여 재사용하는 방법이다.
  • JIT 컴파일러는 모든 메소드에 사용되는 것이 아니다. 빈도가 적은 메소드에는 인터프리터가 더 빠를 수 있다.

3.3 GC(Garbage Collector)

  • C언어 같은 경우 동적 메모리 할당을 통해 힙 영역의 데이터를 읽고 쓴다. 이때 사용자가 직접 동적 메모리 할당을 해제해야 하는 어려움이 있다. (C = unmanaged language)
  • JAVA는 힙 영역의 메모리 관리를 사용자가 하지 않고 GC가 해준다. (JAVA = managed language)
  • 이는 매우 강력하다.

4. Native Method

  • JAVA에서 C, C++, assembly로 작성된 라이브러리를 사용할 수 있게 도와준다.

JDK와 JRE의 차이

  • JDK(Java Development Kit)
    • JDK = JRE(JVM + Library) + JAVA 개발도구(컴파일러javac, 디버거 등)
  • JRE(Java Runtime Environment)
    • JRE = JVM + Library
    • JAVA9 버전부터 더이상 JRE는 배포하지 않고 JDK만 배포한다.

Reference 🌏

Leave a comment