Be-Developer

JVM 밑바닥까지 파헤치기 : 10. 프런트엔드 컴파일과 최적화

10. 프런트엔드 컴파일과 최적화

1. 컴파일러 종류

  • 프런트엔드 컴파일러 : JDK javac,
  • JIT 컴파일러 : Hotspot VM C1,C2 컴파일러, Gral
  • AOT 컴파일러 : Gral, JDK jaotc, java용 GNU컴파일러 (GCJ),,

2. javac 컴파일러

단계

  1. 준비 : plugin, annotation 처리기 초기화
  2. 구문분석 및 심벌테이블 채우기. 1.1 어휘 및 구문 분석 : 소스코드를 토큰화하여(어휘분석) 추상구문 트리 구성(구문분석) 1.2 심벌테이블 채우기 : 심벌 주소와 심벌 정보 생성
  3. plugin annotation 처리기들로 annotation 처리.
  4. 의미 분석 및 바이트코드 생성 3.1 특성검사 : 문법의 정적 정보 확인 3.2 데이터 흐름 및 제어 흐름 분석 : 프로그램의 동적 실행 과정 확인 3.3 편의 문법 제거 3.4 바이트 코드 생성 : 지금까지 생성된 정보들을 바이트코드로 변환
/zulu-8.jdk/Contents/Home/src/com/sun/tools/javac/
├── main
│   ├── CommandLine.java
│   ├── JavaCompiler.java  # 컴파일 핵심로직
│   ├── Main.java
│   ├── Option.java
│   └── OptionHelper.java
├── parser                # 1.1 어휘 분석, 구문분석
│   ├── Scanner.java      # 1.1.1 어휘분석
│   ├── Parser.java       # 1.1.2  구문분석
│   ├── ...
├── tree
│   ├── JCTree.java       # 1.1.3 추상 구문트리
│   └── ...
├── comp
│   ├── Enter.java        # 1.2 심벌 테이블 채우기
│   └── ...
├── processing
│   ├── JavacProcessingEnvironment.java # 2. annotation 처리
│   └── ...
├── comp
│   ├── Attr.java         # 3.1 특성검사
│   ├── Check.java        # 3.1 특성검사
│   ├── Flow.java         # 3.2 데이터 흐름 분석과 제어 흐름 분석
│   ├── Lower.java        # 3.3 편의문법 제거
│   ├── TransTypes.java   # 3.3 편의문법 제거
│   └── ...
├── jvm
│   ├── Gen.java          # 3.4 바이트코드 생성
│   ├── ClassWriter.java  # 3.4 클래스파일에 바이트코드 출력.


1. 구문분석 및 심벌테이블 채우기

  1. 어휘 분석 및 구문 분석
    • 어휘분석 : 소스코드 문자스트림을 토큰집합으로 변환. int a = 1 -> int, a, = , 1
    • 구문분석 : 일련의 토큰들로부터 추상구문트리 구성하는 과정.
    • 추상구문트리: 프로그램 코드의 문법구조를 트리형태로 기술하는 기법.
    • 추상구문트리 생성 이후 소스코드 문자 스트림은 더이상 쓰이지 않는다.
  2. 심벌 테이블 채우기
    • 심벌테이블 : 심벌 주소와 심벌 정보의 집합으로 구성된 데이터 구조. (like key-value hashTable)
    • 결과로 컴파일단위 각각에 대한 추상구문트리의 최상위 노드와, (있는경우) package-info.java 의 최상위 노드 목록이 만들어진다.

2. plugin annotation 처리기들로 annotation 처리

  • plugin annotation processor : 추상구문트리의 임의 요소를 읽고 수정,추가할수있는 컴파일용 플러그인
  • ex) lombok
  • 추상구문트리가 수정되면 1. 구문분석 및 심벌테이블 채우기 단계로 돌아간다.

    그래서 intelliJ에서 lombok plugin이 필요했던건가? 관련있는건가?

3. 의미 분석 및 바이트코드 생성

  • 의미분석의 주된 목적은 구조적으로 올바른 소스가 ‘맥락상으로도 올바른지’ 확인하는것.
  • 특성검사(타입검사), 제어 흐름검사, 데이터 흐름검사..

1. 특성검사

  • 변수를 사용하기에 앞서 선언되었는지.
  • 변수와 할당될 데이터 타입이 일치하는지.
  • 상수접기 (constant folding) : javac 컴파일러가 소스코드에 대해 수행하는 몇안되는 최적화.
    • int a = 1 + 2 -> int a = 3

2. 데이터 흐름 분석과 제어 흐름 분석

  • 프로그램이 맥락상 논리적으로 올바른지 확인
    • 지역변수가 사용되기 전에 값이 할당되었는지
    • 메서드의 모든 실행 경로에서 값을 반환하는지
    • 검사 예외는 모두 올바르게 처리되는지
  • 클래스 로딩시 수행하는 분석목적과 같지만, 검증범위가 달라 항목에 따라 한번만 수행되기도 한다.
final 지역변수
// 두 메서드의 바이트코드는 동일하다.
public void foo1(final int arg){
  final int var = 0;
}

public void foo2(int arg){
  int var = 0;
}
  • 지역변수를 final로 선언해도 클래스변수와 달리 지역변수는 접근플래그에 대한 정보를 저장할 수 없는 구조이다.
  • 지역변수를 final로 선언해도 런타임에는 아무런 영향이 없다.
  • 지역변수의 불변성은 오직 컴파일타임에 javac 컴파일러가 보장한다.

3. 편의문법 제거

  • Generic, varargs, auto boxing, unboxing..
  • JVM 의 런타임에서 직접 지원되지 않으므로, 컴파일과정에서 기본구문구조로 복원된다.

4. 바이트코드 생성

  • 이전단계에서 생성한 정보를(구문트리, 심벌테이블) 바이트코드명령어로 변환하여 저장소에 기록한다.
  • 인스턴스 생성자 <init>()와 클래스 생성자 <cinit>() 이 이 단계에서 구문트리에 추가된다.
  • 프로그램 로직 일부를 최적화된 코드로 교체하기도 한다
    • “a” + “b” -> StringBuffer, StringBuilder로 대체.

3. 자바 편의 문법의 재밌는점.

1. Generic

  • 본질은 매개변수화된 타입 또는 매개변수화된 다형성. Java와 C# 제네릭 비교 표
구분 Java C#
구현 방식 타입 소거 (Type Erasure) 실체화 (Reification)
타입 지원 참조 타입만 지원 기본형 + 참조 타입 모두 지원
성능 박싱/형변환 오버헤드 가능 타입별 최적화 코드 생성
리플렉션 제네릭 타입 추적 불가 런타임 타입 정보 유지
메서드 문법 ` 반환타입 메서드() | 반환타입 메서드()`  
하위 호환성 JVM 변경 없이 구현 CLR 수준에서 지원

  • 자바의 제네릭 정보는 소스코드에만 존재한다. 컴파일된 바이트코드에서는 타입정보가 원시타입으로 대체되고, 적절한 형변환 코드가 해당 위치에 삽입된다.
    • java : List = List
    • C# : List != List
  • 자바에서 불가능한 문법
    public class GenericEx<T> {
    public void test(Object item){
      T newType = new T();  //error
      if(item instanceof T){}  //error
    }
    }
    

    Generic의 역사적 배경

  • 바이너리 하위 호환성.
    • JDK 1.4 에서 컴파일한 프로그램이 JDK 5에서 동작해야한다.
    • 없던 제약이 갑자기 추가되면 안된다는 뜻.
      ArrayList list = new ArrayList();
      list.add(1)
      list.add("hello")
      
    • 위 코드가 실행되게 하기위한 방법
      1. 위의 ArrayList는 그대로 두고, 신규버전 클래스를 추가한다. ArrayList
        • 자바는 이미 레거시가있었고, 코드수만 늘릴수는 없었음.
      2. 기존 타입을 모두 generic으로 변경.
        • 자바가 택한 방식. 이를 위해 타입소거형태로 구현되긴 했으나, 최선은 아닐것.

타입소거

  • 구 JDK코드 실행을 지원하기위해, 원시타입 개념이 등장.
    • 원시타입 : 타입이 같은 모든 제네릭 인스턴스의 공통 상위 타입. (ArrayList -> ArrayList)
  • 제네릭 코드를 decompile 해보면, 원시타입에 형변환코드가 붙어있는 모양임. = 타입소거 방식
    // 원본
    Map<String,String> map = HashMap<String,String>();
    String value = map.get("key")
    // decompile
    Map map = new HashMap()
    String value = (String)map.get("key")
    
  • 타입소거 방식의 문제
    1. primitive type 지원불가. 값형식은 타입변환이 불가능하기때문. (int -x-> Object)
      • wrapper class로 대체하여 사용된다. Int, Long,,
      • boxing,unboxing 이 속도 저하의 원인이됨.
    2. 런타임에 generic type 정보를 얻을 수 없다. ```java
    void convert(List list, Class clazz) // clazz를 같이 받는 이유 ``` 3. 제네릭 오버로딩이 불가능하다. ```java // 컴파일 불가 void method(List list) void method(List list) ``` - JVM에 메서드 시그니처를 바이트코드수준에 저장하는 Signature 속성 추가하여 해결.

값타입과 앞으로의 제네릭

  • 발할라프로젝트에서 타입소거와 값타입 지원을 개선중.
  • 값타입 (<>참조타입)
    • C#에서 값타입은 모두 Object의 하위클래스. int -> Object 형변환 가능.
    • 참조타입과 달리 할당시 값 전체가 복사된다. (참조타입은 참조만 복사.)
    • 값타입의 인스턴스는 메서드의 호출스택에 쉽게 할당되며, 메서드가 종료되면 자동으로 해제되어 GC에 부담이 없다.

2. 오토박싱, 오토 언박싱, 개선된 for문

// 코드
List<Integer> list = Arrays.asList(1,2,3,4)
for (int i : list){
  System.out.println(i)
}
// 컴파일 후 
List list = Arryas.aslist(new Integer[]{    // varargs
  Integer.valueOf(1),   // 오토박싱
  Integer.valueOf(2),,
})
for (Iterator localIterator = list.iterator(); localIterator.hasNext();){   // 개선된 for문. iterator를 구현해야한다.
  int i = ((Integer)localIterator.next()).intValue();   // 오토 박싱, 오토 언박싱
}

오토박싱 주의할점 예

	@Test
	public void test() {
		Integer a = 1;
		Integer b = 2;
		Integer c = 3;
		Integer d = 3;
		Integer e = 321;
		Integer f = 321;
		Long g = 3L;
		System.out.println(c == d);             //true
		System.out.println(e == f);             //false
		System.out.println(c == (a + b));       //true
		System.out.println(c.equals(a + b));    //true
		System.out.println(g == (a + b));       //true
		System.out.println(g.equals(a + b));    //false
	}

3. 조건부 컴파일

  • 자바는 컴파일 단위 전체를 포괄하는 구문트리를 만들어 최상위 노드부터 하나씩 컴파일한다.
    • 따라서 파일 각각이 서로에게 심벌정보를 제공할 수 있다.
    • C는 전처리기가 컴파일전 include를 처리한다.
  • if문의 조건에 상수를 넣으면 조건부 컴파일 가능
    • if(true){A} else {B} 를 컴파일 하면 B 구문은 컴파일되지않는다.

4. 실전 : 플러그인 애너테이션 처리기 제작

  • java 표준 네이밍 검사기 코드

    sonarqube custom rule 추가시에 visitNode, JavaFileScanner 비슷한 구조인데 이것도 플러그인방식인건가? -> X 컴파일 후 별도검증.