Engineering
Big things in JDK 13
2020년 5월 27일
원문에서 보기 ↗Java 13
6개월마다 GA 배포되는 신규 버전으로, openjdk13은 2019년 9월 17일에 배포되었습니다. 주요 기능은 아래와 같습니다.
- JEP350: Dynamic CDS Archives
- JEP351: ZGC: Uncommit Unused Memory (Experimental)
- JEP353: Reimplement the Legacy Socket API
- JEP354: Switch Expressions (Second Preview)
- JEP355: Text Blocks (Preview)
참고: https://openjdk.java.net/projects/jdk/13/
JEP350: Dynamic CDS Archives
openjdk12에서는 Default CDS Archives로 기본 기능으로 추가되었었죠. Dynamic CDS Archives는 확장된 버전입니다. 사용성을 개선하고 Default CDS Archives에는 없는, 로드된 애플리케이션의 클래스와 라이브러리 클래스를 포함하도록 개선되었습니다. AppCDS(Application Class-Data Sharing)는 Java 애플리케이션 실행이 종료될 때 동작하여, Java 프로세스들 사이에 공통으로 사용되는 메타데이터를 공유하는 방식으로 성능(주로 시작 시간)을 향상하기 위한 기능입니다.
Motivation
HotSpot에서 AppCDS를 사용하여 애플리케이션 클래스를 저장하면, 추가적인 Startup 시간과 메모리의 이점을 볼 수 있습니다만, 여전히 세 가지 정도의 추가적인 절차가 필요합니다.
- 클래스 리스트를 생성하기 위한 하나 이상의 trial run
- 생성된 클래스 리스트를 사용하여 아카이브를 덤프
- 아카이브와 함께 실행
또한, 이 세 가지의 절차는 기본 클래스 로더를 사용하는 애플리케이션에서만 동작합니다. HotSpot에서 experimental로 지원하기는 하지만 사용하기가 쉽지는 않다고 합니다.
Goals
JEP350에서는 이러한 불편함을 해결하기 위해서 Java 애플리케이션 실행 시, 간단하게 커맨드라인에 -XX:ArchiveClassesAtExit 옵션을 주는 것으로 AppCDS를 활성화 할 수 있습니다. 이렇게 실행된 Java 애플리케이션은 종료 시에 jsa라는 시스템 아카이브 파일을 생성하는데요. 해당 파일을 이용해 메타데이터를 공유하는 Java 애플리케이션을 향상된 성능으로 실행시킬 수 있습니다. 또한, 아래와 같이 옵션에 인수를 주어 생성될 아카이브 파일의 이름을 지정할 수 있습니다.
% bin/java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello
이렇게 생성된 아카이브 파일은 아래와 같이 사용할 수 있습니다.
% bin/java -XX:SharedArchiveFile=hello.jsa -cp hello.jar Hello
이 방식은 위의 '1. trial run' 절차를 제거하여 AppCDS의 사용이 간편해지고, 기본 제공 클래스 로더와 사용자 정의 클래스 로더 효과를 모두 지원한다고 합니다. 또한, JEP350의 개선기능은 애플리케이션의 첫 실행에서 자동 아카이브 생성을 수행할 수 있습니다. 그러면 '2. 아카이브 덤프' 절차를 제거할 수 있게 되고, 이는 CDS/AppCDS의 사용을 자동화할 수 있습니다.
JEP351: ZGC: Uncommit Unused Memory (Experimental)
사용하지 않는 힙 메모리를 OS에 반환하도록 ZGC를 개선하는 JEP입니다.
ZGC는 JDK11부터 실험적으로 도입된 GC입니다. ZGC에 관련된 간단한 설명은 이전에도 Big things in JDK 11에서도 이야기되었는데요. -XX:+UnlockExperimentalVMOptions XX:+UseZGC으로 ZGC를 활성화할 수 있습니다. 자세한 사용법과 설명은 여기를 참고해주세요.
Motivation
이 당시 ZGC는 오랫동안 사용하지 않더라도 메모리를 OS에 반환하지 않는 문제가 있었는데요. 이는 아래와 같은 몇몇 환경에서는 좋은 방식이 아니었습니다.
- 사용한 리소스만큼 비용을 지불하는 컨테이너 환경
- 오랫동안 유휴(Idle) 상태로 있거나 다른 애플리케이션들과 리소스를 공유하는 환경
- 시작상태와 실행상태의 메모리 사용량이 다른 환경 (실행 시에는 많은 메모리를 사용하지만, 실행 이후에는 일정한 메모리만을 사용하는 환경)
Goals
따라서 ZGC는 ZPage라는, page cache 내의 사용되지 않는 메모리 집합을 정해진 정책에 따라 커밋 해제하여 OS로 반환하되, 최소 힙 크기(-Xms) 아래로는 줄어들지 않도록 지정하게 하였습니다. (-Xms와 -Xmx가 동일한 경우, 이 기능이 암시적으로 비활성화됩니다. 명시적으로 비활성화하기 위해서는 -XX:-ZUncommit을 사용할 수 있습니다) 일반적으로 page cache는 LRU(Least Recently Used) 방식을 사용하고 page 크기별로 구분하기 때문에 메모리를 해제하는 방법은 비교적 간단합니다. 하지만, 문제는 캐시에서 ZPage를 제거할 시기를 결정하는 데 있습니다.
단순하게는 일정 시간이 지나면 제거되도록 설정할 수 있고, 실제로 이 방식은 Shenandoah GC에서, 기본값 5분으로 사용하고 있습니다. ZGC도 -XX:ZUncommitDelay=<seconds>(default 300초) 으로 간단한 시간 정책을 제공할 수 있습니다. 이러한 방식 외에도, 새 옵션을 추가하지 않고 GC가 일어나는 빈도에 기초하여 메모리 해제 주기를 설정할 수도 있습니다.
JEP353: Reimplement the Legacy Socket API
java.net.Socket 및 java.net.ServerSocket API의 기본 구현을 유지보수와 디버그하기 쉬운 형태로 리팩토링하는 JEP입니다.
Motivation
java.net.Socket과 java.net.ServerSocket은 JDK 1.0에서 처음 등장하였는데요. 유지보수와 디버깅이 어려운 레거시 Java 및 C 코드의 혼합 형태로 구현되었습니다. 이 구현에서는 아래와 같은 몇 가지 문제가 있었습니다.
- 스레드 스택을 I/O 버퍼로 사용하여 디폴트 스레드 스택 크기를 몇 번이고 늘려야하는 하는 문제.
- 네이티브 자료구조를 사용해 구현한 비동기 close에는 수년 동안 미묘한 안정성/이식성 문제가 존재.
- 구현에는 여러 가지의 동시성 문제가 있으며 이를 해결하기 위해서는 정밀 검사가 필요.
- 네이티브메소드에서 스레드를 Blocking 하는 대신, park 하는 미래의 환경에서는 현재 구현이 목적에 맞지 않음.
Goals
따라서, java.net.Socket 및 java.net.ServerSocket API는 모든 조작을 SPI(Service Provider Interface) 메커니즘인 java.net.SocketImpl에 위임하는데요. 내장 구현을 "plain" 구현이라고 하며, SocketInputStream 및 SocketOutputStream 클래스를 지원하는 비공개 PlainSocketImpl에 의해 구현됩니다.
아래는 새로운 구현에 대한 내용입니다.
- SocketImpl은 레거시 SPI 메커니즘이며, 새로운 구현에서 해당하지 않은 경우에는 이전 구현을 모방하여 호환되도록 동작합니다.
- Timeout(connect, accept, read)을 사용하는 소켓 연산자를, non-blocking 모드로 변경하고 소켓을 polling 하여 구현합니다.
- SocketImpl이 GC되고 Socket이 명시적으로 닫히지 않은 경우, java.lang.ref.Cleaner 메커니즘이 사용됩니다.
- 연결 재설정 처리는 이전구현과 동일하게 처리됩니다. (연결 재설정 이후 읽기 시도가 실패)
그리고 아래는 이전 구현과 다르게 동작하는 상황에 해당하는 내용입니다. 처음 두 개를 제외한 나머지들은 -Djdk.net.usePlainSocketImpl 옵션으로 완화할 수 있습니다.
- PlainSocketImpl.getINputStream()으로 반환된 InputStream과 OutputStream이 각각 java.io.FileInputStream.과 java.io.FileOutputStream을 extend합니다.
- 사용자 정의 SocketImpl을 사용하는 ServerSocket은 플랫폼 SocketImpl과 함께 반환되는 Socket에 연결할 수 없습니다. (반대도 동일)
- InputStream 및 OutputStream은 이전 구현에서 EOF(End Of File)에 대한 테스트를 먼저 수행하여 -1를 반환하지만, 신규 구현에서는 Null 및 범위 검사를 먼저 수행합니다. 검사 순서가 변경됨에 따라 코드가 깨질 가능성이 있습니다.
- 수신 큐에서 읽지 않은 바이트로 소켓을 닫으면 기본 소켓이 정상적으로 종료됩니다. Windows 이외의 플랫폼에서는 중단/강제종료로 이어집니다.
- Oracle Solaris 특정: 네트워크 오류가 발생하여 setsocketopt 또는 ioctl 호출이 실패하는 경우, 연결 재설정으로 인해 읽기 시도가 실패하지만, 이러한 방식은 깨지기 쉽고 유지보수가 어렵기 때문에 신규 구현에서는 이 동작을 따라 하지 않았습니다.
- Oracle Solaris 특정: TCP 소켓 연결 이후에 IPV6_TLCASS 소켓 옵션을 변경할 수 없게 되었습니다. 이전 구현은 setTrafficClass 함수에 지정된 값을 캐시하여 마스킹하였습니다.
- 예외 시 SocketException을 발생시키지만, 신규 구현에서는 일부 같은 예외가 발생하지 않을 수 있습니다. 또한, 예외 메시지가 다른 경우도 존재합니다. 예) Windows에서 SocketException시, 이전 구현은 오류 코드를 영어 전용 메시지를 사용하지만, 신규 구현은 시스템 메시지를 사용합니다.
- 특정 작업을 실행할 때 성능이 다를 수 있습니다. 이전 구현은 ServerSocket에서 accept 메소드를 호출하는 여러 스레드가 커널에서 대기하지만, 신규 구현은 하나의 스레드가 accept를 호출하기 위해 대기하고 나머지 스레드들은 큐에서 lock을 얻기 위해 대기합니다.
SPI(Service Provider Interface): 플러그인 형태로 제공하는 인터페이스. 인터페이스만 정의하고 각 구현은 가져다 사용하는 벤더에서 구현하도록 함. 예) Java Cryptography Extension
JEP354: Switch Expressions (Second Preview)
JDK 12에서 preview로 언급되었던 Switch Expressions의 second preview입니다. 내용은 JEP325와 동일하고 break 키워드가 yield 키워드로 변경되었습니다.
int result = switch (s) {
case "Foo":
yield 1;
case "Bar":
yield 2;
default:
System.out.println("Neither Foo nor Bar, hmmm...");
yield 0;
};
python을 사용하신 분들이라면 익숙한 키워드이실 텐데요. 동작은 비슷합니다. switch 표현식 내부에서 생성된 특정 값을 반환하는데요. JDK12에서 나왔던 break value; 표현식이 yield value;로 대체되었습니다. return value;가 제어권이 함수 호출자나 생성자에게 있었다면, yield value;는 스위치 표현식에게 제어권을 전달합니다.
JEP325에서 제안된 화살표 방식은 단일 표현식을 나타낼 때는 유효하게 동작합니다.
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> { //블록이나 기존의 switch-case문에서 yield를 사용
int k = day.toString().length();
int result = f(k);
yield result;
}
};
JEP355: Text Blocks (Preview)
JDK15에 제안될 JEP378의 preview입니다. 다른 언어에서도 등장하는 Text block이 Java에도 추가됩니다. 기존에 여러 줄의 텍스트를 사용할 때, +와 new line으로 연결해주었다면 이 제안에서는 """을 통해 multi line으로 텍스트를 입력할 수 있도록 제안하고 있습니다.
HTML example (as-is)
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
HTML example (to-be)
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
SQL example (as-is)
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
"WHERE `CITY` = 'INDIANAPOLIS'\n" +
"ORDER BY `EMP_ID`, `LAST_NAME`;\n";
SQL example (to-be)
String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
""";
Polyglot language example (as-is)
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
" print('\"Hello, world\"');\n" +
"}\n" +
"\n" +
"hello();\n");
Polyglot language example (to-be)
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
function hello() {
print('"Hello, world"');
}
hello();
""");
이 제안은 Java string의 가독성을 향상하고 이스케이프 문자열 사용을 피하고자 만들어졌습니다. 특수문자 이스케이프 방식을 기존의 문자열 사용법과 동일하고 필요한 경우, 아래 String 관련 함수들을 사용할 수 있습니다.
- String::stripIndent()
- String::translateEscapes()
- String::formatted(Object... args)
이전 JDK 변경사항은 아래에서 확인하실 수 있습니다.