Java는 C, C++과 달리 Garbage Collector에 의해 자동으로 메모리가 관리되는 Managed Language다.
C로 작성된 프로그램은 기계어로 컴파일/링크 되지만 Java로 작성된 프로그램은 javac라는 Java 컴파일러에 의해 확장자가 .class인 자바 바이트코드로 컴파일된다.
컴파일 된 자바 바이트코드는 JRE의 classloader에 의해 JVM이라는 Java 가상 머신 상에서 읽어들여서 실행된다.
즉, Java로 작성된 프로그램을 실행시키기 위해서는 반드시 JVM이 필요하다.
위 도표를 보면 JDK(Java Developmentkit) 내에 javac 등의 다양한 툴과 함께 JRE(Java Runtime Environment)가 속해있다.
그리고 JRE 내에는 JVM(Java Virtual Machine)과 Java Class Library가 들어있다.
Java로 작성된 프로그램의 메모리 관리를 담당한다.
내가 아는 메모리 관리 방법은 3가지가 있는데,
첫째가 C의 malloc
, free
를 통한 수동 메모리 관리
둘째가 Modern C++과 Rust의 RAII 패턴을 적용한 반자동 메모리 관리(구분을 위해 표현을 이렇게 했다.)
셋째가 GC를 통한 자동 메모리 관리다.
여기서 말하는 메모리는 각 프로세스가 갖는 독자적인 Heap 영역을 가리키며,
Java는 Managed Language, 자동으로 메모리가 관리되는 언어이기 때문에 GC가 존재한다.
// ...
{
Object o = new Object();
// o lives until here
}
Object
는 Reference Type이기 때문에 Heap에 저장될 것이고,
실제 o
는 단순한 4 or 8byte 주소를 가리키는 비트패턴으로 Stack에 저장될 것이다.
Stack은 SP(Stack Pointer)를 이동시키면 그만이므로 따로 메모리 관리를 할 필요가 없다.
그러나 Heap은 반드시 사용 후에 어떤 방식으로던 메모리 해제를 해야한다.
그렇지 않으면 메모리 누수가 발생하게 될 것이다.
어떤 변수도 참조하지 않는 메모리인데도 불구하고 사용할 수 없는 영역으로 인식되는 것이다.
위 코드에서 o
는 블록이 끝나면 수명이 끝나게 된다.
Stack 상의 주소값을 담은 비트패턴은 다음 번에 다른 함수가 실행될 때 다른 값으로 덮어쓰여질 것이다.
이후 Heap 상의 실제 데이터는 참조 중이지 않은 값으로 인식되고
GC, 가비지 컬렉터에 의해 해제된다.
GC도 C/C++ 등으로 작성된 프로그램이고, Java로 작성된 프로그램을 돌릴 때 항상 동반 실행된다.
그래야 동적 할당된 Heap 상의 메모리들을 해제하여 메모리 누수를 방지할 수 있을테니 말이다.
이렇게 Java 프로그램에 반드시 필요한 GC는 JVM에 포함되어 있다.
추가로 JVM은 자바 바이트코드를 읽고 실행하는 기능이 존재한다.
Java Runtime Environment의 약자로, JVM을 포함한 여러 Standard Libraries, 그리고 Java로 작성된 프로그램을 다양한 플랫폼에서 안정적으로 실행시키기 위한 Components를 모두 포함한 개념이다.
JavaScript를 사용해봤다면 Node.js를 생각하면 된다.
Chrome에 탑재된 V8이 JVM과 비슷하게 GC, JS 코드를 읽고 실행할 수 있고,
Node.js는 이를 사용한다.
그러나 Chrome은 브라우저라 IO, 네트워킹 등의 기능은 포함되어 있지 않기 때문에,
추가적으로 이러한 라이브러리들을 묶어서 제공하는 것이다.
Java도 Standard Libraries가 IO, 네트워킹 등의 반드시 필요한 기능을 기본적으로 제공하고 있어서
개발자들이 이러한 Low Level 단에 대해 높은 이해도를 갖고 있지 않더라도 쉽게 구현할 수 있도록 도와준다.
물론 수동으로 메모리를 관리하는 Unmanaged Language들은 이러한 Runtime이 필요하지 않을 것이다.
개발자가 수동으로 메모리를 관리하기 때문에 GC가 애초에 존재하지 않고,
코드가 컴파일, 링크 될 때 Java 바이트코드처럼 중간 단계를 거치지 않고 바로
컴퓨터가 이해할 수 있는 기계어로 변환되기 때문에 즉시 실행이 가능하다.
JVM, JRE을 포함한 디버거, 컴파일러 등의 개발에 필요한 다양한 툴을 모아둔 패키지다.
물론 컴파일이 완료된 Java 바이트코드를 오직 실행만 할 목적이라면 JDK 없이 JRE만으로 가능하다.
Oracle에서 개발한 OracleJDK와, 커뮤니티에서 개발하는 OpenJDK가 있는데,
과거에는 성능과 안정성, LTS 지원 기간 등의 차이로 유료인 OracleJDK를 주로 사용했었지만,
최근에는 Amazon, Red Hat, Intel, IBM 등의 거대 기업들이 OpenJDK 개발에 참여하면서
OpenJDK의 성능이 OracleJDK와 비등해져서 많이 사용한다고 한다.
JDK의 버전과 Java의 버전이 동일하다고 보면 된다.
터미널에 java --version
을 실행하면 openjdk 19.0.2 2023-01-17
과 같이 JDK 버전과 릴리즈 날짜가 나오는데, JDK 버전이 19이므로 Java 19라고 생각하면 된다.
주의할 점은 Java 8까지는 Java 1.8과 같이 앞에 1 prefix를 붙여서 표기했다.
메이저/마이너 업데이트를 구분하기 위해서였다.
그러나 9부터는 prefix를 붙이지 않는다.
그냥 Java 9다.
Java 1.0~1.8, Java 9~19라고 생각하면 될 것이다.
하위 버전의 Java 바이트코드는 상위 버전의 JRE에서 실행할 수 있다.
예를 들어 JDK 1.8로 컴파일 된 바이트코드는 JRE 17에서 실행할 수 있다.
하위 호환성을 보장하기 때문이다.
그러나 하위 호환성이 깨지는 경우가 있을 수도 있는데, 이를 deprecated 되었다고 하며
openjdk나 oracle의 Deprecated List에서 확인할 수 있다.
반대의 경우, 상위 버전인 JDK 17에서 컴파일 된 바이트코드는 하위 버전인 JRE 1.8에서 실행될 수 없다.
JDK 17이 더 많은 기능을 제공하는데 JRE 1.8은 이러한 기능을 포함하지 않기 때문이다.
하위 버전을 Parent class, 상위 버전을 Child class라고 생각하면 편할듯 하다.
Child는 Parent의 모든 멤버 변수와 메소드를 상속받기 때문에 Parent가 될 수 있지만,
Parent는 Child가 독자적으로 갖고 있는 멤버 변수와 메소드를 갖고 있지 않기 때문에 Child가 될 수 없다.
그래서 JDK 17로 컴파일 된 바이트코드를 JRE 1.8에서 실행하면 오류가 발생하거나 제대로 동작하지 않을 것이다.
최근에 Java/Spring 기반의 백엔드 개발을 공부해보기로 결정하여 학습중인데,
JDK, JRE, JVM 등에 대해 좀 더 개념을 잡아두는 것이 좋을 것 같아서 ChatGPT와 Google 검색을 통해 각 개념에 대해 찾아보고 알게된 내용들을 나름대로 정리해두었다.
인간은 망각의 동물이기 때문에 늘 그랬듯 깨끗히 잊어버리거나 애매모호하게 기억이 유지되는 경우에
정리해둔 이 글을 다시 찾아볼 것이고 추가적인 내용을 알게되면 보강해야겠다.