자바의 성능은 일종의 흑마술로 알려져 있습니다. 그 이유 중 하나는 Java 플랫폼이 매우 복잡하고 많은 경우 문제를 찾기가 어렵기 때문입니다. 그러나 응용 통계나 실증적 추론에 의존하기보다는 지혜와 경험을 바탕으로 Java 성능을 연구하는 역사적 경향도 있습니다. 이 기사에서는 가장 터무니없는 기술 관련 신화 중 일부가 사실이 아님을 폭로하고 싶습니다.
1. Java는 느립니다
Java의 성능에는 많은 오류가 있는데, 이것이 가장 오래되고 아마도 가장 분명한 오류일 것입니다.
1990년대와 2000년대 초반에는 Java가 가끔 매우 느린 것이 사실입니다.
그러나 그 이후로 10년 넘게 가상머신과 JIT 기술이 발전해 현재 자바의 전반적인 성능은 매우 좋다.
6개의 독립적인 웹 성능 벤치마크에서 Java 프레임워크는 24개 테스트 중 22개에서 상위 4위를 차지했습니다.
JVM은 일반적으로 사용되는 코드 경로만 최적화하기 위해 성능 프로파일링을 사용하지만 최적화 효과는 분명합니다. 많은 경우 JIT로 컴파일된 Java 코드는 C++만큼 빠르며 이러한 현상은 점점 더 많이 발생하고 있습니다.
그럼에도 불구하고 여전히 자바 플랫폼이 느리다고 생각하는 사람들이 있다. 이는 자바 플랫폼 초기 버전을 경험한 사람들의 역사적 편견에서 비롯된 것일 수도 있다.
결론을 내리기 전에 객관적인 자세로 최신 실적 결과를 평가해 보시기 바랍니다.
2. Java 코드 한 줄을 따로 볼 수 있습니다.
다음의 짧은 코드 줄을 고려하세요.
MyObject obj = new MyObject();
예 Java 개발자에게는 이 코드 줄이 객체를 할당하고 적절한 생성자를 호출한다는 것이 명백해 보일 수 있습니다.
이를 바탕으로 성능 경계를 도출할 수도 있습니다. 우리는 이 코드 줄이 확실히 일정량의 작업을 수행하게 한다고 가정하고, 이 가정을 기반으로 성능에 미치는 영향을 계산해 볼 수 있습니다.
사실 이런 이해는 어떤 일이든 어떤 상황에서도 이뤄질 것이라는 선입견을 갖게 만든다.
사실 javac 및 JIT 컴파일러 모두 데드 코드를 최적화할 수 있습니다. JIT 컴파일러에 관한 한 프로파일링 데이터를 기반으로 예측을 통해 코드를 최적화할 수도 있습니다. 이 경우 이 코드 줄은 전혀 실행되지 않으므로 성능에 영향을 미치지 않습니다.
또한 JRockit과 같은 일부 JVM에서는 JIT 컴파일러가 객체에 대한 작업을 분해할 수도 있으므로 코드 경로가 여전히 유효한 경우에도 할당 작업을 피할 수 있습니다.
여기서 교훈은 Java 성능 문제를 처리할 때 컨텍스트가 매우 중요하며 성급한 최적화로 인해 직관에 반하는 결과가 발생할 가능성이 있다는 것입니다. 따라서 조기에 최적화하지 않는 것이 가장 좋습니다. 대신 항상 코드를 작성하고 성능 조정 기술을 사용하여 성능 핫스팟을 찾아 개선하십시오.
3. 마이크로벤치마킹은 생각하는 대로 작동합니다.
위에서 본 것처럼 작은 코드 조각을 검사하는 것은 애플리케이션의 전체 성능을 분석하는 것만큼 정확하지 않습니다.
이에도 불구하고 개발자는 마이크로벤치마크 작성을 좋아합니다. 기본 플랫폼의 일부 측면을 조작하는 것이 재미있는 것 같습니다.
Richard Feynman은 "자신을 속이지 마십시오. 당신은 가장 쉽게 속는 사람입니다."라고 말했습니다. 이 문장은 Java 마이크로 벤치마크 작성을 설명하는 데 매우 적합합니다.
좋은 마이크로벤치마크를 작성하는 것은 매우 어렵습니다. Java 플랫폼은 매우 복잡하며 많은 마이크로벤치마크는 Java 플랫폼의 일시적인 효과나 기타 예상치 못한 측면만 측정할 수 있습니다.
예를 들어 경험이 없는 경우 작성하는 마이크로 벤치마크는 시간이나 가비지 수집만 측정할 뿐 실제 영향을 미치는 요인을 포착하지 못하는 경우가 많습니다.
실제로 필요한 개발자와 개발팀만이 마이크로벤치마크를 작성해야 합니다. 이러한 벤치마크는 완전히 공개되고(소스 코드 포함) 재현 가능해야 하며 동료 검토 및 추가 조사를 거쳐야 합니다.
Java 플랫폼의 많은 최적화에서는 통계적 실행과 단일 실행이 결과에 큰 영향을 미치는 것으로 나타났습니다. 진실되고 신뢰할 수 있는 답을 얻으려면 단일 벤치마크를 여러 번 실행한 다음 결과를 집계해야 합니다.
독자들이 마이크로 벤치마크 작성의 필요성을 느낀다면 Georges, Buytaert 및 Eeckhout의 "Statistically Rigorous Java Performance Evaluation" 논문이 좋은 시작이 될 것입니다. 적절한 통계 분석이 없으면 우리는 쉽게 오해를 받을 수 있습니다.
주변에는 잘 개발된 도구와 커뮤니티(예: Google Caliper)가 많이 있습니다. 정말로 마이크로벤치마크를 작성해야 한다면 직접 작성하지 마세요. 필요한 것은 동료의 의견과 경험입니다.
4. 느린 알고리즘은 성능 문제의 가장 흔한 원인입니다
개발자(일반 대중 포함) 사이에는 매우 흔한 인지 오류가 있습니다. 시스템의 모든 것이 중요합니다.
이러한 인지 오류는 Java 성능을 논의할 때도 반영됩니다. Java 개발자는 알고리즘의 품질이 성능 문제의 주요 원인이라고 믿습니다. 개발자는 코드를 생각하다 보니 자연스럽게 자신만의 알고리즘을 생각하게 되는 경향이 있습니다.
실제로 일련의 실제 성능 문제를 다룰 때 사람들이 알고리즘 설계가 근본적인 문제라고 생각할 확률은 10% 미만입니다.
반대로 가비지 수집, 데이터베이스 액세스 및 구성 오류는 알고리즘보다 애플리케이션 속도를 저하시킬 가능성이 더 높습니다.
대부분의 애플리케이션은 상대적으로 적은 양의 데이터를 처리하므로 기본 알고리즘이 비효율적이라 하더라도 일반적으로 심각한 성능 문제를 일으키지 않습니다. 확실히 우리의 알고리즘은 최적이 아닙니다. 그럼에도 불구하고 알고리즘으로 인해 발생하는 성능 문제는 여전히 상대적으로 적으며 애플리케이션 스택의 다른 부분으로 인해 더 많은 성능 문제가 발생합니다.
따라서 가장 좋은 조언은 실제 생산 데이터를 사용하여 성능 문제의 실제 원인을 찾아내는 것입니다. 성능 데이터를 측정하세요. 추측하지 마세요!
기존 아키텍처를 완전히 이해하지 못하고 분석이 중단된 경우 "캐싱이 모든 문제를 해결할 수 있다"는 오류가 추악한 고개를 드는 경우가 많습니다.
개발자의 눈에는 무서운 기존 시스템을 다루는 것보다 앞에 캐싱 레이어를 추가하고 기존 시스템을 숨기고 최선을 다하는 것이 좋습니다. 의심할 바 없이 이 접근 방식은 전체 아키텍처를 더욱 복잡하게 만들 뿐이며, 인수를 맡는 다음 개발자가 시스템의 현재 상태를 이해하려고 하면 상황은 더욱 악화될 것입니다.
대규모의 잘못 설계된 시스템은 전체적인 디자인이 부족하고 한 번에 한 줄의 코드와 한 하위 시스템으로 작성되는 경우가 많습니다. 그러나 대부분의 경우 아키텍처를 단순화하고 리팩터링하면 성능이 향상되고 거의 항상 이해하기가 더 쉽습니다.
따라서 캐싱 추가가 꼭 필요한지 평가할 때 먼저 몇 가지 기본 사용 통계(예: 적중률, 실패율 등)를 수집하여 캐싱 계층이 가져오는 실제 가치를 입증할 계획을 세워야 합니다. .
6. 모든 애플리케이션은 Stop-The-World 문제에 주의해야 합니다.
Java 플랫폼에는 변하지 않는 사실이 있습니다. 가비지 수집을 실행하려면 모든 애플리케이션 스레드가 주기적으로 일시 중지됩니다. 이는 실제 증거가 없음에도 불구하고 Java의 심각한 단점으로 인용되기도 합니다.
실증적 연구에 따르면 디지털 데이터(예: 가격 변동)가 200밀리초에 한 번 이상 자주 변경되면 사람들은 이를 정상적으로 인식할 수 없게 됩니다.
애플리케이션은 주로 사람이 사용하는 것이므로 200ms 이하의 STW(Stop-The-World)는 일반적으로 아무런 영향을 미치지 않는다는 유용한 경험 법칙이 있습니다. 일부 응용 프로그램에는 더 높은 요구 사항(예: 스트리밍 미디어)이 있을 수 있지만 많은 GUI 응용 프로그램에는 이 요구 사항이 필요하지 않습니다.
일부 애플리케이션(예: 지연 시간이 짧은 거래 또는 기계 제어 시스템)은 200밀리초의 일시 중지를 허용할 수 없습니다. 이러한 유형의 애플리케이션을 작성하지 않는 한 사용자는 가비지 수집기의 영향을 거의 느끼지 못할 것입니다.
애플리케이션 스레드 수가 물리적 코어 수를 초과하는 시스템에서는 운영 체제가 CPU에 대한 시분할 액세스를 제어해야 한다는 점을 언급할 가치가 있습니다. Stop-The-World는 무섭게 들리지만 실제로 모든 애플리케이션(JVM이든 다른 애플리케이션이든)은 부족한 컴퓨팅 리소스에 대한 경합 문제에 직면해야 합니다.
측정하지 않으면 JVM이 애플리케이션 성능에 어떤 추가 영향을 미칠지 불분명합니다.
어쨌든 일시정지 시간이 실제로 애플리케이션에 영향을 미치는지 확인하려면 GC 로그를 켜시기 바랍니다. 수동으로 또는 스크립트나 도구를 사용하여 로그를 분석하여 일시 중지 시간을 결정합니다. 그런 다음 실제로 응용 프로그램에 문제가 발생하는지 확인합니다. 가장 중요한 것은 스스로에게 중요한 질문을 던지는 것입니다. 사용자가 실제로 불만을 제기하고 있습니까?
7. 손으로 쓴 개체 풀은 대규모 애플리케이션에 적합합니다.
Stop-The-World 일시 중지가 어느 정도 나쁘다고 생각하는 애플리케이션 개발 팀의 일반적인 반응은 자체적으로 Java로 구현하는 것입니다. 힙 메모리 관리 기술. 이는 종종 객체 풀(또는 전체 참조 카운팅)을 구현하고 도메인 객체를 사용하는 코드를 포함하도록 요구하는 것으로 귀결됩니다.
이 기술은 거의 항상 오해의 소지가 있습니다. 이는 객체 할당 비용이 매우 비싸고 객체 수정 비용이 훨씬 저렴했던 과거에 대한 이해를 기반으로 합니다. 지금은 상황이 완전히 다릅니다.
최신 데스크탑 또는 서버 하드웨어를 사용하면 오늘날의 하드웨어는 할당이 매우 효율적이며 메모리 대역폭은 최소 2~3GB입니다. 이는 큰 숫자이며, 애플리케이션을 특별히 작성하지 않는 한 이렇게 큰 대역폭을 최대한 활용하기는 쉽지 않습니다.
일반적으로 개체 풀링을 올바르게 구현하는 것은 매우 어렵고(특히 여러 스레드가 작동하는 경우) 개체 풀링에는 몇 가지 부정적인 요구 사항도 있으므로 이 기술은 일반적으로 좋은 선택이 아닙니다.
객체 풀 코드를 접하는 모든 개발자는 객체 풀을 이해하고 올바르게 처리할 수 있어야 합니다.
객체 풀을 아는 코드와 객체 풀을 모르는 코드, 경계 그리고 문서에 기록해야 합니다
이러한 추가적인 복잡성은 정기적으로 업데이트되고 검토되어야 합니다
그 중 하나라도 충족되지 않으면 조용히 문제가 발생할 위험이 있습니다( C의 포인터 재사용과 유사) 다시 돌아왔습니다
간단히 말해서 개체 풀은 GC 일시 중지가 허용되지 않는 경우에만 사용할 수 있으며 튜닝 및 재구성으로 일시 중지를 허용 가능한 수준으로 줄이는 데 실패합니다.
8. 가비지 수집에서는 Parallel Old에 비해 CMS가 항상 더 나은 선택입니다
Oracle JDK는 기본적으로 병렬 Stop-The-World 수집기를 사용합니다. , 병렬 오래된 수집기.
CMS(Concurrent-Mark-Sweep)는 대부분의 가비지 수집 주기 동안 애플리케이션 스레드가 계속 실행되도록 허용하는 대안이지만 비용이 많이 들고 몇 가지 주의 사항이 있습니다.
애플리케이션 스레드가 가비지 수집 스레드와 함께 실행되도록 허용하면 필연적으로 문제가 발생합니다. 애플리케이션 스레드가 개체 그래프를 수정하여 개체의 실행 가능성에 영향을 미칠 수 있습니다. 이 상황은 사후에 정리되어야 하므로 CMS에는 실제로 두 개의 STW 단계(보통 매우 짧음)가 있습니다.
이로 인해 다음과 같은 결과가 발생합니다.
모든 애플리케이션 스레드는 안전한 지점으로 이동해야 하며 각 Full GC 중에 두 번 일시 중지됩니다.
가비지 수집 및 애플리케이션이 있더라도; 동시에 실행되지만 애플리케이션 처리량이 감소합니다(보통 50%).
가비지 수집을 위해 CMS를 사용하는 경우 JVM에서 사용하는 장부 정보(및 CPU 주기)가 다른 병렬 처리보다 훨씬 높습니다. 수집가.
이런 비용이 돈만큼 가치가 있는지는 애플리케이션에 따라 다릅니다. 하지만 공짜 점심은 없습니다. CMS 컬렉터의 디자인은 훌륭하지만 만병통치약은 아닙니다.
따라서 CMS가 올바른 가비지 수집 전략인지 확인하기 전에 먼저 Parallel Old의 STW 일시 중지가 실제로 허용되지 않으며 조정할 수 없는지 확인해야 합니다. 마지막으로 모든 지표는 생산 시스템과 동등한 시스템에서 얻어야 함을 강조합니다.
9. 힙 크기를 늘리면 메모리 문제를 해결할 수 있습니다
애플리케이션에 문제가 있고 GC 문제가 의심될 때 많은 애플리케이션 팀의 반응은 힙 크기를 늘리는 것입니다. 경우에 따라 이는 빠른 결과를 제공하고 더 사려 깊은 솔루션을 고려할 시간을 제공합니다. 그러나 성능 문제의 원인을 완전히 이해하지 못한 경우 이 전략은 상황을 더욱 악화시킬 수 있습니다.
많은 도메인 개체를 생성하는(예: 2-3초의 수명을 나타내는) 매우 잘못 코딩된 애플리케이션을 생각해 보세요. 할당률이 충분히 높으면 가비지 수집이 자주 발생하므로 도메인 개체가 이전 세대로 승격됩니다. 도메인 개체가 Old Generation에 진입하자마자 생존 시간이 종료되고 바로 죽지만 다음 Full GC까지 재활용되지 않습니다.
애플리케이션의 힙 크기를 늘리면 상대적으로 수명이 짧은 객체가 들어가고 죽는 데 사용되는 공간만 늘어나게 됩니다. 이로 인해 Stop-The-World 일시 중지 시간이 길어지고 이는 애플리케이션에 유익하지 않습니다.
힙 크기를 수정하거나 다른 매개변수를 조정하기 전에 개체 할당 및 수명의 역학을 이해해야 합니다. 성과 데이터를 측정하지 않고 맹목적으로 행동하는 것은 상황을 더욱 악화시킬 뿐입니다. 여기서는 가비지 수집기의 구세대 배포가 특히 중요합니다.
결론
Java의 성능 튜닝에 있어서 직관은 오해를 불러일으키는 경우가 많습니다. 플랫폼의 동작을 시각화하고 이해를 높이는 데 도움이 되는 실험 데이터와 도구가 필요합니다.
가비지 컬렉션이 가장 좋은 예입니다. 튜닝을 안내하기 위한 데이터를 튜닝하거나 생성하는 경우 GC 하위 시스템은 무한한 잠재력을 가지고 있지만 프로덕션 애플리케이션의 경우 도구를 사용하지 않고는 생성된 데이터의 의미를 이해하기 어렵습니다.
기본적으로 Java 프로세스(개발 환경 및 프로덕션 환경 포함)를 실행할 때 항상 최소한 다음 매개변수를 사용해야 합니다.
-verbose:gc(GC 로그 인쇄)
- Xloggc: (더 포괄적인 GC 로그)
-XX:+PrintGCDetails (더 자세한 출력)
-XX:+PrintTenuringDistribution (객체를 이전 세대로 승격하기 위해 JVM에서 사용하는 연령 임계값 표시)
그런 다음 도구를 사용하여 로그를 분석하거나, 그래프를 생성하거나, GCViewer(오픈 소스) 또는 jClarity Censum과 같은 시각화 도구를 사용할 수 있습니다.