"자바는 느리다" - 어디가 느릴까? 진짜 느릴까?
"자바는 느리다." 라는 말은 어느 정도 프로그래밍을 해온 개발자들이라면 들어봤을 법한 말이다.
그런데 자바가 어디가 느리고 왜 느린지, 그리고 얼마나 느린지에 대한 답을 하는 개발자는 많지 않다.
이번 포스트부터 4개 정도의 포스트로 연재할 내용은 자바는 어떠한 이유로 느리고 그것을 개선한 방법은 무엇인지에 대한 것이다.
- 자바는 어디가, 왜 느릴까?
일단 자바는 C/C++과 달리 포인터로 1. 직접 메모리를 관리하고 운영체제 수준의 2. 시스템 콜을 직접 사용할 수 없다.
JVM이라는 프로세스 위에서 동작하는 추상화된 비교적 고수준의 언어이기 때문에 자바는 느리다.
기타 GC(Garbage Collection)의 메모리 점유 문제등 느린 이유는 더 많이 있다... 여기서 말하고자 하는 것은 위의 2가지 포인트를 개선한 NIO에 대한 것이다.
물론 C/C++과 JAVA의 속도 차이가 엄청난 것은 아니다. (정렬, 생성등의 테스트 한정. + 임베디드 같이 열악한(?) 환경을 제외하면..)
자바가 특별히 성능이 좋지 않은 부분은 스윙(Swing)과 I/O다.
스윙은 관심이 있는 부분이 아니기 때문에 건너뛰고 자바 I/O를 봤을 때 자바의 속도를 개선하는 방법은 메모리를 직접 접근하는 듯!하게 사용하고 시스템 콜을 직접 콜하는 듯!하게 하는 방법이다. + 동기/비동기 제어
그런 방법을 사용해서 I/O의 성능 문제를 개선하는 것이 바로 java.nio.XXX 패키지다. (NIO)
위 그림은 자바에서 IO를 처리하는 전체 구조를 보여주는 그림이다.
유저 영역은 일반적인 프로세스들(실행중인 프로그램)이 존재하는 제한된 권한을 갖는 영역이다.(하드웨어 직접 접근 불가) 반대로 커널 영역은 운영체제에 존재하는 영역으로 하드웨어에 직접 접근이 가능하고 다른 프로세스를 제어할 수 있는 권한이 있다.
자바 I/O프로세스를 정리해보면, (파일 읽기 시도)
1. 제일 먼저 프로세스는 커널에 파일 읽기 명령을 내린다.
2. 커널은 시스템 콜(read())를 사용해서 디스크 컨트롤러가 물리적 디스크로부터 읽어온 파일 데이터를 커널 영역안의 버퍼에 쓴다.
3. 모든 파일 데이터가 버퍼에 복사되면 다시 프로세스안의 버퍼로 복사를 한다.
4. 프로세스 안의 버퍼의 내용으로 프로그래밍한다.
여기서 비효율적인 부분이 바로 보인다.
커널 영역의 데이터를 프로세스 안의 버퍼로 저장하는 일이다. 커널 영역의 버퍼를 직.접. 접근할 수 있다면 굳이 프로세스안의 버퍼로 복사하면서 CPU를 낭비하지 않아도 되고 GC의 관리도 필요없어진다.
* 물리적 디스크에서 디스크컨트롤러가 커널영역의 버퍼로 읽어오는 것은 DMA(Direct Memory Access)기능으로 CPU의 도움없이도 가능하다.
추가적인 문제점은 위에 있는 IO프로세스를 거치는 동안 작업을 요청한 쓰레드는 블록킹된다는 것이다.
이런 문제점을 Non-blocking IO(NIO)를 사용해서 블록킹되는 문제를 개선할 것이다.
NIO의 키워드 3가지는 버퍼, 채널, 셀렉터다. (각각을 잘 이해하면 성능이 좋은 NIO를 이용할 수 있다.)
1. 자바의 포인터 버퍼 (NIO에서 제공하는 Buffer클래스)
- 커널에 의해 관리되는 시스템 메모리를 직접 사용할 수 있는 Buffer 클래스
2. 채널 (Channel)
- 읽기, 쓰기 하나씩 쓸 수 있는 스트림은 단방향식, 채널은 읽기 쓰기 둘다 가능한 양방향식 입출력 클래스
- 네이티브 IO , Scatter/Gather 구현으로 효율적인 IO처리 (시스템 콜 수 줄이기, 모아서 처리하기)
3. 셀렉터 (Selector)
- 네트워크 프로그래밍의 효율을 높이기 위한 것
- 클라이언트 하나당 쓰레드 하나를 생성해서 처리하기 때문에 쓰레드가 많이 생성될 수록 급격한 성능 저하를 가졌던 단점을 개선하는 Reactor패턴의 구현체
I/O 향상을 위한 운영체제 수준의 기술
1. 버퍼
데이터를 한개 씩 여러번 반복적으로 전달하는 것보다 중간에 버퍼를 두고 모아서 한 번에 전달하는 것이 훨씬 효율적이다.
1바이트씩 읽는 것, 한 줄씩 읽는 것, 한 번에 읽는 것을 다른 포스트에서 비교했었으므로 생략한다.
2. Scatter/Gather
자바안에서 여러 개의 버퍼를 만들어 사용하는데 만약 동시에 각각 버퍼에 데이터를 쓰거나 읽는다고 한다면 시스템 콜을 여러번 불러서 읽거나 쓰게 될 것이다.
시스템 콜을 호출하는 것은 가벼운 작업이 아니므로 이렇게하는 것은 비효율적이다.
Scatter와 Gather는 프로세스에서 사용하는 버퍼 목록을 한 번에 넘겨줌으로서, 운영체제에서는 최적화된 로직에 따라 버퍼들로부터 순차적으로 데이터를 읽고 쓸 수 있게 되는 기능이다.
3. 가상 메모리 *****
가상 메모리는 프로그램이 사용할 수 있는 주소 공간을 늘리기 위해 운영체제에서 지원하는 기술이다.
실제 프로그램이 실행되는 데 지금 필요한 페이지(단위)의 가상 주소만 물리 메모리에 넣어 놓는 것이다.
이로써 얻는 장점은 2가지가 있는데 하나는 실제 메모리 크기보다 큰 가상 메모리 공간을 사용할 수 있다는 점이고 다른 하나는 여러 개의 가상 주소가 하나의 물리적 메모리를 참조함으로써 메모리를 효율적으로 사용할 수 있게 해준다는 점이다.
즉, 유저 영역의 버퍼(가상 주소)와 커널 영역의 버퍼(가상 주소)가 같은 물리 메모리를 참조하게 매핑시키면 커널 영역에서 유저 영역으로 데이터를 복사하지 않아도 되게 된다.
4. 메모리 맵 파일
운영체제가 지원하는 Memory-mapped IO는 파일시스템의 페이지들과 유저 영역의 버퍼를 가상 메모리로 매핑시키는 방법이다.
즉, 유저 가상 메모리와 커널 가상메모리가 보는 물리메모리에 디스크(정확히는 디스크 블록)의 내용까지 일치시켜버리는 것이다.
그러면 별도의 입출력 과정을 거치지 않고 자동으로 디스크에 반영되게 해버리는 것이다.
여기서 매우 큰 장점은 큰 파일을 복사하기 위해 많은 양의 메모리를 소비하지 않아도 된다는 점이다.
파일 시스템의 페이지(단위)들을 메모리로서 바라보기 때문에 그때그때 필요한 부분만을 실제 메모리에 올려놓고 사용하면 되기 때문이다.
5. 파일 락(File Lock)
파일락은 쓰레드에서 공부한 동기화와 비슷한 개념으로 어떤 프로세스가 어떤 파일에 락(lock)을 획득했을 때 다른 프로세스가 그 파일을 동시에 접근하지 못하게 하거나 접근할 수 있게하는 제한을 두는 것이다.
이 때 파일 전체 혹은 일부분을 잠궈서 사용하는데 바이트 단위로 계산해서 파일의 잠금 부분을 계산한다.
이렇게 파일 일부분만 잠궈서 사용함으로써 락이 설정되지 않은 파일의 다른 위치에서 여러 프로세스들이 동시에 다른 작업을 할 수 있게 하는 것이다.
이렇게 위의 5가지 기술을 사용하는 NIO를 공부하고 적용해보는 실습을 다음 포스트 부터 하겠다.
이번 포스트는 말 그대로 "개요" 수준이고 다른 포스트에서 키워드별로 파악해본다.
참고 자료
도서 - 자바 I/O NIO 네트워크 프로그래밍
'Java > Java IO+NIO' 카테고리의 다른 글
자바 파일 변경 감지, 와치서비스를 이용한 파일 변경 알림 받기(WatchService, WatchKey) (3) | 2018.08.28 |
---|---|
멀티 스레드 병렬 프로그래밍을 하기 전 반드시 읽어야할 것들 - Java 객체 편(객체 동기화, 클래스의 쓰레드 안정성) (4) | 2018.06.06 |
Java TCP 소켓 프로그래밍 예제 - 채팅프로그램 만들기 (멀티 쓰레드) (1) | 2018.02.21 |
자바 IO에서는 생성자만 보면 된다! (상속, 바이트스트림, 문자스트림, 객체스트림) (4) | 2018.02.18 |
Thread를 올바르게 생성, 시작, 중단하는 방법(feat. daemon thread와 자바런타임 메모리 공유) (5) | 2018.02.15 |