동시성 문제 하드웨어 발전이 두드러지던 시기에는 더 나은 성능을 얻기 위해 단순히 하드웨어를 교체하기만 하면 됐다. 그러나 더 이상 CPU 클록을 높여도 성능 향상을 기대하기 어려운 시대가 되었고, 하드웨어 개발자들은 멀티코어라는 새로운 방법을 찾았다. 덕분에 소프트웨어 개발자들은 예전처럼 하드웨어만 바꾸는 것이 아니라, 소프트웨어 개발에도 멀티코어를 활용하기 위한 동시성 프로그래밍이 필요해졌다. 그러나 동시성 프로그래밍은 기존의 순차적인 프로그래밍보다 훨씬 복잡하고 어렵다. 아래와 같은 예제를 보자. public class Counter { private int i; public Counter(int i) { this.i = i; } public int increment() { return i = i..
Java
리소스 반환 Java를 사용할 때는 GC 덕분에 객체를 직접 삭제하지 않아도 된다. 그러나 객체 내에서 리소스를 가지고 있는 경우, 이것을 반환하지 않고 GC가 일어나면 리소스가 낭비된다. 예를 들어, File을 읽기 위해 InputStream을 열거나, DB 연결을 위해 Connection을 여는 경우, 사용이 끝나면 close를 호출해줘야한다. 그렇지 않으면 file descriptor나 DB Connection 같은 한정된 리소스를 낭비하게 된다. 그러나 close 호출을 빼먹는다고 컴파일 에러가 나거나 exception이 발생하지도 않는다. 따라서 오랜 시간이 지나 문제가 발생하게되고, 이것을 디버깅하기 위해서는 오랜 시간이 걸린다. Java 개발자들은 이러한 실수를 줄이기 위해 여러 방법을 시도..

Java Collections Framework Java Collection은 객체들의 집합을 저장하는 객체라고 할 수 있다. Java 사용자라면 익숙한 Set, List 등이 대표적인 Collection이다. 그리고 이러한 Collection들을 사용하는 표준화된 방법을 제공하는 것이 Collections Framework이다. Set, List같은 Collection들을 인터페이스로 정의하고, 구체적인 구현체들을 포함하며, 다양한 편의 메소드를 제공한다. [이미지 출처] https://www.geeksforgeeks.org/how-to-learn-java-collections-a-complete-guide/ Collection 인터페이스는 위의 그림처럼 구성된다. Collection은 크게 List,..
JIT 최적화 기법 지난 글에서 JIT 최적화의 기법으로 메소드 인라이닝, 루프 펼치기, 탈출 분석을 살펴보았다. 이번 글에서는 단형성 디스패치, 인트린직, 온스택 치환 기법을 살펴보도록 하자. 단형성 디스패치(Monomorphic Dispatch) 단형성 디스패치는 메소드 호출을 최적화하는 방법이다. 일반적으로 Java에서 메소드를 호출할 때 객체의 타입에 따라 호출되는 메소드가 달라진다. 따라서 객체의 OOP(Ordinary Object Pointer)를 참조해 호출할 메소드를 찾아 실행하는 과정을 거친다. 그러나 대부분의 경우, 메소드를 호출하는 위치에서 객체의 타입은 한 가지로 결정된다. 이것을 이용해 메소드를 찾는 과정을 없애 한 가지 메소드만 호출하도록 바꾸는 것이 단형성 디스패치이다. 예를 ..
JIT 컴파일 우리가 작성한 소스코드를 컴퓨터가 실행하도록 만들기 위해서는 컴퓨터의 명령어로 변환해줘야 한다. C/C++같은 언어들은 실행하려는 컴퓨터의 환경에 맞게 미리 컴파일해서 실행가능 파일을 만든다. 반면, Python이나 JavaScript같은 인터프리터 언어들은 소스코드를 한 줄씩 읽어 실행가능한 명령어로 변환한다. 컴파일 언어는 실행가능 파일을 실행만 하면 되기 때문에 비교적 성능이 좋고, 인터프리터 언어는 속도는 비교적 느리지만 인터프리터가 있는 환경이라면 어디서든 같은 코드를 실행시킬 수 있다는 장점이 있다. Java는 독특하게 컴파일 언어의 특징과 인터프리터 언어의 특징을 모두 가진다. 소스코드를 컴파일해서 바이트 코드로 변환하고, 바이트 코드는 JVM의 실행 엔진에 있는 인터프리터로 ..

바이트 코드는 어떻게 생겼나? 우리가 작성한 Java 코드는 자바 컴파일러에 의해 바이트 코드로 변환된다. 바이트 코드는 클래스 로더에 의해 JVM 런타임 메모리에 올라간다. JVM의 실행 엔진의 인터프리터가 메모리의 바이트 코드를 한 줄씩 실행한다. 여기까지 알았다면, 이제 바이트 코드를 직접 확인해보자. 아주 간단한 아래와 같은 자바 코드가 있다고 하자. public class Test { public static void main(String[] args) { String hello = "Hello"; String name = "Java"; System.out.println(hello + " " + name + "!"); } } 위 코드를 컴파일한 다음, 우리가 알아볼 수 있게 디컴파일해서 확인해..

GC 모니터링 가비지 컬렉션은 많은 Java 개발자들을 편하게 만들어 줬지만, 동시에 내부에서 일어나고 있는 일을 파악하기 어렵게 만들었다. 단독으로 실행하는 데스크탑 어플리케이션이나 사용자가 적은 백오피스 서버의 경우 문제가 되지 않지만, 사용자가 많은 서비스에서 성능을 극한까지 끌어올려야 하는 경우는 다르다. JVM 런타임 메모리에서는 어떤 일이 일어나고 있는지, 가비지 컬렉터는 어떻게 동작하는지 알고있어야 제대로 튜닝할 수 있다. 따라서 GC를 모니터링할 수 있는 도구가 필요하다. GC 로그 가장 기본적인 방법이자, 가장 중요한 것은 GC 로그를 출력하는 것이다. GC 로그는 메모리 할당에 실패한다거나, Full GC가 일어나는 등 특정한 이벤트가 발생할 때마다 기록된다. 이렇게 로그를 기록하는 것..

G1GC(Garbage-First GC) G1GC는 충분히 큰 메모리를 가진 멀티프로세서 환경을 위한 가비지 컬렉터이다. CMS GC처럼 STW에 의해 멈추는 시간을 최소화하면서도, thourghput 저하를 최소화하도록 만들어졌다. Java 9부터 최신버전(현재 20)까지 기본 가비지 컬렉터로 사용되고 있다. 그만큼 일반적인 상황에서 성능이 좋고 안정적이라는 뜻이다. G1GC는 이전 글에서 설명한 것들과 대부분 비슷한 특징을 갖는다. latency가 짧고, 일부 동작은 어플리케이션과 동시에 실행되며, STW도 발생한다. 또한, 세대별 가설을 따라 Young, Old 영역을 가지며, 대부분의 GC는 Young 영역에서 일어나고 가끔 일어나는 Old GC는 비교적 오래 걸린다. 여기까지는 일반적인 GC와 ..