수백만 개의 작은 임시 개체를 만드는 모범 사례


109

수백만 개의 작은 개체를 만들고 릴리스하기위한 "모범 사례"는 무엇입니까?

Java로 체스 프로그램을 작성하고 있으며 검색 알고리즘은 가능한 각 이동에 대해 단일 "Move"개체를 생성하며 명목 검색은 초당 백만 개 이상의 이동 개체를 쉽게 생성 할 수 있습니다. JVM GC는 내 개발 시스템의로드를 처리 할 수 ​​있었지만 다음과 같은 대체 접근 방식을 탐색하는 데 관심이 있습니다.

  1. 가비지 수집의 오버 헤드를 최소화하고
  2. 저가형 시스템의 최대 메모리 사용량을 줄입니다.

대부분의 개체는 수명이 매우 짧지 만 생성 된 이동의 약 1 %가 지속되고 지속 된 값으로 반환되므로 풀링 또는 캐싱 기술은 특정 개체를 재사용에서 제외 할 수있는 기능을 제공해야합니다. .

완전한 예제 코드를 기대하지는 않지만 추가 읽기 / 연구 또는 유사한 성격의 오픈 소스 예제에 대한 제안에 감사드립니다.


11
플라이 웨이트 패턴이 귀하의 경우에 적합합니까? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland

4
객체에 캡슐화해야합니까?
nhahtdh

1
개체가 중요한 공통 데이터를 공유하지 않기 때문에 Flyweight 패턴은 적절하지 않습니다. 개체에 데이터를 캡슐화하는 경우 너무 커서 기본 형식으로 압축 할 수 없기 때문에 POJO의 대안을 찾고 있습니다.
겸손한 프로그래머

2

답변:


47

자세한 가비지 콜렉션으로 애플리케이션을 실행하십시오.

java -verbose:gc

그리고 그것이 수집되면 알려줄 것입니다. 빠른 스위프와 전체 스위프의 두 가지 유형이 있습니다.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

화살표는 크기 전후입니다.

전체 GC가 아닌 GC를 수행하는 한 집에 안전하게 있습니다. 일반 GC는 '젊은 세대'의 복사 수집기이므로 더 이상 참조되지 않는 객체는 단순히 잊혀지는 것입니다.

Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning을 읽는 것이 도움이 될 것입니다.


전체 가비지 콜렉션이 드문 지점을 찾으려면 Java 힙 크기를 실험하십시오. Java 7에서 새로운 G1 GC는 어떤 경우에는 더 빠르며 다른 경우에는 더 느립니다.
Michael Shopsin 2013 년

21

버전 6부터 JVM의 서버 모드는 이스케이프 분석 기술을 사용합니다. 그것을 사용하면 GC를 모두 피할 수 있습니다.


1
이스케이프 분석은 종종 실망 스럽습니다. JVM이 당신이 무엇을하고 있는지 알아 내었는지 확인하는 것이 좋습니다.
Nitsan Wakart 2013 년

2
이 옵션을 사용한 경험이있는 경우 : -XX : + PrintEscapeAnalysis 및 -XX : + PrintEliminateAllocations. 공유하면 좋을 것입니다. 아니기 때문에 솔직히 말해요.
Mikhail

stackoverflow.com/questions/9032519/ 를 참조하십시오. JDK 7에 대한 디버그 빌드를 가져와야합니다. 저는 그렇게하지 않았지만 JDK 6에서는 성공적이었습니다.
Nitsan Wakart 2013 년

19

글쎄, 여기에 몇 가지 질문이 있습니다!

1-수명이 짧은 개체는 어떻게 관리됩니까?

앞서 언급했듯이 JVM은 약한 세대 별 가설을 따르기 때문에 엄청난 양의 단기 객체를 완벽하게 처리 할 수 ​​있습니다 .

주 메모리 (힙)에 도달 한 개체에 대해 이야기하고 있습니다. 항상 그런 것은 아닙니다. 생성 한 많은 객체는 CPU 레지스터를 남기지 않습니다. 예를 들어,이 for 루프를 고려하십시오.

for(int i=0, i<max, i++) {
  // stuff that implies i
}

루프 언 롤링 (JVM이 코드에서 많이 수행하는 최적화)에 대해 생각하지 마십시오. 경우 maxIS가 동일 Integer.MAX_VALUE하면 루프가 실행하는 데 시간이 걸릴 수 있습니다. 그러나 i변수는 루프 블록을 벗어나지 않습니다. 따라서 JVM은 해당 변수를 CPU 레지스터에 넣고 정기적으로 증분하지만 주 메모리로 다시 보내지 않습니다.

따라서 수백만 개의 개체를 만드는 것은 로컬에서만 사용되는 경우 큰 문제가 아닙니다. 그들은 Eden에 저장되기 전에 죽을 것이므로 GC는 그들을 알아 채지 못할 것입니다.

2-GC의 오버 헤드를 줄이는 것이 유용합니까?

평소처럼 상황에 따라 다릅니다.

먼저, 무슨 일이 일어나고 있는지 명확하게 볼 수 있도록 GC 로깅을 활성화해야합니다. 다음으로 활성화 할 수 있습니다.-Xloggc:gc.log -XX:+PrintGCDetails .

애플리케이션이 GC주기에서 많은 시간을 소비하는 경우, 예, GC를 조정하십시오. 그렇지 않으면 실제로 가치가 없을 수 있습니다.

예를 들어, 100ms마다 10ms가 걸리는 젊은 GC가있는 경우 GC에서 시간의 10 %를 소비하고 초당 10 개의 컬렉션이 있습니다 (이는 huuuuuge). 이 경우 10 GC / s가 여전히 존재하므로 GC 튜닝에 시간을 소비하지 않습니다.

3-약간의 경험

엄청난 양의 주어진 클래스를 생성하는 응용 프로그램에서 비슷한 문제가 발생했습니다. GC 로그에서 응용 프로그램의 생성 속도가 약 3GB / s라는 것을 알았습니다. 이것은 너무 많은 것입니다 (초당 3GB의 데이터?!).

문제 : 너무 많은 객체가 생성되어 너무 자주 GC가 발생합니다.

필자의 경우 메모리 프로파일 러를 연결하고 클래스가 모든 개체의 막대한 비율을 나타내는 것을 발견했습니다. 인스턴스화를 추적하여이 클래스가 기본적으로 개체에 래핑 된 한 쌍의 부울이라는 것을 알아 냈습니다. 이 경우 두 가지 솔루션을 사용할 수 있습니다.

  • 한 쌍의 부울을 반환하지 않도록 알고리즘을 재 작업하지만 대신 각 부울을 개별적으로 반환하는 두 가지 메서드가 있습니다.

  • 4 개의 다른 인스턴스 만 있다는 것을 알고 객체를 캐시

두 번째는 애플리케이션에 미치는 영향이 가장 적고 도입하기 쉽기 때문에 선택했습니다. 스레드로부터 안전하지 않은 캐시가있는 팩토리를 만드는 데 몇 분이 걸렸습니다.

할당 속도는 1GB / s로 떨어졌고, 젊은 GC의 빈도도 감소했습니다 (3으로 나눈 값).

도움이 되길 바랍니다!


11

가치 객체 (즉, 다른 객체에 대한 참조가 없음) 만 있고 실제로는 그 수가 엄청나게 많은 경우 ByteBuffers기본 바이트 순서로 직접 사용할 수 있으며 [후자는 중요합니다] 몇 백 줄의 할당 / 재사용 코드 + getter / setter. 게터는 다음과 비슷합니다.long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

이렇게하면 한 번만 할당하는 한, 즉 거대한 덩어리를 직접 할당 한 다음 객체를 직접 관리하는 한 거의 전적으로 GC 문제를 해결할 수 있습니다. 참조 대신 색인 만있을 것입니다 (즉,int )ByteBuffer 있습니다. 메모리를 직접 정렬해야 할 수도 있습니다.

이 기술은 C and void* 은를 지만 일부 포장을 사용하면 견딜 수 있습니다. 성능 저하는 컴파일러가이를 제거하지 못하는지 확인하는 경계 일 수 있습니다. 주요 장점은 벡터와 같은 튜플을 처리하는 경우 지역성입니다. 객체 헤더가 없으면 메모리 사용량도 줄어 듭니다.

그 외에는 거의 모든 JVM의 젊은 세대가 사소하게 죽고 할당 비용이 포인터 범프에 불과하므로 그러한 접근 방식이 필요하지 않을 것입니다. final일부 플랫폼 (즉, ARM / Power)에서 메모리 울타리가 필요하므로 필드 를 사용하는 경우 할당 비용이 약간 더 높을 수 있지만 x86에서는 무료입니다.


8

GC가 문제라고 가정하면 (다른 사람들이 그렇지 않을 수도 있다고 지적했듯이) 특별한 경우, 즉 막대한 변동을 겪는 클래스에 대해 자체 메모리 관리를 구현할 것입니다. 개체 풀링을 시도해보십시오. 저는 그것이 아주 잘 작동하는 경우를 보았습니다. 개체 풀을 구현하는 것은 꼼꼼한 방법이므로 여기를 다시 방문 할 필요가 없습니다.

  • 다중 스레딩 : 스레드 로컬 풀을 사용하면 귀하의 경우에 효과적 일 수 있습니다.
  • 백업 데이터 구조 : 제거시 잘 수행되고 할당 오버 헤드가 없으므로 ArrayDeque 사용을 고려하십시오.
  • 수영장 크기 제한 :)

등 전후 측정


6

비슷한 문제를 만났습니다. 우선 작은 물체의 크기를 줄이십시오. 각 개체 인스턴스에서이를 참조하는 몇 가지 기본 필드 값을 도입했습니다.

예를 들어 MouseEvent에는 Point 클래스에 대한 참조가 있습니다. 새 인스턴스를 만드는 대신 포인트를 캐시하고 참조했습니다. 예를 들어 빈 문자열에 대해서도 동일합니다.

또 다른 소스는 하나의 int로 대체 된 여러 부울이며 각 부울에 대해 int의 1 바이트 만 사용합니다.


그냥 흥미로 웠습니다. 그것이 당신에게 현명한 성과를 가져다 준 것은 무엇입니까? 변경 전후에 애플리케이션을 프로파일 링했습니까? 그렇다면 결과는 어떠 했습니까?
Axel 2013 년

@Axel 객체는 훨씬 적은 메모리를 사용하므로 GC가 자주 호출되지 않습니다. 확실히 우리는 앱을 프로파일 링했지만 개선 된 속도의 시각적 효과도있었습니다.
StanislavL

6

나는 얼마 전에 XML 처리 코드로이 시나리오를 다루었 다. 매우 작고 (일반적으로 문자열) 매우 짧고 ( XPath 실패) 수백만 개의 XML 태그 객체를 생성하는 것을 발견했습니다. 검사 일치하지 않음을 의미하므로 폐기를 의미 함 .

몇 가지 심각한 테스트를 수행 한 결과 새 태그를 만드는 대신 폐기 된 태그 목록을 사용하여 속도를 약 7 % 향상시킬 수 있다는 결론에 도달했습니다. 그러나 일단 구현되면 free queue가 너무 커지면 정리하기 위해 추가 된 메커니즘이 필요하다는 것을 발견했습니다. 이로 인해 내 최적화가 완전히 무효화되어 옵션으로 전환했습니다.

요약하면-아마 그럴 가치가 없지만-당신이 그것에 대해 생각하고 있다는 것을 알게되어 기쁩니다.


2

체스 프로그램을 작성하고 있다는 점을 감안할 때 괜찮은 성능을 위해 사용할 수있는 몇 가지 특별한 기술이 있습니다. 한 가지 간단한 방법은 long (또는 바이트)의 큰 배열을 만들고이를 스택으로 처리하는 것입니다. 무브 제너레이터가 무브를 생성 할 때마다 스택에 몇 개의 숫자를 밀어 넣습니다. 예를 들어 정사각형에서 정사각형으로 이동합니다. 검색 트리를 평가할 때 이동을 시작하고 보드 표현을 업데이트합니다.

표현력을 원한다면 물건을 사용하십시오. 속도를 원한다면 (이 경우) 기본으로 이동하십시오.


1

이러한 검색 알고리즘에 사용한 한 가지 솔루션은 Move 개체를 하나만 만들고 새 이동으로 변경 한 다음 범위를 떠나기 전에 이동을 취소하는 것입니다. 한 번에 하나의 동작 만 분석 한 다음 가장 좋은 동작을 어딘가에 저장하고있을 것입니다.

어떤 이유로 든 가능하지 않고 최대 메모리 사용량을 줄이려면 메모리 효율성에 대한 좋은 기사가 있습니다. http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf


죽은 링크. 그 기사에 대한 다른 출처가 있습니까?
dnault

0

수백만 개의 개체를 만들고 적절한 방식으로 코드를 작성하기 만하면됩니다. 이러한 개체에 대한 불필요한 참조를 유지하지 마십시오. GC는 당신을 위해 더러운 일을 할 것입니다. 언급 한대로 verbose GC를 가지고 놀아서 실제로 GC인지 확인할 수 있습니다. Java는 개체를 만들고 해제하는 것입니다. :)


1
죄송합니다 친구, 나는 당신의 접근 방식에 동의하지 않습니다 ... Java는 다른 프로그래밍 언어와 마찬가지로 제약 조건 내에서 문제를 해결하는 것입니다. OP가 GC에 의해 제한되는 경우 어떻게 그를 돕고 있습니까?
Nitsan Wakart 2013 년

1
Java가 실제로 어떻게 작동하는지 그에게 말하고 있습니다. 그가 수백만 개의 임시 객체를 갖는 상황을 피할 수 없다면, 최선의 조언은 임시 클래스가 가벼워 야하고 더 이상 한 단계가 아니라 가능한 한 빨리 참조를 릴리스하도록해야한다는 것입니다. 내가 뭔가를 놓치고 있습니까?
gyorgyabraham

자바는 쓰레기 생성을 지원하고 당신을 위해 그것을 정리할 것입니다. OP가 객체 생성을 피할 수없고 GC에서 보낸 시간에 불만이 있다면 그것은 슬픈 결말입니다. 내 이의는 GC를 위해 더 많은 작업을 수행하도록 권장하는 것입니다. 왜냐하면 그것이 어떻게 든 적절한 Java이기 때문입니다.
Nitsan Wakart 2013 년

0

Java의 스택 할당 및 이스케이프 분석에 대해 읽어야한다고 생각합니다.

이 주제를 더 자세히 살펴보면 객체가 힙에 할당되지 않았고 힙의 객체와 같은 방식으로 GC에 의해 수집되지 않는다는 것을 알 수 있습니다.

이스케이프 분석에 대한 위키피디아 설명과 Java에서 작동하는 방법에 대한 예가 있습니다.

http://en.wikipedia.org/wiki/Escape_analysis


0

나는 GC의 열렬한 팬이 아니기 때문에 항상 주위 방법을 찾으려고 노력합니다. 이 경우 Object Pool 패턴을 사용하는 것이 좋습니다. .

아이디어는 나중에 재사용 할 수 있도록 스택에 저장하여 새 객체를 생성하지 않는 것입니다.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

3
작은 개체에 풀을 사용하는 것은 매우 나쁜 생각입니다. 부팅하려면 스레드 당 풀이 필요합니다 (또는 공유 액세스로 인해 성능이 저하됨). 이러한 풀은 또한 좋은 가비지 수집기보다 성능이 떨어집니다. 마지막으로, GC는 동시 코드 / 구조를 처리 할 때 신의 선물입니다. ABA 문제가 자연스럽게 발생하지 않으므로 많은 알고리즘을 구현하기가 훨씬 쉽습니다. Ref. 동시 환경에서 계산하려면 최소한 원자 적 작업 + 메모리 펜스 (x86의 LOCK ADD 또는 CAS)가 필요합니다.
bestsss

1
풀의 개체 관리 는 가비지 수집기를 실행하는 것 보다 비용이 많이들 수 있습니다 .
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen 일반적으로 동의하지만 그러한 차이를 감지하는 것은 상당히 어려운 일이며 GC가 귀하의 경우에 더 잘 작동한다는 결론에 도달했을 때 그러한 차이가 중요하다면 매우 독특한 경우 여야합니다. 반대로, 개체 풀이 앱을 저장할 수 있습니다.
Ilya Gazman 2016

1
나는 단순히 당신의 주장을 이해하지 못합니까? GC가 객체 풀링보다 빠른지 감지하기가 매우 어렵습니까? 따라서 개체 풀링을 사용해야합니까? JVM은 깨끗한 코딩과 수명이 짧은 객체에 최적화되어 있습니다. 이것이이 질문에 관한 것이라면 (OP가 초당 백만 개를 생성한다면 희망합니다) 제안한 것처럼 더 복잡하고 오류가 발생하기 쉬운 계획으로 전환하는 것이 입증 가능한 이점이 있어야합니다. 이것이 증명하기가 너무 어렵다면 왜 귀찮게 하는가.
Thorbjørn Ravn Andersen

0

개체 풀은 힙에 대한 개체 할당에 비해 엄청난 (때로는 10 배) 향상을 제공합니다. 그러나 연결된 목록을 사용한 위의 구현은 순진하고 잘못되었습니다! 연결 목록은 노력을 무효화하는 내부 구조를 관리하는 개체를 만듭니다. 객체 배열을 사용하는 링 버퍼가 잘 작동합니다. 예제에서 (무브를 관리하는 체스 프로그램) Ringbuffer는 계산 된 모든 무브 목록에 대한 홀더 객체로 래핑되어야합니다. 이동 홀더 개체 참조 만 전달됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.