일반적인 원칙
- JVM을 더 빨리 작동시키는 마법의 스위치는 없다.
- 자바를 더 빨리 실행하게 만드는 '팁, 트릭'은 없다.
- 비밀 알고리즘도 없다.
자바 성능의 본질 by 제임스 고슬링
자바는 블루 칼라(주로 생산직에 종사하는 육체 노동자) 언어입니다. 박사 학위 논문 주제가 아니라 일을 하기 위해 만든 언어입니다.
자바는 실용성을 추구하기 때문에 개발자가 일일이 용량을 세세하게 관리하는 부담을 덜어주고 저수준으로 제어 가능한 일부 기능을 포기하는 발상이다. 이것이 서브시스템이다. 예를들어 메모리관리가 있다.
- 처리율
처리율은 시스템이 수행 가능한 작업 비율을 나타낸 지표이다. 보통 일정 시간 동안 완료한 작업 단위 수. 예를들어 초당 처리가능한 트랜잭션 수.
- 지연
1초에 100리터를 흘려보내는 수도관의 처리율은 바로 1초에 처리되는 부피 즉 100리터를 의미한다. 지연은 수도관 자체의 길이를 의미한다. 하나의 트랜잭션을 처리하고 끝날 때 까지 소요된 시간. 종단시간이라고도 한다.
- 용량
시스템이 보유한 작업 병렬성의 총량으로 동시 처리 가능한 작업 단위 개수를 의미한다.
- 사용률
시스템 리소스를 얼마나 효율적으로 활용하는지를 의미한다.
- 효율
처리율을 리소스 사용률로 나눈 값
- 확장성
리소스를 어느정도 까지 늘리면 거의 선형적으로 확장되지만 부하가 높아지면 완벽한 확장을 저해하는 한계점에 봉착한다.
- 저하
부하를 받으면 지연 또는 처리율 측정값에 변화가 생기는데 이것을 저하라한다.
처음부터 자바는 개발자가 플랫폼을 저수준에서 다 알 필요가 없도록 설계되었다.
→ 그래도 어느정도 이해가 필요함.
VM 스펙에 따르면 JVM은 스택 기반의 해석머신이다. 물리적 CPU 하드웨어인 레지스터는 없지만 일부 결과를 실행스택에 보관하며 이 스택의 맨 위에 쌓인 값들을 가져와서 계산한다.
JVM 인터프리터 기본 로직은 스택을 이용해 중간값들을 담아두고 opcode를 하나씩 순서대로 처리하는 while 루프 안의 switch문이다.
부트 스트랩 클래스가 자바 런타임 코어 클래스를 로드한다(자바 8이전까지 rt.jar에서 가져왔지만 9 이후부터는 런타임이 모듈화되고 클래스로딩 개념자체가 달라졌다) 부트 스트랩 클래스의 주임무는 다른 클래스로더가 나머지 시스템에 필요한 클래스를 로드할 수 있게 필수클래스 (Object, Class, ClassLoader 같은) 만 로드 한다→ 확장 클래스 로더가 생긴다 → 애플리케이션 클래스로더가 생긴다. (자세한 내용은 생략)
javac(자바컴파일러)을 이용해 컴파일을 하는데, 자바 소스를 바이트코드로 가득찬 .class 파일로 변환하는 일을 한다. 참고로 javac로 컴파일하는동안 최적화는 거의 하지 않기 때문에 javap같은 표준 역어셈블리 툴로 열어 보면 자바 코드도 알아볼 수 있다)
바이트코드는 특정 컴퓨터 아키텍쳐에 특정하지 않은 중간 표현형 (Intermediate Representation) 이다. 컴퓨터 아키텍쳐의 지배를 받지 않으므로 이식성이 좋아 개발을 마친 소프트웨어는 JVM 지원 플랫폼 어디서건 실행할 수 있다.
JVM은 클래스를 로드할 때 올바른 형식을 준수하고 있는지 빠짐없이 검사한다.
모든 클래스 파일은 0xCAFEBABE 라는 매직넘버(클래스 파일임을 나타내는 4바이트 16진수)로 시작한다.
99년도에 썬 사는 성능 관점에서 자바에 가장 큰 변화를 가져오는 핫스팟 가상머신을 선보였다. C/C++에 필적할 만한 성능을 자랑하며 진화를 하였다.
자바 프로그램은 바이트코드 인터프리터가 가상화환 스택머신에서 명령어를 실행하며 시작된다. CPU를 추상화한 구조라서 다른 플랫폼에서도 클래스 파일을 문제없이 실행할 수 있다. 하지만 프로그램이 최대의 성능을 내려면 네이티브 기능을 활용해 CPU에서 직접 프로그램을 실행시켜야 한다.
이를 위해 핫스팟은 프로그램 단위를 (메서드와 루프) 인터프리티드 바이트코드에서 네이티브 코드로 컴파일한다. 그래서 JIT (Just In Time) 컴파일이라고 한다.
핫스팟은 인터프리티드 모드로 실행하는 동안 애플리케이션을 모니터링하면서 자주 실행되는 코드를 발견해 JIT 컴파일을 수행한다. 이렇게 분석하는 동안 추적정보가 취합되면서 더 정교하게 최적화를 할 수 있다.
테스트 환경은 가급적 모든 면에서 운영 환경과 똑같이 복제해야 한다. 애플리케이션 서버 뿐만 아니라 (cpu수, OS, 자바 런타임 버전 까지) 웹 서버, db, 로드밸런서, 방화벽 등.
운영환경과 많이 차이나는 성능 테스트는 실제 환경에서 어떤 일들이 일어나날지 예측하기 어렵고 쓸모 있는 겨로가를 얻지 못할 가능성이 크다.
안티패턴은 사람들이 수많은 프로젝트를 수행하면서 밝혀낸, 소프트웨어 프로젝트 또는 팀의 좋지 않은 패턴이다.
개발자의 지루함은 프로젝트에 여러 가지로 해악을 끼칠 수 있는데 Collections.sort()를 쓰지않고 직접 정렬알고리즘을 구현하는 게 그 예이다.
팀원들이 기술을 결정할 때 관심사를 분명히 밝히지 않고, 서로 충분한 논의 없이 진행했을 때 쓴 결과가 나오는 경우. 섣불리 중요한 결정을 내리는것도 또래압박.
새로 나온 멋진 기술을 그대로 맹신하는 행위.
예를들어 하이버네이트의 경우 수박 겉핥기 식으로 이해하고 적용하는 경우.
- 새 컴포넌트는 전후로 충분한 로그를 남긴다.
- 새로 적용하거나 바뀌는 매개변수를 UAT에서 시험해본다.
- 평소 의심했던 범인 (하이버네이트)를 수사 과정의 유일한 용의자로 지목하는 행위를 지양한다.
- 운영환경과 동일한 UAT 환경을 구입한다.
- 알고리즘은 반드시 모든 가비지를 수집해야 한다.
- 살아 있는 객체를 절대 수집해서는 안된다.
→ 개발자가 저수준 제어권을 포기한다는 사상이 바로 자바 관리 방식의 핵심이며, 제임스 고슬링이 블루칼라 언어라고 말한 특징이 잘 드러난다.
gc의 기본 알고리즘 (표시하고 쓸어담기) → 기본적으로 살아 있는 객체는 대부분 DFS로 찾는다.
자바는 C++과 달리 주소를 역참조하는 일반적인 메커니즘이 없고 오로지 (. 연산자)만으로 필드에 액세서하거나 객체 레퍼런스의 메서드를 호출할 수 있다.
핫스팟은 런타임에 oop (Ordinary Object Pointer) 라는 구조체로 자바 객체를 나타낸다. oop는 대부분 기계어 워드라서 예전 32비트 프로세서는 32비트, 요즘은 64비트이다. 그런데 이런 구조는 메모리 낭비가 될 수 있어서 압축을 제공한다.
-XX:+UseCompressOops힙에 있는 모든 객체의 Klass 워드, 참조형 인스턴스 필드, 객체 배열의 각 원소가 압축이 된다.
(KlassOops는 JVM 클래스로더가 로드한 Class 객체를 JVM 수준에서 나타낸 구조체)
참고로 Mark 워드 (인스턴스 관련 메타데이터를 가리키는 포인터) Klass 워드 (클래스 메타데이터를 가리키는 포인터). 핫스팟 객체 헤더에는 일반적으로,
- Mark 워드
- Klass 워드
- length 워드 (배열이라면)
- 32비트 여백 (정렬 규칙 때문에 필요할 경우)
자바에서는 배열은 객체이다. 그래서 JVM의 배열도 oop로 표현이 되고 배열은 Mark 워드, Klass 워드 다음에 배열 길이를 나타내는 Length가 붙는다. 자바 배열의 인덱스가 32비트 값으로 제한되는건 이 때문이고 배열 길이를 따로 메타데이터에 넣어서 관리하기 때문에 c/c++ 언어에서 배열 길이를 몰라 매개변수를 반드시 하나 더 넘겨야 할 일은 없다.
JVM 환경에서의 자바 레퍼런스는 instanceOop 혹은 null 이외의 어떤 것도 가리킬 수 없다.
참고로 Class<?> 인스턴스와 klassOop(힙의 메타데이터 영역에 존재) 는 다르며 klassOop를 자바 변수 안에 넣을 수 없다.
가비지 수집이 일어나는 원인은
- 할당률: 일정 기간 새로 생성된 객체가 사용한 메모리량.
- 객체 수명
이 가설은 경험적으로 알게된 가설로 JVM 메모리 관리의 이론적 근간을 형성한다.
JVM 및 유사 소프트웨어 시스템에서 객체 수명은 이원적 (bimodal) 분포양상을 보인다. 즉 거의 대부분 객체는 아주 짧은 시간만 살아있고 나머지 객체는 기대 수명이 훨씬 더 긴 현상을 말한다.
→ 단명하는 객체를 쉽고 빠르게 수집해야 하며 장수하는 객체는 이와 분리시켜 놓는게 좋다 라는 것.
- 객체마다 세대 카운트 (객체가 무사통과된 가비지 수집 횟수)를 센다.
- 큰 객체를 제외한 나머지 객체는 에덴 공간에 생성하고 여기서 살아남는다면 다른 곳으로 옮긴다.
- 장수했다고 할 정도로 충분히 오래 살아남은 객체들은 별도의 메모리 영역 (올드 또는 테뉴어드 영역에 보관한다) Tenured → 종신