자바를 사용할것이라면 자바에 대해서 능통해야 한다는 생각이 늘 있었고!
올해 초 백기선님의 Live Study라는 컨텐츠를 보게 됐고!
늦게나마 참여해보고자 이렇게 키보드를 두들깁니다!
1주차의 과제는 JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가!
세부 내용은 아래와 같다!
Java에서 모든 소스 코드는 .java 확장자로 끝나는 플레인 텍스트 파일로 작성된다.
그런 다음 해당 소스 파일은 javac 컴파일러
에 의해 .class파일로 컴파일 됩니다.
.class 파일은 프로세서(CPU) 고유의 코드가 아닌 JVM(Java Virtual Machine)의 언어인 바이트 코드가 포함되어 있다.
따라서 자바 런처 툴은 JVM의 인스턴스를 사용하여 애플리케이션을 실행 한다.
[OS에 구애받지 않는다]
바로 동일한 바이트 코드로 변환해 JVM이 어떤 OS에서도 동작할 수 있게 중개자의 역할을 하기 때문이다. 즉, Java는 OS에 구애받지 않고 재사용을 할 수 있게 해준다.
자바 응용 프로그램은 운영체제, 하드웨어가 아닌 JVM하고만 통신하고, JVM이 해당 운영체제가 이해할 수 있도록 변환하여 전달한다.
자바 응용 프로그램은 운영체제에 독립적이나, JVM은 운영체제에 종속적이라 볼 수 있다. 따라서 각 운영체제에 맞는 서로 다른 버전의 JVM이 존재한다.
[스택 기반의 가상 머신]
가상 머신이라면 물리적인 CPU에 의해 처리되는 동작을 흉내낼 수 있어야 한다. 따라서 아래의 개념을 구현(포함) 해야함
이를 구현하는 방식으로 스택 기반, 레지스터 기반이 존재하는데 JVM은 스택 기반의 VM이다. 이는 피연산자를 저장하고 가져올 때 스택을 활용한다. 이에대한 장단점은 아래와 같다.
장점
단점
[심볼릭 레퍼런스]
기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
심볼릭 레퍼런스
: 이는 컴파일까지는 위 타입들을 논리적인 참조를 하지만, Runtime 시점에 실제 물리적인 주소로 대체되는 작업인 Dynamic Linking 작업을 한다.
[가비지 컬렉션]
클래스의 인스턴스는 사용자 코드에 의해 힙 영역에 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다. Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않는다 때문에 GC가 더 이상 필요없는 객체를 찾아 지우는 작업을 한다.
[명확한 기본 자료형의 정의]
C, C++은 플랫폼에 따라 int형의 크기가 변함, 하지만 JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장해줌
컴파일이란 .java
형식의 파일을 JVM이 해석할 수 있는 바이트 코드로 변환하는 작업을 말한다.
즉 HelloWorld.java가 컴파일 되면 HelloWorld.class 파일로 변환되며 .class파일과 JVM을 이용해 해당 파일을 실행하게 된다.
실제 동작을 직접 확인해 보자
위와 같은 메모장 파일을 C:\Java\test\HelloWorld.java에 둔다.
이후 cmd를 이용해 해당 경로로 이동하고 아래 명령어를 입력한다.
다음과 같이 아무런 메시지 없이 다음 명령어를 입력할 창이 나오면 컴파일이 완료된 것이다. 해당 폴더를 다시 열어보면 HelloWorld.java가 컴파일되어 생성된 HelloWorld.class라는 바이트 코드의 파일이 보일것이다.
자바 컴파일러는 JVM이 해석할 수 있는 바이트 코드로 .java 파일을 변환시켜 준다.
이때 .java파일을 소스파일이라고 하고 컴파일된 .class파일을 목적파일이라고 한다.
이러한 특성으로 모든 .java파일은 .class 바이트 코드로 변환되는데 이것 또한 Java가 OS에 관계없이 플랫폼 독립적으로 실행 가능한 환경을 제공하기 위함 입니다.
자바의 컴파일의 결과물로 .java파일을 JVM스펙의 class 파일 구조에 맞는 코드를 바이트 코드라고 한다.
바이트코드는 아래와 같은 과정을 거쳐야 한다.
로딩
(클래스 파일 가져와 JVM메모리에 로드)링킹
(검증, 준비, 분석)초기화
(클래스 변수들 초기화 static 필드들을 설정된 값으로 초기화)바이트코드는 javap 명령어로 확인할 수 있다. 방금 만든 HelloWorld.class파일을 확인해보자
❯ javap -v -l -p HelloWorld.class
Classfile /Users/lhoseok/Java/test/HelloWorld.class
Last modified 2022. 6. 29.; size 427 bytes
MD5 checksum 1ce421a2eb9ab6cb795be6e54f7d09a9
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // HelloWorld
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World!!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World!!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public HelloWorld();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "HelloWorld.java"
여기서 {, } 사이에 존재하는 코드들을 바이트 코드라 말하고, 다음과 같은 구조로 이루어져 있다.
대부분의 명령어들은 오퍼랜드 스택에 값을 넣고, 빼고, 읽고, 복사하고, 스왑 및 메서드 호출의 내용을 담고 있고, 어셈블리어와 비슷한 양상을 보인다.
또한 사진을 보면 기존에 우리는 생성하지 않았던 default 생성자(public HelloWorld();)가 생성된 것을 볼 수 있다. 자바를 공부해봤다면 생성자를 생략해도 기본 생성자가 생성된다는 것을 바이트 코드로 확인할 수 있다.
바이트 코드는 정말 많은 명령어들이 존재하고 이 블로그에 정말 자세히 설명되어 있으니 궁금하다면 꼭 참고하길 바란다.
바이트 코드(HelloWorld.class)파일이 생성된 것을 확인했으니 아래와 같이 명령어를 입력하면 우리가 출력하고자 하는 Hello World!! 문구가 출력될 것이다.
오라클 Tools Reference에서 설명하는 Java application의 실행 방법은 다음과 같다.
과연 위의 의미는 무엇일까? JRE의 시작을 알기전에 JDK, JRE, JVM의 관계를 알아보자
위의 사진을 보면 다음과 같이 해석될 수 있다.
따라서 JDK를 이용해 바이트 코드를 만들고 -> JRE를 이용해 바이트코드를 실행하면 -> JVM에서 실질적인 바이트코드의 실행이 이루어진다.
자세한 실행과정은 다음 블로그를 참고하자 -> HomoEfficio
위에서 자바를 실행하는 과정을 살펴볼때 JDK와 JRE의 간략적인 차이를 배웠다.
좀 더 자세하게 JDK와 JRE의 차이를 알아보자
JDK 및 JRE의 특징을 알아보자
JDK
JRE
위의 설명을 봤을때 JDK는 JRE를 모두 포함하고 있고 부가적으로 자바 컴파일러 Java 애플리케이션 런처, Appletviewer의 기능을 제공해준다.
반대로 JRE는 Java 프로그램을 작성하고 컴파일 하는 기능은 없으나, JVM이 실제로 구동되는 도구들의 모음이며 실행을 위한 도구들이 모여있다.
JDK는 JRE의 상위 집합이되고, JVM은 JRE의 하위 집합이다. (JDK > JRE > JVM)
이 3개 모두 플랫폼(OS)에 따라 다르지만 JVM은 특히 플랫폼에 크게 의존한다.
이들이 플랫폼에 종속적이게 되면서, 실제로 작성되는 Java 코드들은 플랫폼에 독립적일 수 있어진다. 아래 표는 실제 JDK, JRE, JVM의 차이를 보여준다.
1번에서 JVM이 무엇인지 살펴보았다 그렇다면 JVM은 어떤 요소로 구성되어있고 그들의 역할은 무엇인지 알아보자.
먼저 JVM은 Java Virtual Machine의 약자로 컴파일된 자바 바이트 코드를 실행할 수 있는 주체이며, 여기서 바이트 코드를 해당 운영체제가 이해할 수 있는 기계어로 변경을 한다(따라서 운영체제에 종속적)
JVM의 구성요소를 알기위해 제일 처음 사용했던 사진을 다시 가져왔다.
JVM은 Garbage Collector
, Execution Engine
, Class Loader
, Runtime Data Area
와 같이 크게 4개로 구성요소를 나눌 수 있다.
컴파일된 자바의 바이트 코드(.class)들을 엮어서 JVM이 운영체제로부터 할당받은 메모리영역인 Runtime Data Area로 적재하는 역할을 한다. (자바 애플리케이션이 실행중일때 작업 수행됨) 내부적으로 3개의 주요 기능을 수행한다.
위 3가지 과정의 자세한 내용은 다음 링크에서 확인할 수 있다. -> 링크
Class Loader에 의해 메모리에 적재된 바이트코드(클래스)들을 기계어로 번역해 명령어 단위로 실행하는 역할을 한다. 이 때 한 줄씩 읽는 Interpreter
방식이 존재하고, JIT(Just-In-Time)
컴파일러 방식이 존재한다. JIT 컴파일러는 전체 바이트 코드를 네이티브 코드로 변경해 실행 엔진이 네이티브로 컴파일된 코드를 실행하는 것으로 성능을 높여준다. 다음절에 더 자세하게 알아보자
흔히 GC라고 많이 부르는 이것은 Heap 메모리 영역에 생성된 객체들 중에서 참조되지 않는 객체들을 탐색 후 제거하는 역할을 한다.
GC가 수행되는 동안 GC를 수행하는 쓰레드가 아닌 다른 쓰레드들은 모두 일시정지 상태가 된다.
따라서 만약 Full GC가 발생하면 전체 시스템이 일시정지되고 곧 장애로 이어질 수 있다.
GC의 동작은 아래 두 가지의 가정에 의해 등장했다.
위 두가지 가정의 장점을 살리기 위해 HotSpot VM에서는 크게 2개로 물리적 공간을 나눈다.
Young Generation
: 새롭게 생성한 객체의 대부분이 위치, 대부분이 금방 Unreachable상태가 되므로 생성되고 금방 사라짐 이를 Minor GC가 발생한다 말함Old Generation
: Young Generation에서 살아남은 객체가 여기로 복사됨, 대부분 Young Generation보다 크게 할당하며, 크기 덕분에 GC는 상대적으로 적게 발생함 여기서 객체가 사라지는 것을 Major GC(혹은 Full GC)라 말함JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.
다음과 같이 메모리의 영역이 나뉘게 된다.
new 키워드
로 생성된 객체와 배열이 생성되는 영역, 메소드 영역에 로드된 클래스만 생성이 가능하며 GC가 참조되지 않은 메모리를 확인하고 제거하는 영역이다.지역 변수
, 파라미터
, 리턴 값
, 연산
에 사용되는 임시값들이 생성되는 영역이다. 객체를 생성할때 객체의 이름이 저장되며 객체의 이름은 힙의 실제 new로 생성된 객체를 가리킨다(참조), 메소드를 호출할 때마다 개별적으로 스택이 생성된다.쓰레드가 생성됐을때 위 영역들의 공유 정보는
공유
: 메소드 영역, 힙 영역은 모든 쓰레드가 공유비공유
: 스택영역, PC Register, Native Method Stack은 각각의 쓰레드마다 생성됨컴파일된 바이트코드(클래스)는 클래스 로더에 의해 Runtime Data Area로 보내지고, 이 영역의 저장된 바이트코드를 가지고 Execution Engine이 운영체제가 해석할 수 있는 언어인 기계어로 번역해 실행하게 된다. 이 때 인터프리터 방식과 JIT 컴파일러 방식이 존재한다.
다만 JIT 컴파일러가 컴파일하는 과정은 인터프리팅의 속도보다 훨씬 오래걸리므로, JIT을 사용하는 JVM은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하며 일정 정도를 넘으면 JIT 컴파일러가 컴파일을 수행한다.
어떻게 마무리 해야할까..?
백기선님의 라이브 스터디의 1 주차 스터디의 공부 및 정리를 마쳤다.
회고를 해보자면.. 아직 글이 전체적으로 다듬어지지 않고, 내 생각보단 참조를 더 많이 한 것 같아 아쉬움이 남는다. 아무래도 내 생각이란 것은 내가 정의한 개념, 철학이라 생각 된다. 이것을 하기 위해선 충분한 지식의 학습과 정확한 정보를 알고 있어야 좀 더 자유롭게 서술할 수 있을것 같은데.. 이 말은 더 열심히 공부해야겠는 말과 똑같네? 더 노력하자!
(어떤 글이던지 글을 쓰고나서 두고두고 차차 수정해 나가며 나만의 생각, 개념들을 보태봐야 할 것 같다.)
아직 1주차이지만 헷갈렸던 JVM, JDK, JRE의 개념을 제대로 알 수 있었고 더 나아가 내부적인 동작들도 전체적으로 공부할 수 있었다. 앞으로도 정진합시다!!
[블로그]