문자열은 모든 애플리케이션에서 많은 메모리를 차지합니다. 특히, 개별 UTF-16 문자를 포함하는 char[] 배열은 JVM 메모리 소비에 가장 큰 영향을 미칩니다. 각 문자가 2비트를 차지하기 때문입니다.
문자열 중복 제거는 문자열 내부가 실제로는 문자 배열이고 최종적이므로 JVM이 이를 임의로 조작할 수 있다는 점을 활용합니다.
문자열 중복 제거를 위해 개발자는 많은 전략을 고려했지만 최종 구현에서는 다음 접근 방식을 채택했습니다.
가비지 수집기가 String 개체에 액세스할 때마다 char 배열이 표시됩니다. char 배열의 해시 값을 가져와 배열에 대한 약한 참조와 함께 저장합니다. 가비지 수집기가 char 배열과 동일한 해시 코드를 가진 다른 문자열을 찾는 한 두 문자열은 문자별로 비교됩니다.
일치하는 경우 한 문자열이 두 번째 문자열의 char 배열을 가리키도록 수정됩니다. 첫 번째 char 배열은 더 이상 참조되지 않으며 재활용될 수 있습니다.
물론 이 전체 과정에는 약간의 오버헤드가 발생하지만 매우 엄격한 상한선으로 제어됩니다. 예를 들어, 반복되는 문자가 발견되지 않으면 일정 기간 동안 더 이상 확인되지 않습니다.
그렇다면 이 기능은 실제로 어떻게 작동하나요? 먼저, 방금 출시된 Java 8 업데이트 20이 필요하고 다음 구성을 따릅니다. -Xmx256m -XX:+UseG1GC 다음 코드를 실행합니다.
이 코드는 30번의 반복을 실행하고 OutOfMemoryError를 보고합니다. .public class LotsOfStrings { private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>(); public static void main(String[] args) throws Exception { int iteration = 0; while (true) { for (int i = 0; i < 100; i++) { for (int j = 0; j < 1000; j++) { LOTS_OF_STRINGS.add(new String("String " + j)); } } iteration++; System.out.println("Survived Iteration: " + iteration); Thread.sleep(100); } } }
이제 문자열 중복 제거를 활성화하고 다음 구성을 사용하여 위 코드를 실행합니다.
-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
이 시점에서는 더 오래 실행되고 50회 반복 후에 종료될 수 있습니다.
이제 JVM이 수행한 작업도 인쇄합니다.
편의상 모든 데이터의 합계를 직접 계산할 필요가 없습니다. 총 사용하면 됩니다.[GC concurrent-string-deduplication, 4658.2K->0.0B(4658.2K), avg 99.6%, 0.0165023 secs] [Last Exec: 0.0165023 secs, Idle: 0.0953764 secs, Blocked: 0/0.0000000 secs] [Inspected: 119538] [Skipped: 0( 0.0%)] [Hashed: 119538(100.0%)] [Known: 0( 0.0%)] [New: 119538(100.0%) 4658.2K] [Deduplicated: 119538(100.0%) 4658.2K(100.0%)] [Young: 372( 0.3%) 14.5K( 0.3%)] [Old: 119166( 99.7%) 4643.8K( 99.7%)] [Total Exec: 4/0.0802259 secs, Idle: 4/0.6491928 secs, Blocked: 0/0.0000000 secs] [Inspected: 557503] [Skipped: 0( 0.0%)] [Hashed: 556191( 99.8%)] [Known: 903( 0.2%)] [New: 556600( 99.8%) 21.2M] [Deduplicated: 554727( 99.7%) 21.1M( 99.6%)] [Young: 1101( 0.2%) 43.0K( 0.2%)] [Old: 553626( 99.8%) 21.1M( 99.8%)] [Table] [Memory Usage: 81.1K] [Size: 2048, Min: 1024, Max: 16777216] [Entries: 2776, Load: 135.5%, Cached: 0, Added: 2776, Removed: 0] [Resize Count: 1, Shrink Threshold: 1365(66.7%), Grow Threshold: 4096(200.0%)] [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0] [Age Threshold: 3] [Queue] [Dropped: 0]
위 코드는 문자열 중복 제거를 수행한다고 명시하고 있으며, 약 120,000개의 문자열을 보는 데 16ms가 걸렸습니다.
위 기능은 최근 출시된 기능이므로 완전히 검토되지 않았을 수 있습니다. 실제 애플리케이션에서는 정확한 데이터가 다르게 보일 수 있습니다. 특히 문자열이 여러 번 사용되고 전달되는 애플리케이션에서는 일부 문자열을 건너뛰거나 이미 해시코드가 있을 수 있습니다(알고 있듯이 문자열 해시 코드는 느리게 로드됩니다).
위의 경우 모든 문자열이 중복 제거되어 메모리에서 4.5MB의 데이터가 제거되었습니다.
[테이블] 섹션에는 내부 추적 테이블에 대한 통계가 제공되고, [큐] 섹션에는 로드로 인해 삭제된 중복 제거 요청 수가 나열됩니다. 이는 또한 오버헤드 감소 메커니즘의 일부입니다.
그럼 문자열 중복 제거와 문자열 지속성의 차이점은 무엇인가요? 내 블로그에는 String Interning이 메모리 효율성에 얼마나 큰 도움이 되는지에 대한 기사가 있습니다. 실제로 문자열 중복 제거와 지속성은 유사해 보이지만 지속성 메커니즘은 문자 배열뿐만 아니라 전체 문자열 인스턴스를 재사용합니다.
JDK Enhancement Proposal 192 작성자의 주장은 개발자가 상주 문자열을 어디에 두어야 할지 모르거나 적절한 위치가 프레임워크에 의해 숨겨져 있다는 것입니다. 문자열(국가 이름 등)을 복사하는 경우 문자열 중복 제거는 XML 스키마, URL 및 jar 이름과 같은 항목뿐만 아니라 동일한 JVM의 응용 프로그램 간 문자열 복사에도 적합합니다.
애플리케이션 스레드에서 문자열 상주가 발생하고 가비지 수집이 비동기식으로 동시에 처리되는 경우 문자열 중복 제거로 인해 런타임이 증가하지 않습니다. 이는 위 코드에서 Thread.sleep()을 찾은 이유이기도 합니다. 절전 모드가 없으면 GC에 너무 많은 부담이 가해져 문자열 중복 제거가 전혀 발생하지 않습니다. 그러나 이는 단지 예일 뿐이며 실제 애플리케이션에서는 문자열을 실행하는 데 몇 밀리초가 소요되는 경우가 많습니다.