10. 프런트엔드 컴파일과 최적화
1. 컴파일러 종류
- 프런트엔드 컴파일러 : JDK javac,
- JIT 컴파일러 : Hotspot VM C1,C2 컴파일러, Gral
- AOT 컴파일러 : Gral, JDK jaotc, java용 GNU컴파일러 (GCJ),,
2. javac 컴파일러
단계
- 준비 : plugin, annotation 처리기 초기화
- 구문분석 및 심벌테이블 채우기. 1.1 어휘 및 구문 분석 : 소스코드를 토큰화하여(어휘분석) 추상구문 트리 구성(구문분석) 1.2 심벌테이블 채우기 : 심벌 주소와 심벌 정보 생성
- plugin annotation 처리기들로 annotation 처리.
- 의미 분석 및 바이트코드 생성 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. 구문분석 및 심벌테이블 채우기
- 어휘 분석 및 구문 분석
- 어휘분석 : 소스코드 문자스트림을 토큰집합으로 변환. int a = 1 -> int, a, = , 1
- 구문분석 : 일련의 토큰들로부터 추상구문트리 구성하는 과정.
- 추상구문트리: 프로그램 코드의 문법구조를 트리형태로 기술하는 기법.
- 추상구문트리 생성 이후 소스코드 문자 스트림은 더이상 쓰이지 않는다.
- 심벌 테이블 채우기
- 심벌테이블 : 심벌 주소와 심벌 정보의 집합으로 구성된 데이터 구조. (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
- java : 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")
- 위 코드가 실행되게 하기위한 방법
- 위의 ArrayList는 그대로 두고, 신규버전 클래스를 추가한다. ArrayList
- 자바는 이미 레거시가있었고, 코드수만 늘릴수는 없었음.
- 기존 타입을 모두 generic으로 변경.
- 자바가 택한 방식. 이를 위해 타입소거형태로 구현되긴 했으나, 최선은 아닐것.
- 위의 ArrayList는 그대로 두고, 신규버전 클래스를 추가한다. ArrayList
타입소거
- 구 JDK코드 실행을 지원하기위해, 원시타입 개념이 등장.
- 원시타입 : 타입이 같은 모든 제네릭 인스턴스의 공통 상위 타입. (ArrayList
-> 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")
- 타입소거 방식의 문제
- primitive type 지원불가. 값형식은 타입변환이 불가능하기때문. (int -x-> Object)
- wrapper class로 대체하여 사용된다. Int, Long,,
- boxing,unboxing 이 속도 저하의 원인이됨.
- 런타임에 generic type 정보를 얻을 수 없다. ```java
void convert(List list, Class clazz) // clazz를 같이 받는 이유 ``` 3. 제네릭 오버로딩이 불가능하다. ```java // 컴파일 불가 void method(List list) void method(List list) ``` - JVM에 메서드 시그니처를 바이트코드수준에 저장하는 Signature 속성 추가하여 해결. - primitive type 지원불가. 값형식은 타입변환이 불가능하기때문. (int -x-> Object)
값타입과 앞으로의 제네릭
- 발할라프로젝트에서 타입소거와 값타입 지원을 개선중.
- 값타입 (<>참조타입)
- 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 컴파일 후 별도검증.