JVM
Java Virtual Machine
JVM JRE JDK JAVA
- JDK
- Java Development Kit 으로, JRE와 개발에 필요한 tool들을 묶은 것
- 오라클 java 11 버전부터는 JDK만 제공
- 기존에는 JRE와 JDK를 별도로 제공했음
- 자바에 모듈시스템이 들어와 JRE를 만들어서 사용할 수 있음 (TODO :jlint)
- JRE
- Java Runtime Environment 로 jvm + Library 형태
- 자바 어플리케이션을 실행할 수 있도록 구성되어 있다. (배포판)
- jvm 핵심 라이브러리, 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다. (rt.jar, charset.jar ..)
- 📌 개발 관련 도구는 포함하고 있지 않음 → jre로 자바 파일 컴파일 불가능 (javac 없음)
- JVM
-
Java Virtual Machine, 자바를 실행하기 위한 가상 기계
- 자바로 작성된 어플리케이션은 모두 JVM 위에서 실행되기 때문에 자바 어플리케이션이 실행되기 위해서는 반드시 JVM (가상 컴퓨터)이 필요하다.
- 자바 바이트코드(.class) 파일을 OS에 특화된 코드로 변환하여 실행
-
자바 바이트코드를 어떻게 실행할지의 스팩
-
- 자바 바이트코드(.class) 파일을 OS에 특화된 코드로 변환하여 실행
- 바이트코드를 실행하는 표준 (JVM 자체는 표준)
- 특정 밴더(오라클, 아마존 ..)가 구현한 구현체
- ex. Oracle-jdk 안의 jvm
- 특정 밴더(오라클, 아마존 ..)가 구현한 구현체
- 특정 플랫폼에 종속적
- 네이티브코드로 바꿔서 실행해야 하는데 네이티브코드가 os 에 맞춰서 실행해야 하기 때문에
- JVM 언어
- JVM 기반으로 동작하는 프로그래밍 언어
- 자바가 아닌 다른 언어로 작성했더라도 JVM 기반으로 동작 가능
- 자바를 위해 JVM이 만들어졌지만, 사실상 자바 언어와 직접적인 연관관계가 있는게 아니라, 클래스파일(
.class
) 만 있으면 JVM에서 실행해주는것이기 때문에 자바와 의존성이 깊지 않다. - 다른 프로그래밍 언어를 사용하더라도 컴파일 했을 때 클래스파일이 만들어진다거나 자바 파일이 만들어지면 JVM을 활용할 수 있다.
- 클로저, 그루비, JRuby, Jython, Kotlin, Scala …
- ex.
fun main(args : Array<String>) { print("Hello, Kotlin") }
kotlinc Hello.kt -include-runtime -d hello.jar
java -jar hello.jar
jar 파일로 컴파일하는 이유는 런타임에 필요한 파일들을 같이 패키징 해주기 때문
- 자바를 위해 JVM이 만들어졌지만, 사실상 자바 언어와 직접적인 연관관계가 있는게 아니라, 클래스파일(
- JVM은 JVM만 제공되지 않음, 최소한의 단위 JRE 형태로 제공됨
-
- JAVA
- 프로그래밍 언어
- 자바로 작성된 소스코드를 JDK에 들어있는 자바 컴파일러(javac)로 컴파일 하여 바이트코드 (.class)파일을 생성하는 것
- 오라클에서 만든 Oracle JDK 11 버전부터 상용으로 사용할 때 유료
JVM 구조
클래스 로더 시스템
.class
에서 자바 바이트코드를 읽어서 메모리의 적절한 공간에 배치하는 시스템- 로딩 → 링크 → 초기화 순으로 진행
-
로딩: 클래스 로더가
.class
파일을 읽고 바이너리 데이터를 만들고 메소드 영역에 다음 데이터를 저장한다.- FQCN (Fully Qualified Class Name) - 패키지, 풀패키지 경로, 클래스 이름, (클래스 로더) - 클래스인지, 인터페이스인지, enum인지 같은 정보 - 메소드와 변수
-
어떤 클래스 로더를 사용했는지 알고싶다면?
클래스로더는 계층형 구조를 갖고있다. 어떤 클래스로더를 사용했는지 알고싶다면 다음과 같이 하면 된다.
ClassLoader classLoader = App.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); System.out.println(classLoader.getParent().getParent());
결과 jdk.internal.loader.ClassLoaders$AppClassLoader@125ddf16 jdk.internal.loader.ClassLoaders$PlatformClassLoader@6bdf26bb null // 부모가 있는데 네이티브 코드로 구현되어 있어 null로 출력된다.(Bootstrap)
최상위 부모가 읽어오고 못읽어올 경우 그 다음 부모가 읽어오고 제일 마지막 클래스 로더가 읽지 못하면
ClassNotFoundException
발생하는 것- 부트스트랩 클래스로더 : 최상위 우선순위를 가진 클래스로더로 JAVA_HOME/lib 에 있는 코어 자바 API를 제공
- 플랫폼 클래스로더: JAVA_HOME/lib/ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽음.
- 애플리케이션 클래스로더: 애플리케이션 실행할 때 주는
-classpath
옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치에서 클래스를 읽음.
-
-
로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성하여 Heap 영역에 저장 (ex.
Class<Test>
) -
링크 (Verify → Prepare → Resolve(Optional))
- Verify : class 파일 형식이 유효한지 체크
- Prepare : 클래스 변수 (
static
변수) 와 기본값에 필요한 메모리를 준비하는 과정. - Resolve : 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체하는 과정인데, 이 과정은 optional이다. 이때 발생할 수도 있고 실제 해당 레퍼런스를 사용할 때 발생할 수도 있다.
-
초기화
static
변수의 값을 할당하고 만약static
블럭이 있다면 이때 실행된다. -
💡 로딩 : 클래스를 읽어오는 과정
💡 링크 : 레퍼런스를 연결하는 과정
💡 초기화 : static 값들 초기화 및 변수에 할당
메모리
- 메소드 영역
- 클래스 수준의 정보 (클래스 이름, 부모 클래스 이름, 메소드, 변수)를 저장한다.
- 이 영역은 공유 자원으로 다른 영역에서도 참조 가능하다.
-
예를들어, 다음과 같은 것들이 메소드영역에 저장된다.
public class Test { static String name; static { name = "yesol"; } public static void main(String[] args) { System.out.println(name); } }
- 힙 영역
- 객체가 저장된다 (인스턴스)
- 공유자원이다.
-
예.
Test test = new Test();
- 스택, PC, 네이티브 메소드 스택 영역
- 힙이나 메소드 영역처럼 모든 영역이 공유하는 자원이 아니고, 스레드에 국한되어 어떤 쓰레드인지에 따라 ㅏ이포그 스레드에서만 공유하는 자원이다.
-
스택 (Stack) : 쓰레드 마다 런타임 스택 생성, 메소드 호출(Method Call)을 스택 안에 스택 프레임이라 부르는 블럭으로 쌓는다. 쓰레드를 종료하면 런타임 스택도 사라진다.
이러한 Method Call 들이 스택 안에 쌓여 있는 것이다.
- PC (Program Counter registers) : 스레드마다 스택이 생성되는데, pc register도 같이 생긴다. pc 레지스터는 스레드 내 현재 실행할 스텍 프레임을 가리키는 포인터로, 스택과 마찬가지로 해당 스택에 국한된다.
- 네이티브 메소드 스택 : 스레드 마다 생성, 네이티브 메소드를 사용할 때 생기는 별도의 스택이다.
- 실행 엔진 (Execution Engine)
- 인터프리터 (interpreter) : 바이트코드를 한 줄 씩 바이트코드로 변환하여 실행
- JIT 컴파일러 (Just In Time) : 인터프리터는 똑같은 코드가 나와도 한 줄 씩 변환하는데 이는 비효율적, 그래서 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 컴파일 → 그 다음부터는 JIT 컴파일러가 네이티브로 컴파일 해 놓은 코드를 바로 사용한다.
- GC (Garbage Collector) : 더 이상 참조되지 않는 객체를 정리한다.
자바 프로그램 작성 및 실행 과정
자바 코드를 작성 후 실행하려면 다음 과정을 거친다.
-
Hello.java 작성
class Hello { public static void main(String[] args) { System.out.println("Hello, Java"); } }
-
컴파일
컴파일이란 사람이 이해하는 언어(원시코드)를 컴퓨터가 이해할 수 있는 언어(목적코드)로 바꾸어 주는 과정으로, 자바 컴파일러는 jvm을 위한 바이트코드를 생성한다.
javac Hello.java
자바 컴파일러를 이용하여 컴파일 하는 방법으로, 위 명령어를 실행하면
Hello.class
클래스 파일이 생성된다.-
생성과정
- IDE 사용시 Ctrl + s, 즉 저장시 ssd 메모리에 java 파일이 저장된다.
- 컴파일러를 RAM에 올린다 (SSD 보다 빠르기 때문에)
- 컴파일러가 java 파일을 읽어 레지스터에 전달한다.
- 레지스터가 java 파일을 한줄 씩 cpu에게 전달하면서 해석 과정을 거쳐 클래스 파일이 생성된다. 생성된 클래스 파일을 컴파일러에게 다시 전달한다.
- 전달 받은 클래스 파일을 다시 SSD 메모리에 저장한다.
-
클래스 파일 확인 방법
javap -c Hello.java
→ 자바 클래스 파일 안에 들어있는 것이
바이트코드
이다.
-
-
실행 (런타임)
java Hello
Hello, Java.
마지막으로 자바 인터프리터 (java.exe)로 실행하면 위에서 작성한 예제를 실행한 결과를 볼 수 있다.
바이트코드
특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법
자바 바이트코드 (Java Bytecode)
.java
파일은 사람이 이해할 수 있는 언어로 작성했기 때문에 컴퓨터는 이해할 수 없으므로 번역이 필요하다.
→ 컴퓨터가 이해할 수 있는형태로 번역하는 것은 jvm이 담당
그러면 우리는 jvm이 이해할 수 있는 형태로 번역을 해서 전달해야 한다 이 형태가 자바 바이트코드이다.
- 자바 바이트코드란 자바 가상 머신이 이해할 수 있는 언어로 변환된 자바 소스코드
- 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1바이트라 자바 바이트코드라고 불림
- 자바 바이트코드는 jvm만 설치되어 있으면 어떤 운영체제에서도 실행 가능
and Methods in the JVM
JVM은 인스턴스와 클래스 객체를 초기화하는데 독특한 메소드를 사용한다.
<init>
<clinit>
Instance Initialization Methods ()
간단하게 객체를 만들고 인스턴스화를 해보자.
Object obj = new Object();
위 코드를 컴파일 하고, 바이트 코드를 보면
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
객체를 초기화하기 위해서 JVM은 <init>
이라는 특별한 메소드를 호출하는 것을 볼 수 있다.
이 메소드는 인스턴스를 초기화 하는 메소드인데,
if and only if : (무조건 다음 조건이 모두 충족되어야 <init>
메소드가 호출됨)
- it is defined in a class
- its name is
- it returns void
각 클래스는 인스턴스 초기화 메소드를 갖고있지 않거나 여러개 갖고있을 수 있다. 이 메소드는 java와 kotlin같은 jvm 기반의 프로그래밍 언어에서 생성자에 해당된다.
자바 컴파일러가 생성자를 어떻게 <init>
하는지 더 잘 이해하기 위해 다른 예제를 보면,
public class Person {
private String firstName = "Foo"; // <init>
private String lastName = "Bar"; // <init>
// <init>
{
System.out.println("Initializing ...");
}
// <init>
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// <init>
public Person() {
}
}
이 코드의 바이트 코드를 보면,
public Person(java.lang.String, java.lang.String);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Foo
7: putfield #3 // Field firstName:Ljava/lang/String;
10: aload_0
11: ldc #4 // String Bar
13: putfield #5 // Field lastName:Ljava/lang/String;
16: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #7 // String Initializing ...
21: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: aload_0
25: aload_1
26: putfield #3 // Field firstName:Ljava/lang/String;
29: aload_0
30: aload_2
31: putfield #5 // Field lastName:Ljava/lang/String;
34: return
자바코드에서는 생성자(Person()
)와 initializer block({ }
)이 분리되어 있지만, 바이트코드상에서는 같은 인스턴스 초기화 메소드에 있는 것을 볼 수 있다.
- index 0 ~ 13 : 첫번째,
firstName
과lastName
필드를 초기화했다. - index 16~21 : 그다음 초기화 블럭 안에 코딩된
println
함수를 실행 - 마지막으로 위에서 초기화한 필드에 생성자 파라미터에 있는 값으로 업데이트(할당) 했다.
만약 Person
객체를 만든다면,
Person person = new Person("younghee", "yesol");
Code:
0: new #2 // class Person
3: dup
4: ldc #3 // String younghee
6: ldc #4 // String yesol
8: invokespecial #5 // Method Person."<init>":(Ljava/lang/String;Ljava/lang/String;)V
11: astore_1
이 때 jvm이 java 생성자와 상응하는 signature(파라미터로 넘긴 값) 를 가진 <init>
메소드를 호출한다.
💡 JVM 세계에서는 생성자와 다른 인스턴스 초기화자가
메소드로 동일하다.
Class Initialization Methods ()
자바에서 클래스초기화 블럭(static initializer blocks) 은 클래스 레벨의 것들을 초기화 할 때 사용한다.
```java
public class Person {
private static final Logger LOGGER = LoggerFactory.getLogger(Person.class);
static {
System.out.println("Static Initializing...");
}
// ...
}
```
static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Static Initializing...
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
바이트코드를 보면 컴파일러는 static block을 클래스 초기화 메소드 (class initialization method)로 번역하는 것을 볼 수 있다.
💡
메소드는 static 필드와 static 초기화블럭 일때만 호출된다.
JVM은 해당 클래스를 처음 사용할 때 <clinit>
메소드를 호출한다. 그러므로, <clinit>
은 런타임에 호출이 일어난다. 그래서 우리는 바이트코드에서 invocation 이 일어나는것을 볼 수 없다.
정리하자면, <init>
은
- 인스턴스가 만들어 지기 전
- 기본 생성자 인스턴스초기화자
<init>
으로 선언만 바이트코드에서 볼 수 있고,
- 기본 생성자 인스턴스초기화자
- 인스턴스가 만들어 지면 (
new Person("이름", "이름");
)- 넘겨진 파라미터를 가진
<init>
메소드를 호출하는 것을 볼 수 있다.
- 넘겨진 파라미터를 가진
<clinit>
은 해당 클래스를 처음 사용할 때 런타임에 호출이 일어나기때문에 바이트코드에서 호출되는 부분은 볼 수 없다.