2020-10-6 바이트코드 강의 정리(1)

배경

  • JVM메모리나 자바의 리플렉션 동적프록시 등에 대해서 궁금했었다. 자바책에도 깊이 설명이 안되있었는데, 백기선님께서 인프런에 더자바 시리즈로 만드신 강의를 들으면서 정리한 내용이다.

JVM(Java Virtual Machine)

  • 자바 가상 머신으로 자바 바이트 코드(.class파일)을 OS에 특화된 코드로 변환(인터프리터와 JIT컴파일러)하여 실행
  • 바이트코드를 실행하는 표준(JVM 자체는 표준)이자 구현체(특정 밴더가 구현한 JVM)
  • JVM 밴더 : 오라클, 아마존, Azul.
  • 특정 플랫폼에 종속적이다.
  • 최소한의 단위가 JRE

JRE(Java Runtime Environment)

  • 목적 : 자바 애플리케이션을 실행
  • JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스를 가지고 있다(JVM+라이브러리)
  • 개발관련된 도구는 포함하지 않는다(java compiler포함 안됨, JDK에서 제공)

JDK(Java Development Kit)

  • JRE + 개발개발에 필요한 툴.
  • 소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적
  • 자바11부터는 JDK만 제공

자바

  • 프로그래밍언어
  • JDK에 들어있는 javac를 사용하여 바이트코드(.class)로 컴파일 할 수 있다
  • 오라클에서 만든 Oracle JDK 11 버전부터 사용으로 사용할때 유료

타프로그래밍언어지원

  • JVM기반으로 동작하는 프로그래밍언어
  • 클로저, 그루비, Kotlin…
  • 어떤 언어를 쓰던 결국 .class파일로 변환되면 JVM위에서 실행할 수 있다.

JVM 구조

클래스 로더 시스템

  • .class에서 바이트코드를 읽고 메모리에 적절하게 배치

  • 로딩 : 클래스를 읽어오는 과정

  • 링크 : 레퍼런스를 연결하는 과정

  • static한 값들을 초기화

    • static String name;
      
      static {
        name = "hello";
      }
      
    • 위와 같이 static한 값들을 초기화한다.

메모리

  • 스택, PC, 네이티브메소드스택, 메서드, 힙영역으로 구분할 수 있다.
  • 메서드 영역은 클래스수준의 정보(이름, 부모클래스 이름, 메서드, 변수)저장 및 공유
  • 힙영역 : 객체를 저장하고 공유
  • 스택영역에는 쓰레드 마다 런타임 스택을 만들고, 그 안에 메서드 호출을 호출 스택 프레임이라 부르는 블럭으로 쌓는다. 쓰레드 종료하면 런타임 스택도 사라진다.
  • PC(Program Counter) 레지스터 : 스레드마다 스레드 내 현재 실행할 스택프레임을 가리키는 포인터가 생성.
  • 네이티브 메서드 스택 : 네이티브 메서드를 호출할때 사용. 네이티브 메서드가 뭐지???. 내부적으로 native키워드가 붙어있고, 자바코드가 아닌 C, C++과 같은 코드로 작성한다.
    • 스택, PC, 네이티브메서드는 스레드에 국한된거다.
    • JNI 인터페이스를 통해 접근하고 사용한다.

실행엔진

  • 인터프리터 : 바이트코드를 한줄씩 실행. 한줄마다 컴파일하는거다.
  • JIT 컴파일러 : 인터프리터 효율을 높이기 위해, 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바꿔준다. 바이트코드를 네이티브 코드로 변경한다.
  • GC : 더이상 참조되지 않는 객체를 모아서 정리.

클래스로더

  • 로딩-> 링크 -> 초기화순서로 진행
  • 로딩
    • 클래스로더가 .class파일을 읽고 적절한 바이너리 데이터를 만들고, 메서드 영역에 클래스정보를 저장
    • 메서드 영역에 저장하는 데이터 : FQCN, 클래스, 인터페이스, ENUM, 메서드와 변수
    • 로딩이 끝나면 해당 클래스 타입의 Class객체를 생성하여 Heap에 저장. Class<MyCustomClass>와 같이 저장.
  • 링크
    • Verify, Prepare, Resolution순서로 진행
    • .class파일이 유효한지 체크
    • Preparation : 클래스변수(static 변수)와 기본값에 필요한 메모리 Resolve => 메모리를 준비하는 과정
    • Resolve : 심볼릭 메모리 레퍼런스를 메서드 영역에서 실제 레퍼런스로 교체
  • 초기화
    • static 변수의 값을 할당.

바이트코드 조작

  • jacoco라는것을 이용해서 코드 커버리지를 알 수 있다. 어떻게 커버리지를 알 수 있을까? 바이트코드를 이용한다.
  • 바이트코드를 조작하기 위한 툴
    • ASM이라는 라이브러리 => Visitor패턴과 Adaptor패턴을 활용한다.
    • Javassist
    • ByteBuddy
public class Wizard {
  public static void main(String[] args) {
    try {
      new ByteBuddy().redefine(Hat.class)
        .method(named("pullOut")).intercept(FixedValue.value("Rabbit"))
        .make().saveIn(new File("/Users/jaeyeonkim/Desktop/study/thejava/target/classes/"));
    } catch (IOException e) {
      e.printStackTrace();
    }
    System.out.println(new Hat().pullOut());
  }
}


// Source Code
  public class Hat {
    public String pullOut() {
      return "";
    }
  }
  // Byte Code
  public class Hat {
    public Hat() {
    }

    public String pullOut() {
      return "Rabbit";
    }
  }

  • ByteBuddy를 통해 Hat클래스의 바이트코드를 조작해서, Rabbit을 추가했다.
  • 위와 같은 순서로 실행하면 안될 확률이 높다. 이미 redefined(Hat.class)에서 Hat class를 이용할때, ClassLoader에는 Hat을 읽어들인다. 다시 재정의를 하더라도 새로 읽지는 않는다.
  • 이런문제를 개선하기 위해 바이트코드를 조작하는 agent를 새로 만들어야한다.
public class WizardAgent {
  public static void premain(String agentArgs, Instrumentation inst) {
    new AgentBuilder.Default()
      .type(ElementMatchers.any())
      .transform(new AgentBuilder.Transformer() {
        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
          return builder.method(named("pullOut")).intercept(FixedValue.value("Agent Rabbit"));
        }
      }).installOn(inst);
  }
}

  • premain이라는 함수를 정의한다. premain 모드는 자바 프로세스를 실행할때, option을 줘서 agent를 붙인다.
  • agent를 jar로 패키징하면서 값을 넣어야한다.
  • mvn jar plugin manifest를 써야한다.
  • agent를 jar파일로 패키징하고, jvm option을 이용해서 해당 jar파일 경로를 추가해주면, 클래스 파일을 변경하는 방식이 아닌 agent를 통해 클래스로딩할때 사용할 수 있다.
  • 클래스파일 자체가 바뀌지는 않는다. 단지 로딩할때 agent를 통해 하는 거다. 메모리 내부에 바이트코드가 변경된다.

사용예시

  • 프로그램 분석
    • 버그찾는 툴
    • 코드 복잡도 계산
  • 클래스파일 생성
    • 프록시 : 스프링AOP, Mock객체, JPA Lazyloading Proxy
    • 특정 API 호출 접근 제한
    • 언어의 컴파일러
  • 자바코드를 만지지 않고 코드변경이 필요한 경우 : 프로파일러, 최적화, 로깅…
  • 컴포넌트 스캔 방법(ASM을 사용해서 컴포넌트 스캔을 한다)
    • 컴포넌트 스캔에서 빈으로 등록할 후보 클래스를 정보를 찾는데 사용
    • ClassPathScanningCandidateComponentProvider -> SimpleMetadataReader
    • ClassReader와 Visitor를 사용하여 클래스 메타정보를 읽어온다.
Written on October 6, 2020