Java NIO
Java NIO(Non-blocking I/O)
Java NIO 는 JDK 1.4 에 적용된 패키지로 기존의 Java I/O 느린 단점을 보완하기 위해 추가되었다.
기존 Java IO
기본적으로 Java 에서는 C/C++ 처럼 메모리를 직접 관리하거나 OS 수준의 시스템 콜(커널 라이브러리)을 직접 사용 할 수 없다.
- JNI 등을 이용하는 방법은 논외로 둔다.
기존의 Java IO 는 커널 버퍼를 직접 접근할 수 없었다.
그래서 소켓이나 파일 IO 가 발생하면 JVM 은 커널의 버퍼 영역에서 JVM 내부 메모리로 해당 데이터를 불러온 후에 접근이 가능했기 때문에 커널 버퍼의 데이터를 JVM 내부 버퍼로 복사하는 오버헤드 가 존재했음.
출처 : http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/
위 그림은 기존의 Java IO 가 디스크에서 파일을 읽는 과정
- JVM 프로세스는 file 을 읽기 위해 커널에 명령을 전달
- 커널은 시스템 콜을 사용하여 DMA(Direct Memory Access) 를 호출
- DMA 는 디스크 컨트롤러를 이용해 디스크에서 파일을 읽어 커널 버퍼로 복사
- JVM 은 커널 버퍼의 내용을 내부 버퍼에 복사
DMA(Direct Memory Access)
DMA 는 CPU 와 독립적으로 메모리에 접근 할 수 있게 하는 시스템의 기능
PIO(Programmed I/O) 는 CPU 가 주변장치와 데이터를 주고 받는 방식으로 CPU 리소스를 사용하는 오버헤드가 존재
이를 극복하기 위해서 DMA 가 개발되었다.
CPU 가 DMA 컨트롤러를 호출 > DMA 컨트롤러가 주변장치 데이터를 읽은 후 메모리 전송 > 전송완료 후 CPU 에게 완료 신호 송신
이런 과정은 다음과 같은 오버헤드가 있음
- JVM 내부 버퍼로 복사하는 과정에서 CPU 리소스 사용하기 때문에 오버 헤드
- 커널 버퍼 > JVM 버퍼로 데이터를 복사하는 일은 CPU 를 사용하게 됨 (PIO)
- DMA 를 이용해 CPU 자원을 사용하지 않으면 CPU 자원을 다른 곳에서 사용할 수 있음
- JVM 내부 버퍼는 사용 후 GC 대상
- 복사를 진행하는 동안 I/O 를 요청한 thread 는 blocking 됨
- OS 는 디스크에서 읽는 효율을 높이기 위해 최대한 많은 양의 데이터를 커널 버퍼에 저장함
- 따라서 커널 버퍼에서 JVM 내부 버퍼로 데이터를 복사하는 동안 해당 thread 는 다른 일을 할 수 없음
따라서 이런 오버헤드를 줄이기 위해서 커널 버퍼를 JVM 에서 직접 사용하기 위해서 NIO 가 등장
Java NIO
NIO 핵심 용어
- Channel
- Buffer
- Selector
Channel
- 압출력 중 1개만 가능한 단방향 채널과 입출력 모두 가능한 양방향 채널을 지원하는 클래스
- 기존의 Stream 기반 IO 에서는 읽기 or 쓰기만 가능했음 (단방향)
- blocking 과 non-blocking 모두 지원
- non-blocking IO 를 위해서 데이터를 요청한 스레드와 데이터가 있는 버퍼 사이에서 동작 가능
Channel 클레스
- FileChannel : 파일 입출력
- DatagramChannel : UDP 네트워크 입출력
- SocketChannel : TCP 네트워크 입출력
- ServerSocketChannel : TCP 연결 수신을 대기 하고 각 연결에 대해 SocketChannel 을 생성
NIO 는 Channel 을 통해 데이터 입출력을 처리함
- 입출력 요청 스레드는 채널에 데이터 입력 요청
- 채널은 해당 데이터를 버퍼에 추가
- 스레드는 버퍼에서 데이터를 확인
Java NIO Scatter/Gather
- Scatter/Gather 는 채널에서 데이터 read/write 에서 사용하는 개념
Scattering Read
- 하나의 채널은 여러 버퍼로 데이터를 읽음
- 따라서 채널은 데이터를 여러 버퍼로 분산함
ByteBuffer head = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] byteBuffers = {head, body};
channel.read(byteBuffers);
- 위 예시는 고정된 크기의 헤더와 본문이 있을 경우 분산 읽기를 수행하는 예제
- channel.read() 에서는 버퍼가 발생하는 순서대로 데이터를 채움
- head 를 먼저 채우고 body 영역을 채운다
- 즉, 고정된 크기에 버퍼를 이용하여 분산 읽기가 가능함
Gathering Write
- 여러 버퍼의 데이터를 단일 채널에 쓰기
- 따라서 채널은 여러 버퍼의 데이터를 하나의 채널로 수집 가능
ByteBuffer head = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] byteBuffers = {head, body};
channel.write(byteBuffers);
- channel.write() 를 통해 버퍼의 내용을 순서대로 쓰기를 수행
- 실제로 버퍼는 사용하는 만큼만 기록됨
- head 버퍼는 128 바이트 할당했지만 실제로 58 바이트만 사용중이면 58 바이트만 기록됨
- 따라서 동적 데이터에 대한 처리도 가능함
Scatter/Gather
각 버퍼를 개별적으로 매핑하여 전송하지 않고 여러 버퍼 영역에 한번에 전송할 수 있는 DMA 기술
물리적으로 인접하지 않은 여러 버퍼가 있을때 논리적으로 하나의 버퍼로 처리할 수 있도록 도와주는 기법
시스템 콜 호출 빈도를 줄이기 위해서 사용한다
Buffer
- 버퍼는 채널과 상호작용할 때 사용되며 채널에서 데이터 읽거나 쓸 때 사용함
Buffer 클래스
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
Direct Buffer vs Non Direct Buffer
Direct Buffer
- 커널 버퍼를 사용하는 방식으로 위에서 말한 오버헤드를 방지할 수 있음
- 다만 Byte 형식으로만 사용할 수 있음
ByteBuffer buffer = ByteBuffer.allocateDirect(10);
- 버퍼의 할당 및 해제 비용이 높음
- 위 방식을 사용한 객체의 래퍼런스는 JVM Heap 에 생성되어 GC 되지만 커널 버퍼 영역은 JVM 이 아닌 커널에서 제어를 수행함
- 따라서 IO 영향을 많이 받는 life time 긴 버퍼에 대해서만 할당하는 것이 좋음
Non Direct Buffer
- 기존 Java IO 처럼 JVM 내부 메모리에 버퍼를 복사하는 방식을 사용함
- ByteBuffer.allocateDirect 를 제외한 모든 버퍼가 Non Direct Buffer
// heap used
ByteBuffer buffer = ByteBuffer.allocate(10);
일반적인 버퍼 사용
- 버퍼에 데이터 쓰기
- 쓰기모드에서 읽기 모드로 전환 (buffer.flip())
- 버퍼에 데이터 읽기
- 읽은 데이터를 버퍼에서 지운다. (buffer.clear() / buffer.compact())
- buffer.clear() : 버퍼 전체를 비움
- buffer.compact() : 읽은 데이터만 지움
버퍼의 속성
- Capacity : 버퍼의 고정된 크기로 버퍼가 가득차지 않도록 버퍼를 비워야 함
- Position
- 처음 0에서 시작하며 읽기와 쓰기에서 다른 의미를 갖음
- 쓰기 수행시 다음에 쓰여질 위치를 의미
- 읽기 수행시 읽을 위치를 의미
- 쓰기 모드에서 읽기 모드로 전환되면 위치는 0으로 재설정됨
- Limit
- 쓰기 모드에서는 Buffer 에 쓸 수 있는 데이터의 한계
- 읽기 모드에서는 읽을 수 있는 데이터의 제한을 의미
- 읽기 모드로 전환될 때 쓰기 모드의 쓰기 위치로 제한이 설정됨
Selector
- 하나의 스레드로 여러 채널을 관리하고 처리할 수 있는 클래스 (Reactor 패턴)
등장 배경
- Selector 이전에는 각 채널에 응답을 기다리기 위해서 스레드와 채널을 1:1 매핑
- 따라서 각 스레드는 blocking 된 상태로 채널의 응답을 기다린다
- 하지만 채널이 매우 많아지면 채널 응답을 대기하는 스레드도 늘어나기 때문에 리소스 낭비가 발생한다
이런 단점을 보완하기 위해서 1개의 스레드로 여러 채널을 관리할 수 있는 Selector 가 도입됨
멀티 플렉싱
- 하나의 채널을 통해 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술
- 물리적 장치의 효율성을 높이기 위해 최소한의 물리적인 요소만 사용해서 최대한의 데이터를 전달하기 위해서 사용하는 기술
IBM 비동기 bokcing I/O
- 어플리케이션에서 커널에 IO 요청을 보내면 커널은 IO 미완료 상태를 전송
- 어플리케이션은 커널의 응답을 계속 select 한다
- select 의 결과가 유의미한 값이 나오면 어플리케이션은 커널로 부터 IO 처리를 시작한다
- IO 전체가 blocking 되는 것이 아니라 커널의 응답이 blocking 된 것이다.
Java NIO Selector
출처 : http://tutorials.jenkov.com/java-nio/selectors.html
- Java 의 셀렉터는 커널 라이브러리인 select() 를 사용하기 위해서 지원하는 클래스
- 실제로 OS 환경에 따라 지원하는 select() 가 다름 (select, poll, epoll, etc..)
- 채널을 셀렉터에 등록하고 select() 호출하면 채널 들 중에서 준비가 완료된 채널을 가져올 수 있음
- 반환할 채널이 없다면 block 됨
- 셀렉터에 의해서 채널이 반환되면 스레드는 해당 채널을 이용해 이벤트 처리 가능
Selector 장점
- 단일 스레드로 여러 채널을 처리하기 때문에 스레드 수가 적어도 된다
- 스레드 간 전환은 OS 비용이 많이 들고 메모리를 차지하기 때문에 스레드가 적을 수록 좋음
Reference
- https://docs.oracle.com/javase/8/docs/api/java/nio/package-summary.html
- http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/
- https://brunch.co.kr/@myner/47
- https://ko.wikipedia.org/wiki/%EC%A7%81%EC%A0%91%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%A0%91%EA%B7%BC
- https://velog.io/@tkadks123/Java-NIO%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-12
- http://tutorials.jenkov.com/java-nio/scatter-gather.html
- http://tutorials.jenkov.com/java-nio/buffers.html
- https://hbase.tistory.com/39
- https://incredible-larva.tistory.com/entry/IO-Multiplexing-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-1%EB%B6%80