Kotlin Coroutine 동작을 이해해보기 - JVM에 대해서...
지난 9월 12일, droid knight 2023에 다녀왔었고, 그곳에서 김준비님의 Coroutine Deep Dive 에 대한 강연을 듣고나서, 아직도 Coroutine에 대해서 잘 알지 못하고 있었다고 생각해서, 다시 공부해야봐야겠다는 생각을 했습니다.
강연을 듣다보니, 왜 Coroutine이 경량화된 쓰레드를 사용하는 것 처럼 동작하는지에 대해서도 설명이 나왔는데,
오랜만에 JVM 구조나 동작을 떠올려보려고 했더니, 기억이 나지 않아서 다시금 공부하여 정리하는 기회를 가져보려고 합니다.
이를 통해 kotlin에서 coroutine이 왜, 어떻게 light-weight thread 처럼 동작하는지 이해해보고, 다음 포스트에서 코루틴에 대해서 정리를 이어나가보도록 해보겠습니다. (사실상 part one 임)
해당 포스트에서는 kotlin coroutine에 대한 내용은 많이 없으니, coroutine의 내용을 알고 싶은 분은, 앞으로의 포스트에서 확인해주세요
자바? JVM?
먼저 자바와 JVM에 대해서 알아보자면, 보통 프로그래밍을 시작하면 절차지향 언어인 C를 배우고, 그 이후에 객체지향 언어인 Java에 대해서 배우게 됩니다. (학교에서 그랬습니다) 그땐 어쩜 그리 Java가 재미 없던지... C로 개발자 해먹고 살아야지라는 생각을 가졌었습니다.
그러다가 안드로이드를 시작하면서 Java의 재미에 대해, 객체지향의 재미에 대해서 알게 됐고, 지금은 코틀린으로 넘어오게 되었습니다.
본론으로 다시 돌아와, Java를 공부하며 Java의 장점중에서, 운영체제에 독립적이다 라는 문구를 많이 보셨을 겁니다 (외에도 객체지향, 많은 라이브러리, 메모리 자동 정리 등 수많은 장점이 있습니다). 정확히 어떤 것 때문에 운영체제에 독립적이고, 메모리를 어떤식으로 사용하는지 까지 알아보겠습니다.
먼저 JDK (Java Development Kit)는 위와 같이 구성되어 있는데요, Java가 운영체제(OS)에 종속적이지 않지만, 각 OS에서 JDK를 설치해주어야 합니다. 그래야 그 안에 들어있는 javac, JVM, 다양한 libraries를 사용할 수 있기 때문입니다.
먼저 우리가 Person.java 라는 java 파일을 생성하면, 실행할 때 Java의 컴파일러(javac라고 불립니다)는 우리가 작성한 Person이라는 java 파일을 Person.class 라는 class 파일로 변경해줍니다. 이를 컴파일한다고 합니다.
즉, 우리가 작성한 소스코드를 자바 컴파일러가 컴파일 해줍니다.
후에 컴파일된 파일에 대해서 Class Loader를 통해 JVM의 Runtime Data Area로 로딩해줍니다. 로딩된 파일들은 Execution Engine을 통해 해석하여 각각 수행됩니다.
이때, Runtime Data Area의 구조를 알면 매우 좋습니다.
Method Area와 Heap Area는 모든 쓰레드가 공유하여 사용하고, Stack, PC Register, Native method Stack은 각 쓰레드마다 한 세트식 가지고 있습니다. 각각의 영역에 대해서 짤막하게 정리해보자면,
1. Method Area
- Method Area에는 클래스 정보, 타입 정보(interface, class) ,클래스 멤버 변수 정보, 접근 제어자 정보, static 변수, 상수, final class 정보 등을 저장하는 곳입니다.
2. Heap Area
- 힙 영역은 new 키워드로 생성된 객체와 배열이 생성되는 영역입니다. 힙 영역에서는 Garbage Collector(이하 GC)가 지우기 전까지는 생성된 데이터들은 존재하고, 여러 최적화를 통해 사용하지 않는 객체들(Reachability를 체크하여)을 GC가 찾아내어 제거해줍니다.
- 여기서 재미있는 사실이 하나 있는데, primitive type(int, long, boolean, float 등)을 제외한 타입을 Reference type이라고 하는데, 이를 스택에 넣었다 뺐다하는 건 메모리상 비효율 적이라서 heap 영역에 정보를 저장하고, 그 정보의 주소를 참조하는 변수를 스택에 저장합니다.
쉽게 말해서, primitive type은 stack에 변수와 값을 한번에 저장하고, Reference type의 실제 객체 자체는 heap에 저장하지만, heap의 객체를 불러오기 위해 stack영역에 객체의 주소값을 저장합니다. 아래 사진과 같이요!
GC의 동작 원리 참고는 해당 유튜브를 보시면 이해가 쉽습니다! https://www.youtube.com/watch?v=Fe3TVCEJhzo
3. Stack Area
- 스택 영역에는 지역 변수, 메소드의 매개 변수, 임시 변수, 메소드 정보를 저장하고, 해당 메소드 호출이 종료시에 선언된 변수들은 제거됩니다.
4. PC Register(Program Counter Register)
- pc register는 쓰레드가 현재 어떠한 명령을 실행할지, 몇 번째 라인을 실행할지에 대한 정보를 담고 있습니다.
- JVM은 stack-base로 작동하여 JVM은 CPU에 직접 Instruction을 수행하지 않고, Stack에서 Operand를 뽑아내 이를 별도의 메모리 공간에 저장하는 방식을 취하는데, 이러한 메모리 공간을 PC Registers라고 한다.
(Stack base로 Stack에서 연산자를 뽑아 메모리 공간을 저장한다..라고 하는데, 잘 이해는 안가서 그냥 넣었습니다... 그냥 쓰레드가 어디 라인을 실행하는지에 대한 정보를 담고 있다고만 이해하고 있습니다)
5. Native Method Stack
- Java 이외의 언어로 작성된 네이티브 코드를 위한 메모리 영역입니다.
이렇게 Runtime Data Area에 바이트 코드들이 잘 배치가 되었다면, Execution Engine을 통해 명령어 단위로 한줄한줄 실행이 됩니다.
이전의 JVM에서는 인터프리터 방식으로 한줄한줄 실행했었지만, 지금은 인터프리터 방식을 사용하다가, 일정 기준을 넘어가면 런타임에 한꺼번에 변경하여 실행하는 JIT(Just in time)으로 업그레이드 되었다고 합니다. (더 자세하게는 4가지 레벨로 분류하여 캐싱도 하고 ...한다는데, 더 자세하겐 찾아보지 않았습니다 궁금하다면 요기를 -> https://kotlinworld.com/307)
------------
즉 한줄로 정리하자면, 우리가 작성한 자바 코드들은 위와 같이 javac가 컴파일 한뒤 바이트 코드 단위로 JVM에서 메모리에 올리고, Execution Engine이 한줄 씩 실행시켜준다는 것입니다.
------------
그런데, 이게 왜 코루틴과 관련이 있을까요? 코루틴의 공식 문서를 보면 아래와 같이 경량화된 쓰레드처럼 생각될 수 있다고 합니다 (이전엔 아예 그냥 경량화 된 쓰레드라고 했던 것 같은데... 기억의 오류인지....)
코루틴이 위의 코드를 빠르게 수행할 수 있는 것은 왜일까요? 바로, 코루틴은 Thread와는 상관이 없기 때문입니다.
위에서도 말했지만, Stack은 각 쓰레드마다 존재합니다. 즉, 쓰레드 끼리는 메모리를 공유하고 있지 않기 때문에, Stack 안의 데이터는 공유할 수가 없습니다. 그래서 다른 쓰레드 스택의 정보가 필요하다면 Context Switching을 해야합니다. 그런데, 우리는 컴퓨터 이론때도 배웠겠지만, Thread의 Context Switching은 매우 리소스가 많이 든다고 알고 있습니다 (context switching , overhead란 검색어로 찾아보면 매우 많이 나옵니다).
하지만, Coroutine을 사용한다면 이런 Context Switching 없이 사용이 가능합니다. 이를 통해 더 적은 쓰레드를 사용하고, 효율적으로 사용하며 빠르게 작업을 수행할 수 있는 것입니다.
관리의 주체를 Stack에서 Heap으로 변경하여 Thread Context Switching을 통해 하던 job 들을, Coroutine Object에게 할당하여 Context Switching을 하지 않아도 되게끔 만든 것입니다.
가장 중요한 사진이지만, 여기까지 오기 꽤 오래 걸렸죠? 다만 JVM에서 어떤식으로 메모리 구조를 사용하고, 어떠한 점에서 코루틴이 빠른지 알게된 시간 같습니다.
이번에 JVM과 메모리 구조에 대해서 정리했으니, 다음 시간엔 코루틴에 대해서 정리해보겠습니다.
https://doohong.github.io/2018/03/02/Java-runtime-data-area/
https://velog.io/@haron/Call-by-Value-Call-by-Reference
https://velog.io/@leesrock113/Coroutine%EA%B3%BC-Thread
https://aaronryu.github.io/2019/05/27/coroutine-and-thread/