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