코틀린 코드 컴파일 및 빌드

https://velog.velcdn.com/images%2Fba159sal%2Fpost%2F2d3a17cd-8816-461a-856f-e96d8cdb8e5c%2Fimage.png

  • 코틀린 컴파일러는 자바 컴파일러가 자바 소스코드를 컴파일할 때와 마찬가지로 코틀린 소스코드(.kt)를 분석해서 .class 파일을 만들어낸다.
  • 만들어진 .class 파일은 개발 중인 애플리케이션의 유형에 맞는 표준 패키징 과정을 거쳐 실행될 수 있다.

https://velog.velcdn.com/images%2Fba159sal%2Fpost%2F865aeabf-b25c-4ff6-b903-f8fb3eba8674%2Fimage.png

Java 코드와 Kotlin 코드의 빌드 과정은 다음과 같은 순서로 이루어진다.

  1. 코틀린 컴파일러(kotlinc)가 코틀린 코드를 컴파일해 .class 파일을 생성한다. 이 과정에서 코틀린 코드가 참조하는 Java 코드가 함께 로딩되어 사용된다.

    • Parsing (파싱): 코틀린 컴파일러는 .kt 파일의 코드를 읽어 들여 문법적으로 올바른지 검사하고, 추상 구문 트리(AST, Abstract Syntax Tree)를 생성합니다.

    • Type Checking (타입 검사): AST를 분석하여 타입 정보를 추론하고, 타입 오류를 검사합니다. 코틀린의 강력한 타입 추론 시스템은 이 단계에서 중요한 역할을 합니다.

    • Code Generation (코드 생성): 타입 검사가 완료된 AST를 기반으로 JVM 바이트코드(.class 파일)를 생성합니다. 이때 코틀린 런타임 라이브러리대한 참조가 추가됩니다.

    • Optimization (최적화): 생성된 바이트코드를 최적화하여 실행 속도를 향상시킵니다.

  2. Java 컴파일러(javac)가 Java 코드를 컴파일해 .class 파일을 생성한다. 이때 이미 코틀린이 컴파일한 .class 파일의 경로를 클래스 패스에 추가해 컴파일한다.니

    • 코틀린 컴파일러로 컴파일한 코드는 코틀린 런타임 라이브러리(kotlin runtime library)에 의존한다.

    • 코틀린 런타임 라이브러리 : 코틀린 자체 표준 라이브러리 클래스 + 코틀린에서 자바 API의 기능을 확장한 내용

    • 코틀린으로 컴파일한 애플리케이션을 배포할때는 코틀린 런타임 라이브러리도 함께 배포해야 한다.

    • 프로젝트를 컴파일하기 위해 메이븐(Maven)과 그레이(Gradle)앤트(Ant) 등의 빌드 시스템을 사용

    • 빌드 시스템은 모두 코틀린과 자바가 코드베이스에 함께 들어있는 혼합 언어 프로젝트를 지원할 수 있다

    • 메이븐(Maven)과 그레이(Gradle)들은 애플리케이션을 패키지할 때 알아서 코틀린 런타임 라이브러리을 포함시켜준다.

JDK, JRE, JVM의 관계 및 역할

image-20240519134814420.png

JDK (Java Development Kit):

  • 정의: 자바 개발 도구 모음입니다. 자바 애플리케이션을 개발, 컴파일, 실행하는 데 필요한 모든 것을 포함합니다. 개발하려면 필요

  • 구성 요소:

    • JRE (Java Runtime Environment): 자바 애플리케이션을 실행하기 위한 환경입니다.

    • 컴파일러 (javac): 자바 소스 코드를 바이트코드로 변환합니다.

    • 디버거 (jdb): 자바 애플리케이션의 오류를 찾고 수정하는 데 사용됩니다.

    • 자바독 (javadoc): 자바 소스 코드에서 API 문서를 생성합니다.

    • 기타 도구: jar, jlink, jmod 등 다양한 개발 도구가 포함됩니다.

JRE (Java Runtime Environment):

  • 정의: 자바 애플리케이션을 실행하기 위한 환경입니다. 실행하려면 필요

  • 구성 요소:

    • JVM (Java Virtual Machine): 자바 바이트코드를 실행하는 가상 머신입니다.

    • 핵심 라이브러리: 자바 언어의 핵심 기능을 제공하는 클래스 라이브러리입니다.

JVM (Java Virtual Machine):

  • 정의: 자바 바이트코드를 실행하는 가상 머신입니다.

  • 역할:

    • 플랫폼 독립성: JVM은 다양한 운영체제와 하드웨어에서 동일한 자바 바이트코드를 실행할 수 있도록 해줍니다.

    • 메모리 관리: JVM은 자바 애플리케이션의 메모리를 자동으로 관리합니다.

    • 보안: JVM은 자바 애플리케이션의 안전한 실행을 보장합니다.

    • 성능 최적화: JVM은 JIT(Just-In-Time) 컴파일러를 사용하여 바이트코드실행 중에 기계어로 변환하여 성능을 향상시킵니다.

관계: JDK는 JRE를 포함하고, JRE는 JVM을 포함합니다. 즉, JDK를 설치하면 JRE와 JVM도 함께 설치됩니다.

추가 정보:

  • JVM은 자바 애플리케이션의 WORA(Write Once, Run Anywhere) 특성을 가능하게 합니다.
  • JVM은 자바 언어의 핵심 기술이며, 다양한 자바 애플리케이션 개발에 사용됩니다.

JAVA 컴파일

  • Java Compiler가 소스 코드 .java 파일을 .class 파일인 Byte Code(중간 레벨) 로 컴파일한다.
    • 단, 해당 코드는 직접 CPU에서 동작할 수 있는 코드가 아니다. 정확히 말하면 가상머신 JVM이 이해할 수 있는 코드이다
  • 이제 이 Byte Code기계어(Binary 코드) 로 변환시키기 위해 가상 머신이 필요한데, 이것이 JVM(Java Virtual Machine) 의 역할이다.
  • JVM이 Byte Code(.class)기계어(Binary Code) 로 변환한다.
  • 이렇게 JVM에 의해 컴파일된 기계어는 바로 CPU에서 실행되어 사용자에게 서비스를 제공해준다.

단, 간과하지 말아야 할 점은 자바 프로그램과는 달리 자바 가상 머신(JVM)은 운영체제에 종속적이므로, 각 운영체제에 맞는 자바 가상 머신을 설치해야 한다는 점이다.

JIT (Just-In_Time) 컴파일러

image-20240519135439770.png

  • JIT 컴파일러는 같은 코드를 매번 해석하지 않고, 실행할 때 컴파일을 하면서 해당 코드를 캐싱 한다.

    • 이후에는 바뀐 부분만 컴파일하고 나머지는 캐싱된 코드를 사용한다.
  • JIT 컴파일은 프로그램 실행 중에 기계어로 번역하는 컴파일 기법입니다.

  • 전통적인 인터프리터 방식과 정적 컴파일 방식의 장점을 결합하여 성능을 향상시키는 데 목표를 둡니다.

JVM의 동작 방식

image-20240519135733380.png

  • 자바 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당받는다.
  • 자바 컴파일러(javac)가 자바 소스코드(.java)자바 바이트 코드(.class)로 컴파일 한다.
  • Class Loader동적 로딩을 통해 필요한 클래스들을 로딩 및 링크 하여 Runtime Data Area(실질적인 메모리를 할당 받아 관리하는 영역) 에 올린다.
  • Runtime Data Area에 로딩 된 바이트 코드는 Execution Engine을 통해 해석된다.
  • 이 과정에서 Execution Engine에 의해 Garbage Collector의 작동Thread 동기화가 이루어진다.

JVM의 구조

image-20240519135937524.png

image-20240519140002581.png

Execution Engine, 실행 엔진

  • 실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다

  • 자바 바이트 코드(*.class) 는 기계가 바로 수행할 수 있는 언어보다는 가상머신이 이해할 수 있는 중간 레벨로 컴파일 된 코드이다.

    • 그래서 실행 엔진은 이와 같은 바이트 코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경해준다.
  • 이 수행 과정에서 실행 엔진은 인터프리터JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행한다.

image-20240519140102678.png

런타임 데이터 영역 (Runtime Data Area)

image-20240519142249624.png

image-20240519142529389.png

  • 런타입 데이터 영역은 쉽게 말하면 JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.

Method Area

  • Method Area는 우리가 보통 정적(Static) 영역이라고 부르는 메모리이다.
  • 프로그램 실행 중 클래스나 인터페이스를 사용하게 되면, JVM은 Class Loader을 이용해 클래스와 인터페이스의 메타 데이터를 Method Area에 저장한다.
    • 즉, 클래스가 로드 되는 시점은 해당 클래스가 사용되기 위해 호출되는 시점이다.
    • 메타 데이터Type Information, Runtime Constant Pool, Field Information, Method Information, Class Variable을 가리킨다.

image-20240519142652066.png

Stack Area

  • Stack Area는 메서드가 호출될 시 할당되는 영역이다.

  • 메서드 호출 시 메서드 내부의 지역 변수 또한 Stack Area에 할당된다.

    • Heap 공간에 객체 데이터를 올리고 그 객체 데이터에 대한 참조값이 할당된다는 뜻이다.
  • 예외적으로 원시 타입(Primitive type) 변수는 Stack 영역에 값 자체가 할당된다.

  • 코어를 최대한 활용하기 위해 Thread를 사용하여 프로그래밍을 하는데, 각 스레드는 하나의 Stack 영역을 할당 받는다.

    • 즉, 스레드는 각자의 메모리 공간을 가지고 메서드를 수행하는 것이다.
  • 메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성되고 메서드 안에서 사용되는 값들을 저장하고,

  • 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다.

  • 그리고 메서드 수행이 끝나면 프레임별로 삭제된다. (LIFO)

  • 프로세스가 메모리에 로드 될 때 스택 사이즈가 고정되어 있어, 런타임 시에 스택 사이즈를 바꿀 수는 없다.

    • JVM 스택에서 프로그램 실행 중 메모리 크기가 충분하지 않다면 StackOverFlowError 발생

image-20240519144141572.png

Heap Area

image-20240519143026113.png

  • Heap Area는 프로그램이 실행되면서 동적으로 생성된 객체(인스턴스) 가 저장되는 공간이다.
  • Heap Area에 생성된 객체들은 다른 객체의 필드 또는 스택에 존재하는 다른 메서드에 의해 참조될 수 있다.
  • 메서드가 실행되면서 Stack영역에는 참조값만을 저장해놓고 Heap Area에 객체 데이터를 저장해 놓는다.
  • 자료구조에 관한 내용이 Heap Area에서 많이 쓰인다.
  • 예를 들어 아래와 같이 배열과 리스트가 선언되었다고 해보자.
  • 이런 경우 Stack 영역에는 참조값만 저장되며 Heap 영역에 실제 데이터가 저장된다.
1
2
var arrayExample: Array<String> = arrayOf("a", "b", "c")
var listExample: List<String> = listOf("a", "b", "c")
  • listOf는 Arrays.asList로 array를 전달하여 ArrayList를 생성하므로 위와 같은 결과가 나온다.

String을 효율적으로 사용하기 위해 JVM상에서 String을 다른 객체들과 차별되게 저장되도록 해놓았는데, 그것이 바로 Heap Area 상의 String Constant Pool이다.

image-20240519143505108.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val stringA1 = "A"
val stringA2 = "A"

stringA1 == stringA2 // true
stringA1 === stringA2 // true

val stringABC1 = String(charArrayOf('A','B','C'))
val stringABC2 = "ABC"

stringABC1 == stringABC2 // true
stringABC1 === stringABC2 // false
  • String Constant Pool은 플라이웨이트 패턴을 구현한 대표적인 예로 한 번 저장한 변수를 다시 저장하지 않도록 만들어졌다.
  • 따라서 String을 을 이용하여 선언할 경우 먼저 String Constant Pool에 해당 변수가 있는지 확인 후 있으면 기존 값을 참조하도록 주소값을 설정하고, 아니라면 새로운 String은 String Constant Pool에 넣는다.
  • 이러한 과정을 통해 String은 JVM 상에서 매우 효율적으로 동작할 수 있다.
  • 그렇다면, 다른 변수들 처럼 String Constant Pool 바깥에 String을 저장할 수 있는 방법은 없을까? 당연히 있다.
    • ""가 아니라 String 생성자를 이용하여 String을 생성할 경우 Constant Pool 바깥에 생성된다.

PC 레지스터 (Program Counter Register)

  • PC 레지스터는 쓰레드가 시작될 때 생성되며, 현재 수행중인 JVM 명령어 주소(Current Instruction Address)를 저장하는 공간이다.

  • JVM 명령의 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 가지고 있다.

  • 하지만 자바의 PC Register는 CPU Register와 다르다.

  • 자바는 OS나 CPU의 입장에서는, 하나의 프로세스이기 때문에 가상 머신(JVM)의 리소스를 이용해야 한다.

  • 그래서 자바는 CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며,

    • 이를 위한 버퍼 공간으로 PC Register라는 메모리 영역을 만들게 된 것이다
  • 따라서 JVM은 스택에서 비연산값 Operand를 뽑아 별도의 메모리 공간인 PC Register에 저장하는 방식을 취한다.

JVM의 Stack, Heap 할당 방식

  • Stack에는 원시 타입 변수 값이나, Heap을 가리키는 주소값 둘 중 하나만 저장된다.

  • Stack은 좁은 메모리 공간이지만, Heap은 넓은 메모리 공간이다.

    • Stack 메모리에 값을 할당하고 해제하는 것은 많은 비용이 들지 않지만,

    • Heap 메모리에 값을 할당하고 해제하는 것은 많은 비용을 요한다.

JVM 프로세스와 멀티스레드

  • JVM(가상 머신)은 하나의 프로세스 이다.
  • 하나의 프로세스는 여러 작업 단위를 가질수 있는데 이 작업 단위를 바로 스레드라고 한다.
  • JVM에서는 Main Thread라 불리는 쓰레드가 있고, 우리가 main() 메서드를 사용해 불리는 것이 바로 Main Thread이다.
  • Main Thread가 종료되면 나머지 Thread 들도 자동으로 종료가 된다

멀티 쓰레드의 경우 여러 쓰레드가 하나의 Heap 영역의 변수에 동시 접근하여 변경이 생기면, 동기화 이슈 발생

해결법

  1. 불변(읽기 전용) 변수로 선언, val
  2. 가변 변수일 경우 해당 값을 변경하는 연산을 @Synchronized 키워드를 이용해 한번에 하나의 스레드에서만 접근할 수 있게 막아야 함
  3. 가장 좋은 방법은 불변 변수로 선언하고, 임계 구역에서 접근하는 변수는 변화하지 않게 하는 것이다.
1
2
3
4
5
6
7
8
val immutableString = "Kotlin World

@Synchronized
fun append(str: String?): StringBuffer? {
    toStringCache = null
    super.append(str)
    return this
}
  • 최근 CPU는 기본적으로 여러개의 코어를 가지고 있어 여러 개의 스레드를 활용하는 프로그래밍이 가능하다.

  • 따라서 멀티스레드를 활용하는 비동기 프로그래밍이 요즘 경향이다.

  • 비동기 프로그래밍이란, 연산을 다른 스레드로 넘겨버리고, 현재 스레드에서는 다른 작업을 할 수 있도록 하는 프로그래밍 방식이다.

    • 비동기 프로그래밍의 이점은 메인 스레드를 blocking 하지 않고 다른 스레드에서 작업을 수행하는 것이 가능하다는 것이다.

    • UI가 있는 프로그램에서는 보통 메인스레드가 UI를 제어하게 되는데 메인 스레드가 non-blocking되면, UI가 끊기지 않아 사용자 경험 또한 개선시킬 수 있다.

JVM에서 GC

image-20240519150244342.png

  • GC란, Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage) 를 모아 주기적으로 제거하는 것

  • 자동으로 처리해준다 해도 메모리가 언제 해제되는지 정확하게 알 수 없어 제어하기 힘들며,

  • 가비지 컬렉션(GC)이 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 발생되는 문제점이 있다. => Stop the world

GC 대상

image-20240519150935951.png

GC 방식

  • Mark-Sweep 이란 다양한 GC에서 사용되는 객체를 솎아내는 내부 알고리즘이다.

  • 가비지 컬렉션이 동작하는 아주 기초적인 청소 과정이라고 생각하면 된다.

image-20240519151135000.png

  • 모든 머신들이 그렇듯 JVM 또한 사용되지 않는 객체들이 제때 메모리에서 정리되지 못하고 한 번에 정리되거나 한다면 앱이 버벅거리거나, 제대로 동작하지 않을 수 있다.

  • 또한 만약 사용되지 않는 객체가 GC의 대상이 되지 못한다면 Out of Memory Error 로 인해 앱이 강제 종료될 수도 있다.

  • Young Generation 영역은 짧게 살아남는 메모리들이 존재하는 공간이다.

    • 모든 객체는 처음에는 Young Generation에 생성되며, Young Generation의 공간은 Old Generation에 비해 상대적으로 적기 때문에 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸린다.
    • 작은 공간에서 데이터를 찾기 위해 걸리는 시간이 적기 때문이다. 이 때문에 Young Generation 영역에서 발생되는 GC는 Minor GC라 불린다.
  • Old Generation은 길게 살아남는 메모리들이 존재하는 공간이다.

    • Old Generation의 객체들은 처음에는 Young Generation에 의해 시작되었으나, GC 과정 중에 제거되지 않은 경우 Old Generation으로 이동한다.
    • Old Generation은 Young Generation에 비해 상대적으로 큰 공간을 가지고 있으며 이 공간에서 메모리 상의 객체 제거에 많은 시간이 걸린다.
    • 이 때문에 Old Generation에서 발생되는 GC는 Major GC라 불린다.

image-20240519150503084.png

  • Eden -> Survior0 -> Surviior1 이래도 GC에서도 살아남는다면, Old Generation 영역으로 넘어간다.

image-20240519150614980.png

  • Old Generation에서는 Major GC가 일어난다.
  • Major GC는 매우 큰 공간이기 때문에 데이터를 지우는데 많은 시간이 걸린다.
  • 또한 Major GC가 일어나면 Thread가 멈추고 Mark and Sweep 작업을 해야 해서 CPU에 부하를 주기 때문에
    • Major GC가 자주 일어나는 앱들에서는 GC가 일어날 때마다 멈추거나 버벅이는 현상이 발생한다.
  • 따라서 최대한 Major GC는 일어나지 않도록 하는 것이 좋다. 제때 참조(Reference)를 해제하고, 오래 붙잡는 객체를 최소화해야 이러한 장애 상황을 최소화 할 수 있다.