JIT 컴파일 우리가 작성한 소스코드를 컴퓨터가 실행하도록 만들기 위해서는 컴퓨터의 명령어로 변환해줘야 한다. C/C++같은 언어들은 실행하려는 컴퓨터의 환경에 맞게 미리 컴파일해서 실행가능 파일을 만든다. 반면, Python이나 JavaScript같은 인터프리터 언어들은 소스코드를 한 줄씩 읽어 실행가능한 명령어로 변환한다. 컴파일 언어는 실행가능 파일을 실행만 하면 되기 때문에 비교적 성능이 좋고, 인터프리터 언어는 속도는 비교적 느리지만 인터프리터가 있는 환경이라면 어디서든 같은 코드를 실행시킬 수 있다는 장점이 있다. Java는 독특하게 컴파일 언어의 특징과 인터프리터 언어의 특징을 모두 가진다. 소스코드를 컴파일해서 바이트 코드로 변환하고, 바이트 코드는 JVM의 실행 엔진에 있는 인터프리터로 ..
JVM

바이트 코드는 어떻게 생겼나? 우리가 작성한 Java 코드는 자바 컴파일러에 의해 바이트 코드로 변환된다. 바이트 코드는 클래스 로더에 의해 JVM 런타임 메모리에 올라간다. JVM의 실행 엔진의 인터프리터가 메모리의 바이트 코드를 한 줄씩 실행한다. 여기까지 알았다면, 이제 바이트 코드를 직접 확인해보자. 아주 간단한 아래와 같은 자바 코드가 있다고 하자. public class Test { public static void main(String[] args) { String hello = "Hello"; String name = "Java"; System.out.println(hello + " " + name + "!"); } } 위 코드를 컴파일한 다음, 우리가 알아볼 수 있게 디컴파일해서 확인해..
움직이는 객체 JVM을 공부해 본 사람이라면 GC가 일어나는 과정을 공부해 본 적이 있을 것이다. 가비지 컬렉터는 메모리를 여러 영역으로 나눠서 관리한다. 새롭게 생성된 객체는 Eden 영역에 할당되고, GC가 일어날 때까지 살아남은 객체들은 Survivor 영역으로 이동된다. 이렇게 몇 차례를 살아남은 객체들은 Old 영역으로 이동한다. GC 알고리즘에 따라 조금씩 다를 수 있지만 기본적인 원리는 그렇다. 어쨌든, 우리의 직관적인 생각과는 다르게 한 번 생성된 객체들은 메모리 상에서 가만히 있지 않고 끊임없이 움직인다. 이 말은 객체의 주소값이 계속 변한다는 것이고, 따라서 오래된 객체를 참조하면 segmentation fault가 발생하게 된다는 뜻인가? 물론 Java 개발자라면 이런 경우를 본 적이..

마이크로 벤치마킹 거리 측정할 때를 생각해보자. 멀리뛰기에서는 cm단위로 측정할 것이고, 내비게이션은 km단위로 측정한다. 몸무게는 kg단위로, 자동차 무게는 t단위로 측정할 것이다. 이처럼 무언가를 측정할 때는 특정한 '스케일'이 중요하다. 성능 테스트도 마찬가지다. 수 많은 마이크로 서비스로 이루어진 시스템에서 응답 시간을 측정할 때와, Java 메소드 하나를 실행하는 데 걸린 시간을 측정할 때는 우리의 관심사가 다르다. Java 메소드의 실행은 상대적으로 짧은 시간에 일어나기 때문에 JVM의 동작에 많은 영향을 받는다. 예를 들면 GC에 의해 STW가 일어나는 시간, JIT 컴파일러에 의해 최적화가 되면 시간이 우리의 의도와 다르게 측정될 수 있다. 이렇게 비교적 작은 Java 코드에 대한 벤치마크..

Native Image JVM을 공부하다가 JVM 없이도 Java 어플리케이션을 실행할 수 있다는 재밌는 사실을 발견했다. C언어를 컴파일하면 실행가능한 파일이 생성되는 것처럼, Java를 바이트코드가 아닌 실행가능한 파일로 컴파일할 수 있다는 뜻이다. 이러한 파일을 Native Image라고 한다. 이렇게 Native Image를 실행하면 JVM에서 실행하는 것이 아니기 때문에 시작 시간이 빠르며, 메모리를 아낄 수 있다고 한다. GraalVM Native Image를 만들기 위해서는 GraalVM이 필요하다. GraalVM은 기존에 C++로 작성된 JIT 컴파일러를 Java로 대체해 Java로 Java를 실행하는(meta-circular Java) 목표를 가지고 만든 JVM이다. 뿐만 아니라, 컴파일..

런타임 데이터 영역 이전 글에서 클래스 로더와 실행 엔진에 대해 살펴보았다. 이제 남은 것은 JVM 실행시에 데이터를 저장하는 런타임 데이터 영역이 남았다. 사실 Native Method Interface와 Native Method Library가 있긴 하지만, 이름에서 알 수 있는 내용이 전부다. Java에서 C/C++같은 다른 언어들을 사용할 수 있도록 해주는 역할이다. [이미지 출처] https://www.baeldung.com/java-compiled-interpreted 다시 런타임 데이터 영역으로 돌아오면, 위의 그림에서 볼 수 있듯이 5가지 영역으로 구성된다. 하나씩 살펴보도록 하자. 메소드 영역 메소드 영역은 이미 본 적이 있다. 클래스 로더에서 바이트코드를 로딩하면 바로 이 메소드 영역에..

바이트코드 실행 바이트코드를 열어보면 명령어는 조금 다르지만 어셈블리어와 비슷한 형태로 만들어진 것을 확인할 수 있다. 실제 CPU에서 어셈블리 명령어를 읽어서 실행하는 것처럼, JVM은 바이트코드 명령어들을 한 줄씩 읽어서 명령어의 종류에 따라 동작을 실행한다. 이러한 바이트코드는 실제 CPU에서 실행할 수 있는 형태가 아니기때문에, 이것을 CPU에서 실행할 수 있는 형태로 변경하는 작업이 필요하다. 이러한 작업을 해주는 것이 JVM의 실행 엔진이다. 실행 엔진은 크게 3부분으로 구성되어있다. 바이트코드 명령어를 하드웨어 명령어로 해석해서 실행하는 interpreter, 성능을 개선하기 위해 자주 사용되는 바이트코드를 미리 하드웨어 명령어로 컴파일하는 JIT(Just In Time) 컴파일러, 그리고 ..

JVM의 이해 JVM은 Java Virtual Machine의 약자로, 이름 그대로 Java 코드를 실행해주는 가상 머신이다. 일반적인 가상 머신이 하드웨어의 동작을 소프트웨어로 구현하여 호스트 컴퓨터에 다른 운영체제를 실행할 수 있는 것처럼, JVM 역시 호스트의 환경과 관계없이 Java 프로그램을 실행할 수 있는 환경을 제공해준다. 이러한 특징 덕분에 'Write Once, Run Anywhere'라는 문구가 나오기도 했다. 또한, 이름과 달리 Java 뿐만 아니라 Kotlin, Scala 등 다양한 언어들을 지원한다. 이러한 언어들은 Java가 서버 개발에 널리 사용된 이후에 Java의 부족한 부분들을 개선하기 위해서 등장했기 때문이다. 어쨌든 JVM을 사용하는 개발자라면 JVM의 구조에 대해 이해..