프로그램과 실행 2 : JVM, JIT

<JVM>




















( JAVA SE7 에디션에 기반 JVM 개요도 )


이전 글 "프로그램과 실행1" 에서는 
.java 파일 -> 컴파일 -> 바이트코드 .class 클래스 파일까지 다루었다.

위 그림을 참고하고 이어서 기계어까지의 번역 과정을 설명하면,
JVM은 Class loader를 통해 생성된 바이트코드인 .class파일을
JVM 규격에 정의된 대로 로드한다.
그 후, 바이트코드를 기계어로 번역할 때 JIT 컴파일러를 사용한다.


<JIT>

JIT는 Just In Time의 약자인데,
JIT 컴파일이라고하면,  
C나 C++에서 하는 것처럼 프로그램을 실행하기 전에 처음 한 번 컴파일하는 대신, 
프로그램을 실행하는 시점에서 필요한 부분을 즉석으로 컴파일하는 방식을 말한다.

JIT 컴파일러는 같은 코드를 매번 해석하는 대신
처음 실행될 때 인터프리트를 하면서 자주 쓰이는 코드를 캐싱한다.

이후에는 캐싱된 코드를 가져다 쓰기 때문에 인터프리터의 느린 실행 속도를 개선할 수 있다.

보다시피 Java는 컴파일언어지만,
기계어로 변환되기까지는 JVM에서 인터프리팅 과정을 추가적으로 거친다.




<JVM의 특성>

  • 스택 기반의 가상 머신
    단일 상속 형태의 객체 지향 프로그래밍을 가상 머신 수준에서 구현
  • 포인터를 지원하되 C와 같이 주소 값을 임의로 조작이 가능한 포인터 연산이 불가능

  • 가비지 컬렉션 사용

  • 모든 기본 타입의 정의를 명확히 함으로써 플랫폼 독립성 보장

  • 데이터 흐름 분석(영어: data flow analysis)에 기반한
    자바 바이트코드 검증기(영어: verifier)를 통해
    스택 넘침, 명령어 피연산자의 타입 규칙 위반, 필드 접근 규칙 위반,
    지역 변수의 초기화 전 사용 등
    많은 문제를 실행 전에 검증한다.
       -> 실행 시 안전을 보장하고 별도의 부담을 줄여줌

  • 명령어에서 스택에서 가져올 피연산자의 타입을 명령어에 지정
       (예: 정수 덧셈은 iadd, 단정밀도 실수 덧셈은 fadd)

  • 스택 기반 가상 머신
    -> 대다수의 명령어가 스택 선두에서 피연산자를 택하고 결과는 다시 스택에 넣는다 



<JVM의 구성요소>

1.클래스 로더

자바 클래스로더(Java Classloader)는 

자바 클래스를 자바 가상 머신(JVM)으로 동적 로드하는
자바 런타임 환경(JRE)의 일부이다.

자바 런타임 시스템은 클래스로더 때문에 파일과 파일 시스템에 대해 알 필요가 없다. 위임(delegation)은 클래스로더에 대해 학습할 때 이해하는데 중요한 개념이다.

라이브러리라고 하는 것은 
소프트웨어를 구동하는데 필요한 Object code의 모음이다.

자바 언어에서 라이브러리들은 JAR 파일로 묶여있는 것이 보통이다.
JAR 파일에 포함된 가장 중요한 형식의 오브젝트는 자바 클래스이다.
클래스는 코드의 명명 단위로 간주할 수 있다.

클래스 로더는
1. 라이브러리를 위치시키고
2. 내용물을 읽으며
3. 라이브러리들 안에 포함된 클래스들을 읽는 역할을 한다.

로딩은 일반적으로 요청이 오면 이루어지는데,
이 말은 클래스가 프로그램에 의해 호출될 때까지 로드하지 않는다는 것을 뜻한다.
명명된 클래스는 주어진 클래스로더에 의해 한 번만 로드될 수 있다.
    JVM이 시작되면 3개의 클래스 로더들이 사용된다

        - 부트스트랩 클래스 로더
          - 확장 클래스 로더
            - 시스템 클래스 로더

              부트스트랩 클래스 로더
              <JAVA_HOME>/jre/lib 디렉터리에 위치한 핵심 자바 라이브러리들을 불러들인다.
              핵심 JVM의 일부분인 이 클래스 로더는 네이티브 코드로 작성되어 있다.

                  확장 클래스 로더
                  확장 디렉터리에 코드를 로드한다.
                  확장 디렉터리에는
                  - <JAVA_HOME>/jre/lib/ext
                  -  java.ext.dirs 시스템 속성에 지정된 기타 디렉터리
                  가 있다.

                  sun.misc.Launcher$ExtClassLoader 클래스에 의해 구현되어 있다.


                  시스템 클래스 로더
                  goog_1432546573java.class.path에서 볼 수 있으며 CLASSPATH 환경 변수에 매핑된다.
                  sun.misc.Launcher$AppClassLoader 클래스에 의해 구현되어 있다.


                  2.바이트코드 명령어

                  자바 가상 머신이 실행하는 명령어의 형태이다.
                  각각의 바이트코드는 1바이트로 구성되지만
                  몇 개의 파라미터가 사용되는 경우가 있어 총 몇 바이트로 구성되는 경우가 있다.
                  256개의 명령코드 모두가 사용되지는 않는다.


                  3.JVM 언어

                  자바 바이트코드는 주로 'Java 7'로 작성된 소스 코드를 컴파일하여 생성하며
                  현재 JVM의 구조는 자바 언어의 구조를 거의 일대일로 반영하고 있다.
                  여담으로, 자바 바이트코드는 바로 기계어로 번역된 결과물이 아니므로
                  중간언어다.

                  Java -> Byte code (중간언어)-> 기계어의 컴파일 과정을 가지는 특성 때문에
                  역컴파일러(실행 코드로부터 소스 코드를 역으로 추출하는 프로그램)에 취약하다.

                  역컴파일 과정이 잘되면 보안에 민감한 코드를 쉽게 추출해낼 수 있기 때문에
                  그만큼 해킹에 취약하다. 

                  특히 라인정보 등의 디버깅 정보를 포함하여 컴파일 할 경우는
                  주석문을 제외한 거의 완전한 소스코드를 추출할 수 있다.

                  이에 대한 대비로 역컴파일을 어렵게 하고 만일 성공하더라도 원래의 소스 구조를 알기 힘들게 변경하는 프로그램(obfuscator) 들이 존재한다.

                  obfuscator들은 주로 다음과 같은 작업을 수행한다.
                  1. 클래스 파일의 디버깅 정보 제거
                  2. 래스명/함수명/변수명등을 임의의 단문자 형태로 치환
                  함으로서 생성되는 클래스 파일의 크기를 줄인다.


                  <java 언어와 java 바이트코드와의 관계>

                  IBM developerWorks journal에 따르면...
                  "바이트코드를 이해하고
                   자바 컴파일러에 의해 바이트코드가 어떻게 생성될 것인지를 이해하는 것은
                   C나 C++ 프로그래머가 어셈블리어를 이해하는 것과 같다"
                  라고한다. 

                  위의 비유를 읽고나니
                  어셈블리어를 이해하는 C/C++ 프로그래머가 
                  어셈블리어에 대한 지식이 없는 사람보다 어떤 메리트를 더 가지고 있을까?
                  라는 의문이 들었다.
                   

                  c컴파일러가 c 소스코드로부터 실행파일을 만드는 과정은 다음과 같다.

                  1 .c file -> Preprocess -> .i file 

                  C컴파일러가 쉽게 인식할 수 있도록 C언어 소스를 재정리합니다.
                  주석 삭제.
                  헤더파일 복사.
                  define 문 치환 작업 등을 수행한다.


                  2 .i file -> .s file

                  assembler는 CPU 제조회사에 맞게 작성되기 때문에 
                  컴파일러는 각 머신의 CPU 제조사에 맞는 assembler 소스 코드를 생성한다.

                  Assembler의 표준 문법은 다양하다.
                  GAS(GNU Assembler) : Linux
                  MASM(Microsoft Macro Assembler)
                  NASM(Netwide Assembler) 등



                  3 .s file(어셈블러) -> 어셈블 -> .o file ( Object 파일 : 사람이 이해할 수 없는 기계어 )



                  4 .o file + library -> Linker 프로그램의 linking -> 실행파일

                  Linking을 통해 여러 파일의 기능을 합쳐서 실행파일 1개를 만들 수 있다.


                  확장자가 .a인 archive 파일
                  확장자가 .so 파일인 shared object 파일이 있다. 

                  hello.c 실행 파일은
                  (hello.o) + (표준 라이브러리인 .a 또는 .so 파일) 을 합쳐서 만들어진다.

                  C언어의 표준 라이브러리도
                  여러개의 object 파일을 합쳐서 만든 하나의 파일이다.




                  위 과정을 보면,
                  C로 작성된 소스코드로부터 어셈블리어 코드를 생성하는 것이
                  C언어 컴파일러가 하는 작업 중 하나라는 것을 알 수 있다.

                  이 내용을 참고하여 나름대로 해석한 결과,
                  Assembly어 또는 Bytecode를 아는 개발자는
                  원시코드로부터 기계어에 근접한 변환 형태를 예상해볼 수 있으므로
                  - 주어진 자원에 더 최적화된 소프트웨어를 만들 수 있다.
                  - 원시 코드에서는 파악하기 힘든 보안 결함을 예측할 수 있다.
                  라는 생각이 들었다.
                   
                   
                  출처 : https://ko.wikipedia.org/wiki/자바_가상_머신
                  출처 : https://www.it-note.kr/263 [IT 개발자 Note:티스토리]

                  댓글

                  이 블로그의 인기 게시물

                  실무진 면접 경험으로 정리하는 백엔드 (1) : 에듀 테크 기업 면접

                  노마드코더 개발자북클럽 Clean code TIL 6 : 6장. 객체와 자료구조

                  백엔드 개발자가 Djnago fullstack 사이드 프로젝트를하며 ( html, css, vanillaJS 그리고 JS프레임워크 )