Java 쓰레기 수거 비용 절감에 대한 몇 가지 건의
계속해서 지연되어 발표될 Java9에 따라 G1(Garbage First) 쓰레기 수거기는 HotSpot 가상 머신의 기본 쓰레기 수거기가 됩니다.시리얼 쓰레기 수거기에서 CMS 수집기까지 JVM은 많은 GC의 실현을 보았고 G1은 차세대 쓰레기 수거기가 될 것이다.
쓰레기 수집기의 발전에 따라 모든 GC는 이전 세대에 비해 커다란 발전과 개선을 가져왔다.parallelGC는serialGC에 비해 쓰레기 수집기를 다선정 방식으로 작동하게 하여 다핵컴퓨터의 계산 능력을 충분히 이용하였다.CMS("Concurrent Mark-Sweep") 수집기는parallel GC에 비해 회수 과정을 여러 단계로 나누어 응용 라인이 실행 중일 때 수집 작업을 병행하여 완성할 수 있고,'stop-the-world'를 빈번하게 실행하는 상황을 크게 개선했다.G1은 대량의 메모리를 가진 JVM에 대해 더 좋은 성능을 보이고 예측 가능하고 통일된 정지 과정을 가진다.
Tip #1: 컬렉션 용량 예측
모든 표준 자바 집합은 사용자 정의와 확장의 실현 (예를 들어 Trove와 Google의 Guava) 을 포함하고 밑바닥에는 수조 (원본 데이터 형식이나 대상 기반 형식) 를 사용한다.수조가 분배되면 크기가 변하지 않기 때문에 집합에 요소를 추가할 때 대부분의 경우 새로운 대용량 수조를 낡은 수조로 교체해야 한다.
집합 초기화의 크기를 제공하지 않아도 대부분의 집합의 실현은 재분배 수조의 처리를 최대한 최적화하고 비용을 최소화한다.그러나 구조 집합을 할 때 크기를 제공하면 가장 좋은 효과를 얻을 수 있다.
다음 코드를 간단한 예로 들어 살펴보겠습니다.
public static List reverse(List & lt; ? extends T & gt; list) {
List result = new ArrayList();
for (int i = list.size() - 1; i & gt; = 0; i--) {
result.add(list.get(i));
}
return result;
}
This method allocates a new array, then fills it up with items from another list, only in reverse order. 이 방법은 새로운 수조를 분배한 다음에 다른list의 원소로 이 수조를 채운다. 단지 원소의 수순에 변화가 생겼을 뿐이다.이 처리 방식은 엄청난 성능 대가를 치르게 될 것입니다. 최적화된 점은 요소를 새로운list에 추가하는 코드입니다.매번 원소를 추가할 때마다list는 새로운 원소를 수용할 수 있는 충분한 위치를 확보해야 한다.만약 빈 위치가 있다면, 간단하게 새로운 요소를 다음 빈 슬롯에 저장할 뿐이다.없으면 새로운 밑바닥 그룹을 분배하고 낡은 그룹의 내용을 새 그룹에 복사한 다음 새로운 요소를 추가합니다.이것은 여러 차례 수조를 분배하게 될 것이며, 나머지 낡은 수조는 결국 GC에 의해 회수될 것이다.
우리는 집합을 구성할 때 그 밑바닥의 수조를 통해 그것이 얼마나 많은 원소를 저장하는지 알게 함으로써 이러한 불필요한 분배를 피할 수 있다
public static List reverse(List & lt; ? extends T & gt; list) {
List result = new ArrayList(list.size());
for (int i = list.size() - 1; i & gt; = 0; i--) {
result.add(list.get(i));
}
return result;
}
위의 코드는 ArrayList의 구조기를 통해 충분한 공간을 지정하여 list.size()
개의 요소를 저장하고 초기화할 때 분배의 실행을 완성합니다. 이것은 List가 교체되는 과정에서 메모리를 다시 분배할 필요가 없다는 것을 의미합니다.Guava의 집합 클래스는 더욱 진일보하여 집합을 초기화할 때 기대 요소의 개수를 명확하게 지정하거나 예측 값을 지정할 수 있습니다.
List result = Lists.newArrayListWithCapacity(list.size());
List result = Lists.newArrayListWithExpectedSize(list.size());
위의 코드에서 전자는 집합이 얼마나 많은 요소를 저장할지 정확하게 알고 있으며, 후자의 분배 방식은 잘못된 예측 상황을 고려하는 데 사용된다.Tip #2: 데이터 흐름 직접 처리
데이터 흐름을 처리할 때, 예를 들어 한 파일에서 데이터를 읽거나 네트워크에서 데이터를 다운로드할 때, 아래의 코드는 매우 흔히 볼 수 있다.
byte[] fileData = readFileToByteArray(new File("myfile.txt"));
생성된 바이트 배열은 XML 문서, JSON 객체 또는 프로토콜 버퍼 메시지, 그리고 흔히 볼 수 있는 옵션으로 해석될 수 있습니다.큰 파일이나 파일의 크기를 예측할 수 없을 때, 위의 방법은 매우 현명하지 않다. 왜냐하면 JVM이 실제 파일을 처리하기 위해 버퍼를 분배할 수 없을 때, Out OfMemeory Errors를 초래하기 때문이다.
설령 데이터의 크기가 관리할 수 있다 하더라도, 쓰레기를 회수할 때, 위의 모델을 사용하면 여전히 큰 비용을 초래할 것이다. 왜냐하면 그것은 더미 속에 매우 큰 구역을 분배하여 파일 데이터를 저장하기 때문이다.
더 좋은 처리 방식은 적당한 InputStream (예를 들어 이 예에서 File InputStream) 을 사용하여 해상도에 직접 전달하고 전체 파일을 한 바이트 그룹에 한꺼번에 읽지 않는 것이다.모든 메인스트림의 소스 오픈 라이브러리는 입력 흐름을 직접 받아들여 처리하기 위해 상응하는 API를 제공한다. 예를 들어 다음과 같다.
FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);
Tip #3: 변경할 수 없는 객체 사용불변성은 좋은 점이 너무 많다.내가 군더더기 얘기도 안 해도 돼.그러나 쓰레기 수거에 영향을 미칠 수 있는 장점이 있으니 주목해야 한다.
가변되지 않는 대상의 속성은 대상이 만들어진 후에 수정될 수 없다. 예를 들어 다음과 같다.
public class ObjectPair {
private final Object first;
private final Object second;
public ObjectPair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
위의 클래스를 실례화하면 변하지 않는 대상이 생길 수 있습니다. 모든 속성을final로 수식하고 구조가 완성되면 바꿀 수 없습니다.불가변성은 하나의 불가변 용기에 인용된 모든 대상을 의미하며, 용기 구조가 완성되기 전에 대상은 이미 생성되었다.GC의 경우 이 용기의 젊음은 적어도 자신이 가지고 있는 최연소 인용과 같다.이는 젊은 세대가 쓰레기 수거를 수행하는 과정에서 GC가 변하지 않는 대상이 낡은 시대에 있기 때문에 그들을 뛰어넘고, 이러한 변하지 않는 대상이 낡은 시대에 어떤 대상에도 인용되지 않는다는 것을 확정할 때까지 그것들에 대한 회수를 완성한다는 것을 의미한다.
더 적은 스캔 대상은 메모리 페이지에 대한 더 적은 스캔을 의미하고, 더 적은 스캔 메모리 페이지는 더 짧은 GC 생명주기를 의미하며, 더 짧은 GC 정지와 더 좋은 총 흡수량을 의미한다.
Tip #4: 문자열 맞춤법 주의
문자열은 모든 JVM 기반 응용 프로그램에서 가장 많이 사용되는 비원본 데이터 구조일 수 있습니다.그러나 은밀한 비용 부담과 간편한 사용으로 인해 대량의 메모리를 차지하는 죄가 화근이 되기 쉽다.
이 문제는 문자열 값이 아니라 실행할 때 메모리를 초기화하는 데 있다.동적 구성 문자열의 예를 살펴보겠습니다.
public static String toString(T[] array) {
String result = "[";
for (int i = 0; i & lt; array.length; i++) {
result += (array[i] == array ? "this" : array[i]);
if (i & lt; array.length - 1) {
result += ", ";
}
}
result += "]";
return result;
}
이것은 보기에 괜찮은 방법입니다. 문자 그룹을 받고 문자열을 되돌려줍니다.그러나 이것은 대상 메모리 분배에 재난적이다.이 문법 사탕의 배후를 똑똑히 보기는 어렵지만 배후의 실제 상황은 이렇다.
public static String toString(T[] array) {
String result = "[";
for (int i = 0; i & lt; array.length; i++) {
StringBuilder sb1 = new StringBuilder(result);
sb1.append(array[i] == array ? "this" : array[i]);
result = sb1.toString();
if (i & lt; array.length - 1) {
StringBuilder sb2 = new StringBuilder(result);
sb2.append(", ");
result = sb2.toString();
}
}
StringBuilder sb3 = new StringBuilder(result);
sb3.append("]");
result = sb3.toString();
return result;
}
문자열은 변할 수 없다. 이것은 한 번의 연결이 발생할 때마다 그 자체가 수정되지 않고 순서대로 새로운 문자열을 분배한다는 것을 의미한다.또한 컴파일러는 표준 StringBuilder 클래스를 사용하여 이러한 결합 작업을 수행합니다.매번 교체될 때마다 임시 문자열을 은밀하게 분배하고 임시 StringBuilder 대상을 은밀하게 분배하여 최종 결과를 구축하는 데 도움을 주기 때문에 문제가 생길 수 있다.가장 좋은 방법은 위의 상황을 피하고 StringBuilder와 직접적인 추가를 사용하여 로컬 연결 문자("+")를 대체하는 것이다.다음은 예입니다.
public static String toString(T[] array) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i & lt; array.length; i++) {
sb.append(array[i] == array ? "this" : array[i]);
if (i & lt; array.length - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
여기에서 우리는 방법이 시작될 때만 유일한 StringBuilder를 분배했다.이로써 모든 문자열과list의 요소는 별도의 StringBuilder에 추가됩니다.최종적으로 toString()
방법을 사용하여 한꺼번에 문자열로 되돌려줍니다.Tip #5: 특정 원본 유형의 컬렉션 사용
Java 표준의 집합 라이브러리는 간단하고 범용을 지원하며 집합을 사용할 때 유형을 반정적으로 연결할 수 있습니다.예를 들어 문자열만 저장하는 Set이나 저장
Map<Pair, List>
같은 맵을 만들려면 이런 처리 방식이 매우 좋습니다.진정한 문제는 우리가list 메모리 int 형식이나map 메모리 더블 형식을value로 사용하려는 데서 비롯된다.범주형은 원본 데이터 형식을 지원하지 않기 때문에 다른 선택은 포장 형식을 사용하여 교체하는 것입니다. 여기서 우리는 List를 사용합니다.
이러한 처리 방식은 매우 낭비적이다. 왜냐하면 하나의 Integer는 완전한 대상이고 하나의 대상의 머리는 12바이트와 그 내부의 유지보수된 int 속성을 차지하기 때문이다. 각 Integer 대상은 모두 16바이트를 차지한다.이것은 같은 개수를 저장하는 int 형식의list에 비해 소모되는 공간이 4배입니다!이보다 더 심각한 문제는 사실상 Integer가 진정한 대상 실례이기 때문에 쓰레기 수집 단계에서 쓰레기 수집기에 의해 회수 여부를 고려해야 한다는 점이다.
이 문제를 처리하기 위해 우리는 Takipi에서 아주 좋은 Trove 집합 라이브러리를 사용한다.Trove는 메모리를 더 효율적으로 사용할 수 있는 원본 유형의 집합을 지원하기 위해 일부 범용적인 특정을 배제했다.예를 들어 우리는 매우 소모적인 성능
Map<Integer, Double>
을 사용하는데 Trove에서 또 다른 특별한 선택 방안이 있는데 그 형식은 TIntDoubleMap이다
TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);
...
Trove의 밑바닥은 원본 형식의 수조를 사용하기 때문에 집합을 조작할 때 원소의 포장(int->Integer
이나 해체(Integer->int
가 발생하지 않습니다. 저장 대상이 없습니다. 밑바닥은 원본 데이터 형식의 저장을 사용하기 때문입니다.마지막
쓰레기 수집기가 지속적으로 개선되고 운행할 때의 최적화와 JIT 컴파일러도 점점 스마트해진다.우리는 개발자로서 GC의 우호적인 코드를 어떻게 작성할지 갈수록 적게 고려할 것이다.그러나 현재 단계에서 G1이 어떻게 개선되든지 간에 우리는 여전히 JVM의 성능을 향상시키는 데 도움을 줄 수 있는 많은 일을 하고 있다.
자, 이상은 이 글의 전체 내용입니다. 본고의 내용이 여러분의 학습이나 자바 사용에 어느 정도 도움이 되기를 바랍니다. 의문이 있으면 댓글을 남겨 주십시오.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
38. Java의 Leetcode 솔루션텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.