Java 프로그램의 실행 과정과 JIT 컴파일러

Socra·2024년 12월 25일
post-thumbnail

Java의 시작

Java는 Sun MicrosystemsJames Gosling과 Green 프로젝트 연구팀이 만들어 1995년 발표했다.

당시 가전제품이 다양한 하드웨어 및 소프트웨어 환경에서 운영되고 있었기 때문에, 특정 하드웨어에 종속되지 않는 소프트웨어 플랫폼이 필요했다. Green 프로젝트의 목표는 모든 가전제품에서 동작할 수 있는 플랫폼 독립적인 프로그래밍 언어와 런타임 환경을 개발하고 통신 기능을 내장하는 것이었다.

James Gosling은 초기에 C++로 운영체제를 만들려고 시도했지만 C++의 복잡성으로 실패하고, C++의 문제점(플랫폼 종속성, 메모리 관리, 복잡성)을 해결한 프로젝트를 위한 언어 Oak를 만들었다.

Java라고 불리기 이전엔 James Gosling의 사무실 밖에 있던 오크 나무에서 따온 ‘Oak’로 불렸다. 하지만 ‘Oak’는 이미 Oak Technology의 상표로 등록되어 있어 이름을 변경해야 했는데, 인도네시아의 커피 재배지 자바 섬에서 따와 ‘Java’라는 이름이 붙게 되었다.

factorio thumbnail Mount Merapi, Central Java, Indonesia / https://www.triptipedia.com/tip/Wvj2HwL/get-to-know-more-about-the-island-of-java




1993년 그래픽 기반 World Wide Web이 발표되고, 1994년 당시 웹 브라우저의 표준이 되었던 Netscape Navigator가 발표되는 등 Java가 탄생한 시기(1995년)는 웹 기술이 급속도로 성장하고 있던 시기였다.

플랫폼 독립적인 Java의 특징은 웹 기반 응용 프로그램에 적합했고, Sun은 “Write Once, Run EveryWhere” 라는 슬로건으로 Java를 홍보했다.

이후 Java는 초기 설계 목표를 넘어 오늘날 널리 사용되는 범용 프로그래밍 언어로 발전했다.

James Gosling이 왜 자바를 만들었는지, 어떤 생각을 하며 만들었는지 알 수 있는 인터뷰 영상이 하나 있다.

https://youtu.be/RrMptmNYkSw?si=oo5KeXzqwS-Z9wFj

Java의 특징

Java는 다음과 같은 목표로 설계되었다.

Java 언어 설계 목적 5가지

  1. Simple, Object Oriented, and Familiar
  2. Robust and Secure
  3. Architecture Neutral and Portable
  4. High Performance
  5. Interpreted, Threaded, and Dynamic

https://www.oracle.com/java/technologies/introduction-to-java.html


  • 단순, 객체지향, 친숙함
    • C++에서 필요한 기능만 포함하고 포인터 연산, 연산자 중복 정의, 다중 상속을 제거
    • Smalltalk, Eiffel 등의 언어에 영향을 받은 객체 지향 언어
    • 기본 데이터 타입을 제외한 거의 모든 것이 객체
    • C/C++과 유사한 문법
    1. 견고, 안전
    • 포인터 제거(당시 프로그램 대부분의 오류는 포인터 때문이었다)
    • 컴파일 시 강력한 데이터타입 검사
    • 바이러스, 파일 삭제/수정, 데이터 파괴 작업, 컴퓨터 오류 연산 등 방지
    1. 컴퓨터 구조에 중립적, 이식성
    • 컴퓨터 구조에 중립적인 byte code로 컴파일 후 JVM에서 실행되어 모든 플랫폼에서 동일한 실행 결과 생성
    1. 고성능
    • 실행 시 바이트 코드를 해석해야 하기 때문에 기본적으로 실행 속도는 느리지만, JIT 컴파일러를 도입해 이를 보완

    JIT(just-in-time): 런타임에 코드를 컴파일 하는 기술로, 인터프리터 언어에서 실행 시간 최적화를 위해 주로 사용한다.

    1. 인터프리트 방식, 멀티 스레드 지원, 동적
    • 언어 수준의 멀티스레딩 지원
    • 멀티 프로세서 하드웨어를 지원해 멀티CPU 시스템에서 높은 효율

    Java의 실행 과정

    Java 프로그램의 실행 과정

    C언어는 소스코드가 컴파일되어 기계어로 변환되면 바로 실행된다.

    하지만 자바 컴파일러는 바로 기계어로 변환하지 않고, 플랫폼에 중립적인 바이트 코드(byte code)를 생성한다. 이후, 바이트 코드는 JVM에 의해 해석되어 실행된다.

    Compile Time

    1. Mycode.java : IDE를 통해 작성된 Java 코드 파일이 있다.
    2. Mycode.class : IDE에서 Run 버튼을 누르거나, javac Mycode.java 를 입력하면 Java 컴파일러가 syntax error, compile time error를 확인한 후 java code를 플랫폼 중립적인 bytecode로 변환한다. bytecode는 JVM을 통해서만 실행시킬 수 있다.

    Run Time

    1. class loader(JVM의 구성요소)가 bytecodeJVM으로 로드한다.
    2. bytecode verifier(JVM의 구성요소)가 bytecode의 무결성을 검사하고, 문제가 발견되지 않은 경우 interpreter로 넘겨준다.
    3. Java는 컴파일러 언어임과 동시에 인터프리터 언어인 하이브리드 언어이므로, JVM 내부 interpreterbytecode의 각 행을 실행할 수 있는 기계어로 변환해 OS/하드웨어, 즉 실행할 CPU로 전달한다.

    JIT(Just-In-Time) Compiler

    JIT는 어떤 원리로 애플리케이션을 더 빠르게 만들까?

    먼저, JIT 컴파일러 없이 인터프리터로만 동작하는 경우를 생각해보자.

    인터프리터는 bytecode 명령어와 해당 플랫폼의 기계어 명령어 간의 매핑 정보를 사용해 bytecode를 한 줄씩 기계어로 변환한다.

    인터프리터만 사용하면 컴파일 시간이 소모되지 않아 애플리케이션을 로드하고 시작하는 시간은 빠르다.

    하지만, 동일한 코드가 반복될 때마다 매번 기계어로 변환하기 때문에 비효율적이고 런타임 성능이 떨어진다. (No optimization)

    c1 compiler

    여러 번 실행된 코드는 앞으로도 많이 실행될 것이니까 반복되는 코드를 캐싱하면 성능을 개선할 수 있지 않을까?

    이를 위해, JVM은 코드 스니펫이 실제로 몇 번 실행되었는지 performance counter를 유지한다.

    코드가 반복적으로 실행되어 특정 counter값에 도달할 경우, 컴파일후 컴파일된 복사본을 JVM의 code cache에 저장하고 기본적인 최적화를 수행한다.

    메서드 인라이닝, 빠른 메서드 호출, 간단한 루프 최적화, 널체크 제거, 강제 명령어 재배치, 간단한 상수 전파, 스택 프레임 최적화 등

    이 작업을 하는 JIT 컴파일러가 c1 compiler다.

    c2 compiler

    c1 컴파일러는 낮은 수준의 최적화를 하고, 코드 캐시에 저장한다.

    performance counters - Interpreter가 코드를 변환하면서 어떤 코드가 몇 번 실행되는지 기억한다.

    컴파일하고 컴파일된 복사본을 저장해 optimize하는것이 낫다

    240MB(java8)의 code cache에 저장함.

    -XX:InitialCodeCacheSize= : 시작 시 코드 캐시 사이즈

    -XX:ReservedCodeCasheSize= : 코드캐시 사이즈 최대치

    -XX:CodeCacheExpansionSize= : 코드 캐시 사이즈가 늘어나는 단위 설정

    0개의 댓글