티스토리 뷰

Java/Java Live Study

1. JVM에 대하여

지우 초이 2021. 3. 8. 13:28

A. JVM이란 무엇인가?

A-1. Java의 탄생 배경

1990년대는 인터넷의 태동기였고 인터넷 네트워크 환경을 고려한 기술들이 많이 나오기 시작했습니다. 1996년에 출시된 Java 역시, 네트워크 환경에서 동작하는 기기들을 고려한 어플리케이션 개발 환경을 만들기 위해 디자인 되었습니다.

오라클 공식문서에 따르면, "Java는 네트워크를 통해 소프트웨어의 컴포넌트들을 안전하게 전송하고 실행할 수 있도록 돕는다" 라는 이야기가 나오는데요. 여기서 "네트워크 환경에서 동작하는 기기를 고려하는 개발 환경" 라는 의미를 생각해볼 수 있습니다.

네트워크 환경에서 동작하는 개발 환경.

1️⃣C/C++

예를들어서, 기존 C/C++은 소스 코드를 각 기계에 직접 컴파일을 해서 실행시켜야 했습니다.

  1. 소스 코드를 💾저장장치에 담기 ⇒ 기계에서 컴파일하여 실행.
  2. 각 기계 버전에 맞게 실행 파일을 준비 ⇒ 💾 저장 장치에 담기 ⇒ 같은 종류의 기계에서 실행.

2️⃣Java

반면에, Java는 네트워크를 통해서 호스트에게 소스코드를 전달하는 방식을 고려했습니다. Java는 실행할 수 있는 소스를 네트워크를 통해 전달하면 어떤 기계에서든지 실행이 가능했습니다.

  1. Java 소스코드를 바로 인터넷을 통해 다운받는다. ⇒ 바로 해석하여 ⇒ 실행한다.

네트워크를 통해서 소스코드를 전달한다면 가장 중요한 부분은 아마 "플랫폼 독립적" 혹은 "각 기계에서 직접 소스코드를 실행시킬 수 있는 기능" 인 부분이 아닐까 싶습니다. 만약 당시 네트워크를 통해 소스코드를 호스트에게 전달한 후에, 기계에 맞게 컴파일을 다시하고 실행시켜야 한다면, 과연 네트워크로 소스코드를 보내는것이 크게 유의미한 일일까 (특히 90년대 당시에는..) 라는 생각이 듭니다.

✅ Java의 요구사항.

위에서 언급Java라는 환경을 개발하기 위해서는 아래와 같은 요구사항들이 있을것이라 생각해 볼 수 있겠습니다.

  • 어떤 호스트의 아키텍쳐에서도 작동되어야한다.
  • 소프트웨어 컴포넌트가 안전하게 호스트로 전송될 수 있어야 한다.
  • 어플리케이션이 안전하게 작동이 되어야 한다.
  • 그리고 호스트에서 실행될 때도, 호스트의 리소스를 많이 차지 하지 않도록 관리해야 한다.

Java는 위의 요구사항들을 충족하기 위해서 JVM(Java Virtual Machine) 의 도움을 받습니다.

A-2. JVM의 역할

JVM은 자바 어플리케이션을 구동하기 위해 별도의 소프트웨어로 돌아가는 가상 머신으로, 자바 플랫폼의 초석같은 존재입니다. 자바 어플리케이션은 JVM 위에서 돌아갑니다.

JVM 은 크게 두가지 특징을 가지고 있습니다.

  1. 플랫폼 독립적인 실행을 돕는다.
  2. 안정적이고 안전한 소프트웨어의 실행을 돕는다.

역할 1 : 플랫폼 독립적인 코드실행.

앞서 말했던 Java의 네트워크를 활용한 동작 방식을 보면..

Java 소스코드를 바로 인터넷을 통해 다운받는다. ⇒ 바로 해석하여 ⇒ 실행한다.

여기서 Java 소스코드를 다운받는다고 하였는데 프로그래머가 직접 작성한 코드가 아니라, 컴파일된 코드 1차적으로 컴파일 한 '바이트코드' 를 다운받습니다.

바이트 코드는 일반 Java 언어보단 기계어에 가깝지만 완전히 기계어는 아닙니다. 바이트코드는 JVM에 의해 런타임에서 해석되고 실행됩니다. 따라서, JVM이 지원되는 OS/아키텍쳐이기만 한다면, 소스코드를 실행할 수 있습니다.

다른 컴파일 언어와 Java를 비교해보겠습니다.

1️⃣ C 컴파일러

https://docs.oracle.com/cd/E19620-01/805-4031/6j3qv1oec/index.html

C 컴파일러는 어떤 아키텍쳐를 지원할건지 어떤 종류의 어플리케이션 바이너리를 만들것인지 컴파일러 단에서 결정합니다.

예를들어서, '인텔 아키텍쳐'를 위해 컴파일된 바이너리 코드를 다른 아키텍쳐에서 작동하려고 하면 정상적으로 작동하지 않을것입니다.

2️⃣Java 컴파일러

https://docs.oracle.com/cd/E19620-01/805-4031/6j3qv1oec/index.html

Java의 컴파일러는 어떤 아키텍쳐에서도 읽을 수 있는 '바이트코드' 를 컴파일러를 생성하고, 그 바이트코드는 각 아키텍쳐/운영체제에 맞는 JVM에서 해석되며 런타임에서 돌아갑니다. 따라서 Java프로그래머 입장에서는 한번만 작성하고 바이트코드만 만들어두면 다른 아키텍쳐/운영체제에서 똑같은 방식으로 구동 가능합니다.

역할 2 : 안전하고 안정적인 실행을 돕는다.

자바의 컴파일된 파일들은 네트워크를 통해서 다양한 호스트에게 전송됩니다.

예를들어 Java의 애플릿같은경우, HTML/JS처럼 브라우저에서 소스코드를 다운받고 바로 해석하며 실행시킵니다.

만약 호스트가 소스코드를 받고 브라우저가 코드를 바로 실행시켰는데, 그 코드가 호스트에서 안전하지 못한 행동들을 하면 어떡할까요?

예를들어서,

  1. 하드웨어나 다른 소프트웨어에 손상을 입히거나..
  2. 디스크를 읽고 쓰는 행위들....
  3. 다른 호스트와 네트워크를 연결하거나..
  4. 개인 정보를 넘겨주거나...
  5. 호스트 코드의 리소스를 모두 점유해버리거나...
  6. 허가되지 않은 메모리 영역에 접근해버리거나...

따라서 다운로드된 자바 코드들은 자바 인터프리터가 허용할 수 없는 명령들을 수행하지 못하도록 해야합니다.

Java Security Model은 네트워크로부터 다운되어진 믿을 수 없는 코드들의 예측 불가능한 실행을 방지하는것에 주안점을 맞추고 있습니다.

Java Security Model 중 JVM과 연관된 일부 기능들에 대해서 알아보겠습니다.

📦SandBox

https://docs.oracle.com/javase/7/docs/technotes/guides/security/spec/security-spec.doc1.html

외부에서 코드를 다운받아 실행될 때 (remote code) 는 독립적인 sandbox 영역에서 수행됩니다. Sandbox에서 내부의 정보에 접근할 때는, SecurityManager라는 JAVA API를 통해서만 제한적으로 접근 가능합니다.

🛠 JVM Built-in Mechanism

JVM이 제공하는 (혹은 Java lang, JVM 인터프리터)가 제공하는 여러 메커니즘들이 있습니다.

  • C/C++ 의 문제점 개선.

    • 원시타입 (primitive types)들은 항상 정확한 사이즈를 보장받습니다.
    • 모든 명령들은 특정한 순서대로 수행됩니다.
    • 포인터의 개념이 없습니다. 비정상적이고 예측 불가능한 메모리 접근도 불가합니다. 예측 가능한 구조적인 메모리 접근 (Structured memory access)만 가능합니다.
  • Garbage Collection

    • 런타임에서 특정 주기마다 계속 메모리 체크를 하며 도달가능하지 않은 객체들은 메모리 점유를 해제시킵니다.
    • 따라서, C/C++에서 처럼 메모리 공간을 점유 후에, free()를 할필요가 없습니다.
  • Reference Checking

    • 객체의 참조를 사용하는 경우 JVM은 런타임에서 정상적인 객체 참조를 하는지 체크하고, 문제가 있을경우 exception을 발생시킵니다.
      • Null에 접근하는지 체크..
      • 비정상적인 타입 캐스팅을 하는지 체크..
      • Array의 범위 안에 접근하는지 체크..
      • 등등....
  • 미리 정해지지 않는 메모리 레이아웃 (Unspecified memory layout)

    • JVM은 변수할당, 객체할당 등 메모리에 할당하는 부분을 JVM의 데이터 공간 (runtime data areas)에 런타임으로 필요할때마다 할당하기 때문에, 자바 소스코드나 클래스 파일만 보고 메모리 주소 정보를 예측하거나 알 수 없습니다. (메모리 주소값을 활용하는 공격으로부터 안전)
  • ※ A-2 REFERENCE

    Security 파트 내용출처

B. 컴파일 하는 방법

  1. 자바를 설치합니다.

    Java SE 15 - Downloads

  2. HelloWorld.java를 입력합니다.

     class HelloWorld {
         public static void main(String[] args) {
             System.out.println("Hello, World!"); 
         }
     }
  3. javac HelloWorld.java 를 통해 컴파일 합니다.

.class 파일이 나오는것을 알 수 있습니다.

이 .class파일은 앞써 말했던 jvm이 해석할 '바이트코드' 입니다.

몇가지 컴파일 옵션

javac - Java programming language compiler

  • -d [directory] : class file이 생성될 부분을 결정합니다.
  • -sourcepath [path] : 인터페이스의 정의나 클래스의 정의를 찾기 위한 소스코드의 경로를 지정합니다. 폴더, zip, jar 등이 사용될 수 있습니다.
  • -classpath or -cp [path]: 컴파일할때 필요한 class파일, 어노테이션 프로세서, 소스파일의 경로를 지정합니다.

컴파일 옵션을 보면 Java만의 몇가지 🧐 흥미로운 점 🧐이 보입니다.

  1. 옵션들을 보면 java의 컴파일러는 단순히 .java → .class로 바꿔주는것 뿐만 아니라, 다른 .class파일과 함께 java 소스코드의 여러가지 종속성등을 해결해주거나 확인해줄 수 있습니다.
  2. java는 각 클래스마다 .class파일을 생성합니다. 따라서, 여러 코드에서 나온 다량의 .class파일을 묶어 수 있는 방법을 제시하는것도 보입니다. 예를들어 다양한 class파일들을 .jar이나 .zip으로 묶어 관리할 수 있습니다.
  3. Java는 여러 .class파일을 생성하고 읽어야 하기때문에, 여러 .class파일의 관리도 중요할것입니다. 따라서, .class 파일들의 네임스페이스를 정확하게 구분해줄 수 있도록 하는 package 방식으로 java 프로젝트를 구성하는 이유도 이해가 갑니다.

C. 실행하는 방법

java HelloWorld 를 통해

본격적으로 소스코드를 수행시키면 아래와 같이 나옵니다.

java를 수행하면 java 프로세스(jvm)에 의해 수행되는것을 확인할 수 있습니다.

바이트 코드란 무엇인가?

Java 바이트 코드는 자바의 컴파일러를 통해 소스코드를 1차적으로 해석한 결과입니다.

  • 코드의 명령어의 크기가 1 바이트라, 바이트 코드입니다.
  • "1차적으로 해석했다" 라는 의미는 이 바이트코드는 완전히 기계어(바이너리) 형태가 아니라서 이 형태로는 실행할 수 없고, 최종적으로 JVM에서 런타임으로 해석하며 기계어로 변경합니다.
  • 또한, 완전히 기계어가 아니고 모든 JVM이 해석할 수 있는 코드이기 때문에, 한번 컴파일을 하면 운영체제나 아키텍쳐에 상관 없이 어떤 JVM에서도 동일하게 실행 가능합니다.
    • 이 말은 즉, 어떤 다른 언어라도 .class 파일만 만들 수 있다면, JVM에서 구동할 수 있습니다. (예: 코틀린)

JVM이 JAVA코드를 직접 해석하지 않고, 컴파일 과정을 거쳐 .calss로 만드는 이유는 여러가지가 있습니다.

  1. JVM에서 해석 혹은 코드를 최적화 하는 시간을 단축시켜줄 수 있습니다.
    1. Java 혹은 High-level language 코드를 바로 해석하는것은 시간이 오래걸립니다.
  2. JVM이 소스코드의 구조를 파악하는것에 도움을 줄 수 있습니다.
  3. 등등...

간단한 실습

HelloWorld.java

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

컴파일 명령 : javac HelloWorld.java

컴파일 명령어를 치면 이렇게 컴파일러가 .class 파일을 만들어 줍니다.

.class 파일의 이름은 Java파일의 파일명이 아닌, Java파일안에 있는 "클래스명"을 따릅니다.

클래스 당 한개의 .class 파일을 만듭니다.

예를들어 아래의 코드는 두개의 .class 파일을 만듭니다.

예를들어 아래와 같이 Java파일을 컴파일했다면

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
class HelloWorld2 {
}                                                                                                      

두개의 .class 파일이 만들어집니다.

class 파일을 열면 아래와 같은 알수없는 문자열들이 나옵니다.

javap 를 통해서 디컴파일이 가능합니다.

javap -c HelloWorld.class 로 디컴파일 한 모습은 아래와 같습니다.

class HelloWorld {
  HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13                 // String Hello, World!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

원본 소스

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

GCC C컴파일러와 비교해보겠습니다.

Java코드와 비슷한 동작을 하는 C코드입니다.

#include <stdio.h>

int main() {
  printf("%s", "Hello!");
    return 0;
}

gcc 컴파일러로 컴파일을 한 후에, otool을 이용해 디컴파일링을 하면...

otool -tv a.out

a.out:
(__TEXT,__text) section
_main:
0000000100003f60    pushq    %rbp
0000000100003f61    movq    %rsp, %rbp
0000000100003f64    subq    $0x10, %rsp
0000000100003f68    leaq    0x3b(%rip), %rdi
0000000100003f6f    leaq    0x37(%rip), %rsi
0000000100003f76    movb    $0x0, %al
0000000100003f78    callq    0x100003f8a
0000000100003f7d    xorl    %ecx, %ecx
0000000100003f7f    movl    %eax, -0x4(%rbp)
0000000100003f82    movl    %ecx, %eax
0000000100003f84    addq    $0x10, %rsp
0000000100003f88    popq    %rbp
0000000100003f89    retq

Java의 디컴파일 결과와는 상당히 상이한 결과를 볼 수 있습니다.

물론 서로 언어도 다르고, 사용되는 라이브러리도 다르고, 디컴파일을 위해 사용된 툴이 다르다는 점도 있지만, 바이트코드가 결과적으로는 "기계어에 덜 가깝다"라는 의미로도 해석할 수 도 있을것 같습니다.

D. JIT 컴파일러란 무엇이고 어떻게 동작하는가?

JIT는 Just In Time 이라고 하며, 자바 런타임 환경에서 바이트코드의 컴파일을 담당합니다.

JVM은 바이트 코드 해석과 실행을 위해 인터프리터와 JIT를 사용합니다.

https://docs.oracle.com/cd/E19620-01/805-4031/6j3qv1oec/index.html

JVM이 인터프리터를 가지고 있다면, 그것으로 한줄 씩 바이트코드를 해석하고 실행하면 문제가 없을것같은데, 왜 굳이 JIT 추가로 사용할까요?

JIT 을 사용하는 이유.

바이트 코드를 해석&실행(인터프리팅)의 실행 과정은 느립니다.

예를들어서,

단 한줄의 코드는 x=y+(2*x) 는 아래의 바이트코드로 바뀌고, 6줄의 바이트 코드를 해석 & 실행하기 위해 decoding, fetching, excuting 을 하며 위한 수십~수백개의 명령을 수행해야합니다.

http://people.cse.iitd.ac.in/~sbansal/csl862-virt/readings/CompileJava97.pdf

따라서 가장 좋은 방법은 빠르게 수행할 수 있는 기계어번역(컴파일)을 해서 그 결과를 반복적으로 사용하는 방법입니다. 이러면 수행해야 하는 명령의 수가 줄기 때문에, 퍼포먼스를 늘릴 수 있습니다.

우리는 완전한 컴파일을 런타임 이전에 하지 않기 때문에 (플랫폼 독립성과, 다양한 안전장치를 확보할 수 있는 방법이기 때문에), 런타임에서 컴파일을 하는 방법을 사용해야 합니다.

한편, 런타임에서 컴파일을 할 때 고려해야 할 중요한 사항은 "컴파일 시간" 입니다.

  • 당연한 이야기일 수 있겠지만 런타임에서 컴파일 방법을 사용하려면 런타임 컴파일을 하는 시간이 인터프리팅 (바이트코드 해석 + 수행) 하는 시간보다 빨라야합니다. 만약 그렇지 않다면 굳이 컴파일을 사용할 이유는 없습니다.

  • 런타임 컴파일 시간이 길어지면, 처음 앱을 켤 때 코드를 컴파일 하는데의 시간 (start-up) 시간이 길어져서 앱이 시작될때 까지 유저가 기다려야합니다.

    • 런타임 코드 컴파일은 아무래도 초반 앱을 켰을 때 의 상황 (warm-up) 구간에서만 사용량이 높습니다. 따라서, 최대한 start-up~ warm-up 구간이 길어지지 않도록 적당히 컴파일 활용과 인터프리터 활용에 밸런스를 맞추는것이 중요합니다.http://cr.openjdk.java.net/~vlivanov/talks/2015_JIT_Overview.pdf

JIT 컴파일러의 동작방식

JIT 컴파일러는 일반적인 컴파일러처럼 최적화 과정등을 최대한 덜어내고, 컴파일 과정을 빠르게 하여서 실행시키는것에 주안점을 둡니다.

따라서 JIT은 다양한 휴리스틱 기법들을 활용합니다.

  • 처음 실행되는 or method invocation call count 체크하여 사용이 많이되거나 필요한 메소드를 우선적 컴파일 하기.

  • 바이트코드나 생성되는 코드 profile등을 참고하여, 컴파일이 필요한 코드와 필요하지 않은 코드 구분하기.

http://cr.openjdk.java.net/~vlivanov/talks/2015_JIT_Overview.pdf

⇒ 만약 실행되지 않는 부분이라면, 굳이 컴파일 하지 않습니다.
  • 자주 수행되는 코드라면 최적화를 나중에 다시 컴파일 수행하기.

  • 바이트코드 & JIT Reference

    Reference

E. JVM의 구성 요소

 

https://dailyheumsi.tistory.com/196

E-1. 클래스 로더 (Class Loaders)

JVM은 클래스를 불러오고, 연결하고 초기화 하는 과정을 동적으로 진행합니다.

E-1-1. 로딩

JVM은 .class파일에서 class, interface 타입등을 찾고 생성합니다.

E-1-2. 링킹

링킹 과정은 불려와진 클래스나 인터페이스가 초기화 과정에서 실행될 수 있도록, JVM의 런타임 영역에 메모리 할당을 시작합니다.

1단계 verification

.class 파일이 valid한지 체크합니다.

만약 valid 하지 않다면 에러가 발생합니다.

2단계 preparation

메모리를 클래스의 static 변수는 기본값으로 초기화 시켜줍니다.

3단계 resolution

symbolic reference를 실제 메모리 레퍼런스로 교체됩니다.

E-1-3. 초기화

초기화 과정에서는 class와 interface 초기화 메소드를 수행합니다.

이때, static 변수에 입력한 값으로 초기화되고 static 메소드들이 수행됩니다.

E-2. 메모리 (Runtime Data Areas)

어떤 데이터는 JVM이 실행될 때 같이 실행되고 JVM이 꺼질 때 같이 사라지는 영역이 있고, 또 어떤 영역은 각각의 쓰레드 별로 라이프사이클을 같이 하는 경우가 있습니다.

쓰레드 단위의 영역 (쓰레드 생성될 때마다 생기는 영역)

PC, 네이티브 메소드 스택, 스택

전체 공유 영역

힙, 메소드

E-2-1. Stack

파라미터, 지역변수, return 주소 등을 저장하는 부분입니다. 만약 하나의 스레드가 스택에 제한된 사이즈를 넘어가면 stack overflow 에러가 나옵니다. 만약 스택 사이즈를 조정 가능하다면, outofmememory 에러가 나올 수 있습니다.

E-2-2. PC(Program Counter Register)

JVM은 다양한 쓰레드의 실행을 한번에 할 수 있습니다. 따라서 각 쓰레드별로 PC들이 여러개 있습니다.

E-2-3. Native Method Stack

  • 네이티브 (Native) 메소드란?

    JVM에서 Java외의 다른 언어들로 쓰여진 메소드 등을 읽어 Java앱과 같이 사용할 수 있도록 합니다. 보통 운영체제/하드웨어 컨트롤 등의 이유로 low-level 언어들과 사용합니다.

네이티브 메소드를 부를 때 사용하는 스택영역입니다. JVM의 구현체마다 다르지만, 다른 언어 시스템이기 때문에 JVM 만의 안전을 위한 여러 제한이 없어집니다.

E-2-4. Heap Area

모든 쓰레드들 사이에서 공유되며, 오브젝트, 클래스 인스턴스, 메타데이터, arrays 등이 런타임에 생성될 때 이 영역에 할당됩니다.

JVM이 시작될 때 생성되며, Garbage Collector에 의해 관리를 받는 부분입니다.

E-2-5. Method Area

클래스 별 구조 정보 (상수 풀, 생성자 및 메서드, 메서드 데이터, static 변수, 부모클래스 이름 등..) 를 저장하는 부분입니다.

E-2-6 Runtime Constant Pool.

클래스 별, 타입 별 심볼 테이블 역할을 하는 역할을 합니다. (링킹 과정에서 사용됩니다)

E-3. 실행엔진

E-3-1. 인터프리터

바이트코드를 한줄 한줄 해석하고, 변환하고, 읽어 네이티브 코드로 변환합니다.

E-3-2. JIT

  • 인터프린터의 실행 퍼포먼스에서의 불리함을 극복하기 위해 코드의 일부 구간을 기계어로 컴파일을 해놓는 과정입니다.
  • JIT부분에서 자세히 설명합니다.

E-3-3. Garbage Collector

  • 힙영역에서 참조되지 않는 객체를 찾아 정리합니다.

E-4. 네이티브 메소드 인터페이스

  • 다른 언어로 만들어진 어플리케이션과 상호 작용을 할 수 있도록 인터페이스를 제공합니다.

E-5. 네이티브 메소드 라이브러리

  • 이미 구현되어있는 여러 네이티브 메소드들 입니다.

  • Reference

    레퍼런스

JDK와 JRE의 차이.

JRE

JRE는 소프트웨어 환경을 일컫습니다.

JRE는 Java Runtime Enviroment로써, JVM + 그와 관련된 기능들의 환경 의미합니다.

자바 어플리케이션을 구동하려면 JRE가 필수적으로 필요합니다.

https://docs.oracle.com/cd/E19620-01/805-4031/6j3qv1oec/index.html

JDK

JDK는 java develpment kit으로서, Java 개발에 도움될 수 있는 툴들을 추가하여 포함하여 배포합니다.

https://jhnyang.tistory.com/224

javac - 자바 컴파일러

jdb - 자바 디버거

jconsole - JVM 모니터링 유틸

javadoc - 자바 doc만드는 유틸.

'Java > Java Live Study' 카테고리의 다른 글

1-1. JVM 추가 내용.  (0) 2021.03.08
자바 라이브 스터디.  (0) 2021.03.08
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함