팁 #1: 컬렉션 용량 예측
사용자 정의 및 확장 구현(예: Trove 및 Google Guava)을 포함한 모든 표준 Java 컬렉션은 내부적으로 배열(기본 데이터 유형 또는 객체 기반 유형)을 사용합니다. 배열이 할당되면 크기가 변경되지 않으므로 컬렉션에 요소를 추가하면 대부분의 경우 이전 배열을 대체하기 위해 새로운 대용량 배열을 다시 적용해야 합니다(에서 사용하는 배열 참조). 컬렉션의 기본 구현).
컬렉션 초기화 크기가 제공되지 않더라도 대부분의 컬렉션 구현은 배열 재할당 처리를 최적화하고 오버헤드를 최소로 상각하려고 시도합니다. 그러나 컬렉션을 구성할 때 크기를 제공하면 최상의 결과를 얻을 수 있습니다.
간단한 예로 다음 코드를 분석해 보겠습니다.
public static List reverse(List & lt; ? 확장 T & gt; 목록) {
목록 결과 = new ArrayList();
for (int i = list.size() - 1; i & gt; = 0; i--) {
result.add(list.get(i));
}
결과 반환;
}
이 메서드는 새 배열을 할당한 다음 역순으로만 변경하여 다른 목록의 항목으로 채웁니다.
이 처리 방법은 성능 비용이 많이 들 수 있으며, 최적화 지점은 새 목록에 요소를 추가하는 코드 줄입니다. 각 요소가 추가되면 목록은 기본 배열에 새 요소를 수용할 수 있는 충분한 공간이 있는지 확인해야 합니다. 빈 슬롯이 있으면 새 요소는 다음 빈 슬롯에 저장됩니다. 그렇지 않은 경우 새 기본 배열이 할당되고 이전 배열 내용이 새 배열에 복사되며 새 요소가 추가됩니다. 이로 인해 배열이 여러 번 할당되고 나머지 이전 배열은 결국 GC에 의해 회수됩니다.
컬렉션을 구성할 때 기본 배열에 저장할 요소 수를 알려줌으로써 이러한 중복 할당을 피할 수 있습니다
public static List reverse(List & lt; ? 확장 T & gt; 목록) {
목록 결과 = new ArrayList(list.size());
for (int i = list.size() - 1; i & gt; = 0; i--) {
result.add(list.get(i));
}
결과 반환;
}
위의 코드는 ArrayList의 생성자를 통해 list.size() 요소를 저장할 만큼 큰 공간을 지정하고 초기화 중에 할당을 완료합니다. 즉, List는 반복 프로세스 중에 메모리를 다시 할당할 필요가 없습니다.
Guava의 컬렉션 클래스는 한 단계 더 발전하여 컬렉션을 초기화할 때 예상 요소 수를 명시적으로 지정하거나 예측 값을 지정할 수 있습니다.
1
2목록 결과 = Lists.newArrayListWithCapacity(list.size());
목록 결과 = Lists.newArrayListWithExpectedSize(list.size());
위 코드에서 전자는 컬렉션이 저장할 요소 수를 이미 정확히 알고 있을 때 사용되는 반면, 후자는 잘못된 추정을 고려하는 방식으로 할당됩니다.
팁 #2: 데이터 스트림을 직접 처리
파일에서 데이터를 읽거나 네트워크에서 데이터를 다운로드하는 등 데이터 스트림을 처리할 때 다음 코드가 매우 일반적입니다.
1byte[] fileData = readFileToByteArray(새 파일("myfile.txt"));
결과 바이트 배열은 XML 문서, JSON 개체 또는 프로토콜 버퍼 메시지로 구문 분석될 수 있으며 몇 가지 일반적인 옵션을 사용할 수 있습니다.
JVM이 실제 파일을 처리하기 위해 버퍼를 할당할 수 없을 때 OutOfMemoryErrors가 발생하기 때문에 위의 접근 방식은 대용량 파일이나 크기를 예측할 수 없는 파일을 처리할 때 현명하지 않습니다.
데이터 크기가 관리 가능하더라도 위 패턴을 사용하면 파일 데이터를 저장하기 위해 힙에 매우 큰 영역을 할당하기 때문에 가비지 수집 시 엄청난 오버헤드가 발생합니다.
이를 처리하는 더 좋은 방법은 전체 파일을 한 번에 바이트 배열로 읽는 대신 적절한 InputStream(예: 이 예에서는 FileInputStream)을 사용하여 파서에 직접 전달하는 것입니다. 모든 주류 오픈 소스 라이브러리는 다음과 같이 처리를 위해 입력 스트림을 직접 받아들이는 해당 API를 제공합니다. FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);
팁 #3: 불변 객체를 사용하세요
불변성은 많은 이점을 가지고 있습니다. 자세히 설명할 필요도 없습니다. 그러나 살펴보아야 할 가비지 수집에 영향을 미치는 한 가지 장점이 있습니다.
불변 객체의 속성은 객체가 생성된 후에는 수정할 수 없습니다(여기의 예에서는 참조 데이터 유형의 속성을 사용합니다).
공개 클래스 ObjectPair {
비공개 최종 객체 먼저;
비공개 최종 개체 두 번째;
public ObjectPair(객체 먼저, 객체 두 번째) {
this.first = 처음;
this.second = 초;
}
공개 객체 getFirst() {
먼저 돌아가세요;
}
공개 객체 getSecond() {
두 번째로 돌아오세요;
}
}
위 클래스를 인스턴스화하면 불변 객체가 생성됩니다. 모든 속성은 final로 수정되며 생성이 완료된 후에는 변경할 수 없습니다.
불변성은 불변 컨테이너가 참조하는 모든 객체가 컨테이너가 생성되기 전에 생성된다는 것을 의미합니다. GC에 관한 한, 컨테이너는 최소한 보유하고 있는 가장 어린 참조만큼 젊습니다. 즉, Young 세대에서 가비지 수집을 수행할 때 GC는 불변 객체가 Old 세대에 있기 때문에 건너뜁니다. 이러한 불변 객체가 GC의 어떤 객체에서도 참조되지 않는다는 것이 확인될 때까지 이 객체의 수집을 완료하지 않습니다. 재활용.
스캔 개체 수가 적다는 것은 메모리 페이지 스캔 횟수가 적다는 것을 의미하며, 이는 GC 수명이 짧아지고 GC 일시 중지가 짧아지고 전체 처리량이 향상됨을 의미합니다.
팁 #4: 문자열 연결에 주의하세요
문자열은 아마도 모든 JVM 기반 애플리케이션에서 가장 일반적으로 사용되는 비원시 데이터 구조일 것입니다. 그러나 암묵적인 오버헤드와 사용 편의성으로 인해 많은 양의 메모리를 차지하는 주범이 되기 매우 쉽습니다.
문제는 분명히 문자열 리터럴이 아니라 런타임에 할당된 메모리를 초기화하는 데 있습니다. 문자열을 동적으로 작성하는 예를 간단히 살펴보겠습니다.
공개 정적 문자열 toString(T[] 배열) {
문자열 결과 = "[";
for (int i = 0; i & lt; array.length; i++) {
결과 += (배열[i] == 배열 ? "this" : 배열[i]);
if (i & lt; array.length - 1) {
결과 += ", ";
}
}
결과 += "]";
결과 반환;
}
이것은 문자 배열을 가져와서 문자열을 반환하는 좋은 방법인 것 같습니다. 그러나 이는 객체 메모리 할당에 있어 재앙입니다.
이 구문 설탕의 이면을 보기는 어렵지만, 이면의 현실은 다음과 같습니다.
공개 정적 문자열 toString(T[] 배열) {
문자열 결과 = "[";
for (int i = 0; i & lt; array.length; i++) {
StringBuilder sb1 = 새로운 StringBuilder(결과);
sb1.append(array[i] == 배열 ? "this" : 배열[i]);
결과 = sb1.toString();
if (i & lt; array.length - 1) {
StringBuilder sb2 = 새로운 StringBuilder(결과);
sb2.append(", ");
결과 = sb2.toString();
}
}
StringBuilder sb3 = 새로운 StringBuilder(결과);
sb3.append("]");
결과 = sb3.toString();
결과 반환;
}
문자열은 변경할 수 없습니다. 즉, 연결이 발생할 때마다 문자열 자체는 수정되지 않지만 새 문자열이 차례로 할당됩니다. 또한 컴파일러는 표준 StringBuilder 클래스를 사용하여 이러한 연결 작업을 수행합니다. 이는 각 반복이 최종 결과를 빌드하는 데 도움이 되는 임시 문자열과 임시 StringBuilder 개체를 암시적으로 할당하기 때문에 문제가 됩니다.
가장 좋은 방법은 위의 상황을 피하고 기본 연결 연산자("+") 대신 StringBuilder 및 직접 추가를 사용하는 것입니다. 예를 들면 다음과 같습니다.
공개 정적 문자열 toString(T[] 배열) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i & lt; array.length; i++) {
sb.append(array[i] == 배열 ? "this" : 배열[i]);
if (i & lt; array.length - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
여기서는 메서드 시작 부분에 유일한 StringBuilder를 할당합니다. 이 시점에서 모든 문자열과 목록 요소가 단일 StringBuilder에 추가되었습니다. 마지막으로 toString() 메서드를 사용하여 이를 문자열로 변환하고 한 번에 반환합니다.
팁 #5: 특정 네이티브 유형의 컬렉션을 사용하세요
Java의 표준 컬렉션 라이브러리는 간단하고 제네릭을 지원하므로 컬렉션을 사용할 때 유형의 반정적 바인딩을 허용합니다. 예를 들어, 문자열만 저장하는 Set이나 Map
실제 문제는 목록을 사용하여 int 유형을 저장하거나 맵을 사용하여 double 유형을 값으로 저장하려고 할 때 발생합니다. 제네릭은 기본 데이터 유형을 지원하지 않기 때문에 다른 옵션은 대신 래퍼 유형을 사용하는 것입니다. 여기서는 List 를 사용합니다.
Integer는 완전한 객체이기 때문에 이 처리 방법은 매우 낭비적입니다. 객체의 헤더는 12바이트를 차지하고 각 Integer 객체는 총 16바이트를 차지합니다. 이는 동일한 수의 항목을 저장하는 int 유형 목록보다 4배 많은 공간을 소비합니다! 이보다 더 심각한 문제는 Integer가 실제 객체 인스턴스이기 때문에 재활용을 위해 가비지 수집 단계에서 가비지 수집기가 이를 고려해야 한다는 사실입니다.
이를 처리하기 위해 우리는 Takipi의 멋진 Trove 컬렉션 라이브러리를 사용합니다. Trove는 메모리 효율성이 더 높은 네이티브 유형의 특수 컬렉션을 선호하여 일부 제네릭 특성을 포기했습니다. 예를 들어, 성능을 많이 소모하는 Map
TIntDoubleMap 맵 = 새로운 TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);
...
Trove의 기본 구현은 네이티브 유형의 배열을 사용하므로 컬렉션을 운영할 때 요소의 박싱(int->Integer) 또는 언박싱(Integer->int)이 발생하지 않으며 기본 구현이 다음을 사용하기 때문에 객체가 저장되지 않습니다. 기본 데이터 유형 저장소.
위 내용은 Java 가비지 수집 오버헤드를 줄이는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!