수색…


소개

이 주제에서는 Java 응용 프로그램 성능과 관련된 많은 "함정"(즉 초보 자바 프로그래머가 저지르는 실수)에 대해 설명합니다.

비고

이 주제에서는 비효율적 인 "마이크로"Java 코딩 관례에 대해 설명합니다. 대부분의 경우, 비효율은 상대적으로 적지 만 가능한 경우이를 피할 가치가 있습니다.

함정 - 로그 메시지를 만드는 오버 헤드

TRACEDEBUG 로그 수준은 런타임에 주어진 코드의 작동에 대한 높은 세부 사항을 전달할 수 있습니다. 보통 로그 레벨을이 값보다 높게 설정하는 것이 좋지만, "꺼져있는"경우에도 성능에 영향을 미치지 않도록주의해야합니다.

이 로그 문을 고려하십시오.

// Processing a request of some kind, logging the parameters
LOG.debug("Request coming from " + myInetAddress.toString() 
          + " parameters: " + Arrays.toString(veryLongParamArray));

로그 수준이 INFO 로 설정된 경우에도 debug() 전달 된 인수는 해당 줄을 실행할 때마다 평가됩니다. 이로 인해 여러 번 계산할 때 불필요하게 소모됩니다.

  • String 연결 : 여러 String 인스턴스가 생성됩니다.
  • InetAddress 는 DNS 검색을 수행 할 수도 있습니다.
  • veryLongParamArray 는 매우 길 수도 있습니다. String을 생성하면 메모리를 소비하고 시간이 걸립니다.

해결책

대부분의 로깅 프레임 워크는 수정 문자열과 객체 참조를 사용하여 로그 메시지를 작성하는 수단을 제공합니다. 로그 메시지는 메시지가 실제로 로그 된 경우에만 평가됩니다. 예:

// No toString() evaluation, no string concatenation if debug is disabled
LOG.debug("Request coming from {} parameters: {}", myInetAddress, parameters));

String.valueOf (Object)를 사용하여 모든 매개 변수를 문자열로 변환 할 수 있다면 아주 잘 작동합니다. 로그 메시지 compuation이 더 복잡한 경우 로깅하기 전에 로그 수준을 확인할 수 있습니다.

if (LOG.isDebugEnabled()) {
    // Argument expression evaluated only when DEBUG is enabled
    LOG.debug("Request coming from {}, parameters: {}", myInetAddress,
              Arrays.toString(veryLongParamArray);
}

여기서 값 비싼 Arrays.toString(Obect[]) 계산을 사용하는 LOG.debug()DEBUG 가 실제로 활성화 된 경우에만 처리됩니다.

피톨 (Pitfall) - 루프의 문자열 연결은 확장되지 않습니다.

다음 코드를 예제로 생각해보십시오.

public String joinWords(List<String> words) {
    String message = "";
    for (String word : words) {
        message = message + " " + word;
    }
    return message;
}

불행히도이 코드는 words 목록이 길면 비효율적입니다. 문제의 근본 원인은 다음과 같습니다.

message = message + " " + word;

각 루프 반복에 대해이 명령문은 원래 message 문자열의 모든 문자 사본에 추가 문자가 추가 된 새 message 문자열을 작성합니다. 이것은 많은 임시 문자열을 생성하고 많은 복사를 수행합니다.

joinWords 를 분석 할 때 평균 길이가 M 인 N 개의 단어가 있다고 가정하면 O (N) 개의 임시 문자열이 만들어지고 O (MN 2 ) 문자가 복사됩니다. N 2 구성 요소는 특히 문제가 있습니다.

이런 종류의 문제 1에 대한 권장 접근법은 다음과 같이 문자열 연결 대신 StringBuilder 를 사용하는 것입니다.

public String joinWords2(List<String> words) {
    StringBuilder message = new StringBuilder();
    for (String word : words) {
        message.append(" ").append(word);
    }
    return message.toString();
}

joinWords2 의 분석은 빌더의 문자를 보유하고있는 StringBuilder 백업 어레이를 "증가시키는"간접비를 고려해야합니다. 그러나 새로 생성 된 객체의 수는 O (logN)이고 복사 된 문자 수는 O (MN) 문자라는 것을 알 수 있습니다. 후자는 최종 toString() 호출에서 복사 된 문자를 포함합니다.

( StringBuilder 를 시작할 수있는 정확한 용량으로 작성하여 더 조정할 수도 있지만 전체적인 복잡성은 동일하게 유지됩니다.)

원래의 joinWords 메소드로 되돌아 가면, 중요한 Java 문에 의해 다음과 같이 결정됩니다.

  StringBuilder tmp = new StringBuilder();
  tmp.append(message).append(" ").append(word);
  message = tmp.toString();

그러나 Java 컴파일러는 joinWords2 코드에서 joinWords2 것처럼 루프에서 StringBuilder "끌어 올리지"않습니다.

참고:


1 - Java 8 이상에서는 Joiner 클래스를 사용하여이 특정 문제를 해결할 수 있습니다. 그러나 그것은이 예가 실제로 있어야 하는 것이 아닙니다.

Pitfall - 'new'를 사용하여 프리미티브 래퍼 인스턴스를 만드는 것은 비효율적입니다.

Java 언어를 사용하면 Integer , Boolean 등의 인스턴스를 new 만들 수 있지만 일반적으로 좋지 않습니다. autoboxing (Java 5 이상) 또는 valueOf 메소드를 사용하는 것이 좋습니다.

 Integer i1 = new Integer(1);      // BAD
 Integer i2 = 2;                   // BEST (autoboxing)
 Integer i3 = Integer.valueOf(3);  // OK

new Integer(int) 명시 적으로 사용하는 것이 나쁜 생각 인 이유는 JIT 컴파일러가 최적화하지 않는 한 새로운 객체를 만드는 것입니다. 대조적으로, autoboxing 또는 명시적인 valueOf 호출이 사용되면 Java 런타임은 기존 객체의 캐시에서 Integer 객체를 재사용하려고 시도합니다. 런타임에 캐시 "히트 (hit)"가있을 때마다 객체 생성을 피할 수 있습니다. 또한 힙 메모리를 절약하고 객체 이동으로 인한 GC 오버 헤드를 줄입니다.

노트:

  1. 최근 Java 구현에서 autoboxing은 valueOf 를 호출하여 구현되며 Boolean , Byte , Short , Integer , LongCharacter 대한 캐시가 있습니다.
  2. 정수형에 대한 캐싱 동작은 Java 언어 사양에서 요구합니다.

Pitfall - '새 문자열 (문자열)'호출이 비효율적입니다.

new String(String) 을 사용하여 new String(String) 을 복제하는 것은 비효율적이며 거의 항상 불필요합니다.

  • 문자열 객체는 변경할 수 없으므로 변경 사항을 방지하기 위해 객체를 복사 할 필요가 없습니다.
  • 일부 이전 Java 버전에서는 String 객체가 다른 String 객체와 함께 배킹 배열을 공유 할 수 있습니다. 이러한 버전에서는 (큰) 문자열의 (작은) 하위 문자열을 생성하고 유지함으로써 메모리를 누출하는 것이 가능합니다. 그러나 Java 7 이상에서는 String backing arrays가 공유되지 않습니다.

실용적인 이점이 없으면 new String(String) 호출하는 것은 낭비입니다.

  • 복사에는 CPU 시간이 필요합니다.
  • 복사본은 더 많은 메모리를 사용하여 응용 프로그램의 메모 리 공간을 늘리거나 GC 오버 헤드를 증가시킵니다.
  • String 객체가 복사되면 equals(Object)hashCode() equals(Object) 와 같은 연산이 느려질 수 있습니다.

Pitfall - System.gc () 호출이 비효율적입니다.

System.gc() 를 호출하는 것은 (거의 항상) 나쁜 생각입니다.

gc() 메서드의 javadoc은 다음을 지정합니다.

" gc 메소드를 호출하면 Java Virtual Machine이 현재 사용중인 메모리를 신속하게 재사용 할 수 있도록하기 위해 사용되지 않는 객체를 재활용하는 데 많은 노력을 기울이고 있음을 알 수 있습니다. 메소드 호출에서 제어가 반환되면 Java Virtual Machine은 재생을 위해 최선의 노력을 다했습니다 모든 폐기 된 물체의 공간. "

이것에서 끌어낼 수있는 몇 가지 중요한 포인트가 있습니다 :

  1. "알려줌"이라기보다는 "제안하다"라는 단어를 사용한다는 것은 JVM이 제안을 무시할 수 있다는 것을 의미합니다. 기본 JVM 동작 (최근 릴리스)은 제안 사항을 따르는 것이지만 JVM을 시작할 때 -XX:+DisableExplicitGC 를 설정하면 무시할 수 있습니다.

  2. 문구 "폐기 된 모든 객체의 공간을 되찾기위한 최선의 노력"은 gc 를 호출하면 "전체"가비지 수집이 gc 을 의미합니다.

그렇다면 왜 System.gc() 를 나쁜 생각이라고 System.gc() 니까?

첫째, 전체 가비지 콜렉션을 실행하는 것은 비용이 많이 듭니다. 완전한 GC는 여전히 접근 할 수있는 모든 객체를 방문하고 "표시"하는 것을 포함합니다. 즉 쓰레기가 아닌 모든 물체. 수집 할 쓰레기가 많지 않을 때이를 트리거하면 GC는 비교적 적은 이익을 위해 많은 작업을 수행합니다.

둘째, 전체 가비지 수집은 수집되지 않은 객체의 "지역"속성을 방해하기 쉽습니다. 거의 같은 시간에 동일한 스레드에 의해 할당 된 객체는 메모리에서 가깝게 할당되는 경향이 있습니다. 이것은 좋다. 동시에 할당 된 객체는 관련성이 높습니다. 즉 서로를 참조하십시오. 응용 프로그램에서 이러한 참조를 사용하면 다양한 메모리 및 페이지 캐싱 효과로 인해 메모리 액세스 속도가 빨라질 수 있습니다. 불행하게도, 전체 가비지 수집은 한 번 닫힌 객체가 더 멀리 떨어져 있도록 객체를 움직이는 경향이 있습니다.

셋째, 전체 가비지 수집을 실행하면 수집이 완료 될 때까지 애플리케이션이 일시 중지됩니다. 이 상황이 발생하는 동안 응용 프로그램은 응답하지 않습니다.

사실 가장 좋은 전략은 JVM이 GC를 실행할 시점과 실행할 콜렉션의 종류를 결정하게하는 것입니다. 간섭하지 않으면 JVM은 처리량을 최적화하거나 GC 일시 중지 시간을 최소화하는 시간 및 수집 유형을 선택합니다.


처음에는 "... (거의 항상) 나쁜 생각 ..."이라고했습니다. 실제로 좋은 아이디어 있는 몇 가지 시나리오 있습니다.

  1. 가비지 수집에 민감한 코드 (예 : finalizers 또는 weak / soft / phantom 참조가 포함 된 코드)에 대한 단위 테스트를 구현하는 경우 System.gc() 를 호출해야 할 수 있습니다.

  2. 일부 대화식 응용 프로그램에서는 가비지 콜렉션 일시 중지가있는 경우 사용자가 신경 쓰지 않는 특정 시점이있을 수 있습니다. 한 가지 예가 "놀이"에서 자연스러운 일시 중지가있는 게임입니다. 예를 들어 새로운 레벨을 로딩 할 때.

Pitfall - 원시 래퍼 유형을 과도하게 사용하면 비효율적입니다.

다음과 같은 두 가지 코드를 고려하십시오.

int a = 1000;
int b = a + 1;

Integer a = 1000;
Integer b = a + 1;

질문 : 어느 버전이 더 효율적입니까?

답변 : 두 버전은 거의 동일하지만 첫 번째 버전은 두 번째 버전보다 훨씬 효율적입니다.

두 번째 버전은 더 많은 공간을 사용하는 숫자에 대한 표현을 사용하고 있으며 장면 뒤에서 자동 복싱 및 자동 언 박싱에 의존합니다. 사실 두 번째 버전은 다음 코드와 직접적으로 같습니다.

Integer a = Integer.valueOf(1000);               // box 1000
Integer b = Integer.valueOf(a.intValue() + 1);   // unbox 1000, add 1, box 1001

이것을 int 를 사용하는 다른 버전과 비교할 때 Integer 를 사용할 때 세 가지 추가 메서드 호출이 분명히 있습니다. valueOf 의 경우 호출은 각각 새 Integer 객체를 만들고 초기화합니다. 이 여분의 복싱과 언 박싱 작업은 모두 두 번째 버전을 첫 번째 버전보다 느리게 만듭니다.

그 외에도 두 번째 버전에서는 각 valueOf 호출에서 힙에 객체를 할당합니다. 공간 활용도는 플랫폼에 따라 IntegerInteger 객체의 16 바이트 영역에있을 가능성이 큽니다. 반대로 int 버전에서는 ab 가 지역 변수라고 가정하고 추가 힙 공간이 필요하지 않습니다.


프리미티브가 더 빠른 다른 큰 이유는 boxed equivalent가 각각의 배열 유형이 메모리에 어떻게 배치되어 있는지에 있습니다.

int[]Integer[] 를 예로 들자면 int[] 경우 int 이 메모리에 연속적으로 배치됩니다. 그러나 경우에 Integer[] 가에 배치되는 값 만 참조 (포인터)이 아니다 Integer 차례에 실제 포함하는 개체, int 값을.

추가 수준의 간접 참조 외에도 값을 반복 할 때 캐시 지역성에 있어서는 큰 변화가 될 수 있습니다. int[] 경우 CPU는 배열의 모든 값을 메모리에서 연속적으로 가져올 수 있으므로 한 번에 캐시로 가져올 수 있습니다. 그러나 Integer[] 의 경우 배열에 실제 값에 대한 참조 만 있기 때문에 CPU는 잠재적으로 각 요소에 대해 추가 메모리 가져 오기를 수행해야합니다.


즉, 원시 래퍼 유형을 사용하는 것은 CPU와 메모리 자원 모두에서 비교적 비쌉니다. 그것들을 불필요하게 사용하는 것이 효율적입니다.

함정 -지도의 키 반복은 비효율적 일 수 있습니다.

다음 예제 코드는 필요한 것보다 느립니다.

Map<String, String> map = new HashMap<>(); 
for (String key : map.keySet()) {
    String value = map.get(key);
    // Do something with key and value
}

이는 맵의 각 키에 대한 맵 룩업 ( get() 메소드)이 필요하기 때문입니다. 이 조회는 효율적이지 않을 수 있습니다 (HashMap에서는 키에 hashCode 를 호출 한 다음 내부 데이터 구조에서 올바른 버킷을 조회하고 때로는 equals 호출하는 경우도 equals ). 대형지도에서는 ​​이것이 단순한 오버 헤드가 아닐 수도 있습니다.

이 문제를 피하는 올바른 방법은지도 항목을 반복하는 것입니다.지도 항목은 컬렉션 항목 에서 자세히 설명합니다.

Pitfall - 컬렉션이 비어 있는지 테스트하기 위해 size ()를 사용하는 것은 비효율적입니다.

Java Collections Framework에서는 모든 Collection 객체에 대해 두 가지 관련 메서드를 제공합니다.

  • size()Collection 의 항목 수를 반환하고
  • isEmpty() 메서드는 Collection 이 비어있는 경우에만 true를 반환합니다.

두 방법 모두 수집 비어 있음을 테스트하는 데 사용할 수 있습니다. 예 :

Collection<String> strings = new ArrayList<>();
boolean isEmpty_wrong = strings.size() == 0; // Avoid this
boolean isEmpty = strings.isEmpty();         // Best

이러한 접근 방식이 동일하게 보이지만 일부 수집 구현에서는 크기를 저장하지 않습니다. 이러한 컬렉션의 경우 size() 구현은 호출 될 때마다 크기를 계산해야합니다. 예를 들면 :

  • 간단한 연결 목록 클래스 ( java.util.LinkedList 제외)는 요소를 계산하기 위해 목록을 탐색해야합니다.
  • ConcurrentHashMap 클래스는 모든 맵의 "세그먼트"에있는 항목을 합산해야합니다.
  • 느린 구현의 컬렉션은 요소를 계산하기 위해 전체 컬렉션을 메모리에 구현해야 할 수도 있습니다.

대조적으로 isEmpty() 메서드는 컬렉션 에 적어도 하나 이상의 요소가 있는지 테스트해야합니다. 이것은 요소 계산에 수반되지 않습니다.

size() == 0isEmpty() 보다 덜 효율적인 것은 아니지만 제대로 구현 된 isEmpty()size() == 0 보다 효율적이지 않은 것은 상상할 수 없습니다. 따라서 isEmpty() 가 선호됩니다.

함정 - 정규식에 대한 효율성 문제

정규식 일치는 강력한 도구 (Java 및 기타 환경에서)이지만 몇 가지 단점이 있습니다. 이 중 하나는 정규 표현식이 다소 비싼 경향이 있다는 것입니다.

Pattern과 Matcher 인스턴스는 재사용되어야한다.

다음 예제를 고려하십시오.

/**
 * Test if all strings in a list consist of English letters and numbers.
 * @param strings the list to be checked
 * @return 'true' if an only if all strings satisfy the criteria
 * @throws NullPointerException if 'strings' is 'null' or a 'null' element.
 */
public boolean allAlphanumeric(List<String> strings) {
    for (String s : strings) {
        if (!s.matches("[A-Za-z0-9]*")) {
            return false;
        }  
    }
    return true;
}

이 코드는 정확하지만 비효율적입니다. 문제는 matches(...) 호출에 있습니다. 후드 아래에서 s.matches("[A-Za-z0-9]*") 는 다음과 같습니다.

Pattern.matches(s, "[A-Za-z0-9]*")

차례로

Pattern.compile("[A-Za-z0-9]*").matcher(s).matches()

Pattern.compile("[A-Za-z0-9]*") 호출은 정규식을 구문 분석하고 분석하며 정규식 엔진에서 사용할 데이터 구조를 보유하는 Pattern 객체를 생성합니다. 이것은 단순한 계산입니다. 그런 다음 s 인수를 래핑하도록 Matcher 객체가 작성됩니다. 마침내 match() 를 호출하여 실제 패턴 일치를 수행합니다.

문제는이 작업이 각각의 루프 반복마다 반복된다는 것입니다. 해결 방법은 다음과 같이 코드를 재구성하는 것입니다.

private static Pattern ALPHA_NUMERIC = Pattern.compile("[A-Za-z0-9]*");

public boolean allAlphanumeric(List<String> strings) {
    Matcher matcher = ALPHA_NUMERIC.matcher("");
    for (String s : strings) {
        matcher.reset(s);
        if (!matcher.matches()) {
            return false;
        }  
    }
    return true;
}

Patternjavadoc 에서는, 다음과 같이되어 있습니다.

이 클래스의 인스턴스는 불변으로, 복수의 병행 thread에 의해 사용하기에 안전합니다. Matcher 클래스의 인스턴스는 그러한 사용에 안전하지 않습니다.

find ()를 사용할 때 match ()를 사용하지 마십시오.

문자열 s 에 행에 3 자리 이상의 숫자가 포함되어 있는지 테스트하려고한다고 가정합니다. 당신은 다음과 같은 다양한 방법으로이를 표현합니다.

  if (s.matches(".*[0-9]{3}.*")) {
      System.out.println("matches");
  }

또는

  if (Pattern.compile("[0-9]{3}").matcher(s).find()) {
      System.out.println("matches");
  }

첫 번째 방법은 더 간결하지만 더 효율적이지는 않습니다. 첫 번째 버전에서는 전체 문자열을 패턴과 비교하려고 시도합니다. 또한 ". *"는 "탐욕스러운"패턴이기 때문에 패턴 매치는 문자열의 끝까지 "간절히"나아가고 일치하는 것을 찾을 때까지 뒤로 추적 할 것입니다.

대조적으로, 두 번째 버전은 왼쪽에서 오른쪽으로 검색하고 행에서 3 자리 숫자를 찾자 마자 검색을 중지합니다.

정규 표현식보다 효율적인 대안 사용

정규 표현식은 강력한 도구이지만, 유일한 도구는 아닙니다. 많은 작업이 다른 방법으로보다 효율적으로 수행 될 수 있습니다. 예 :

 Pattern.compile("ABC").matcher(s).find()

같은 일을한다.

 s.contains("ABC")

후자가 훨씬 더 효율적이라는 점만 제외하면 (정규 표현식을 컴파일하는 비용을 할부 상환 할 수 있다고하더라도).

흔히 비 정규 표현식은 더 복잡합니다. 예를 들어, matches() 메소드가 수행 한 테스트는 이전의 allAlplanumeric 메소드를 다음과 같이 다시 작성할 수 있습니다.

 public boolean matches(String s) {
     for (char c : s) {
         if ((c >= 'A' && c <= 'Z') ||
             (c >= 'a' && c <= 'z') ||
             (c >= '0' && c <= '9')) {
              return false;
         }
     }
     return true;
 }

Matcher 사용하는 것보다 코드가 더 많이 필요하지만 훨씬 빨라질 것입니다.

비극적 인 역 추적

(이것은 정규 표현식의 모든 구현에서 잠재적으로 문제가 될 수 있지만 Pattern 사용의 함정이기 때문에 여기에서 언급 할 것입니다.)

이 (고안 한) 예를 생각해보십시오.

Pattern pat = Pattern.compile("(A+)+B");
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB").matches());
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC").matches());

최초의 println 호출은 신속하게 true 인쇄 true . 두 번째 것은 false 을 인쇄합니다. 결국. 실제로 위의 코드로 실험하면 C 앞에 A 를 추가 할 때마다 시간이 두 배가 걸리는 것을 볼 수 있습니다.

이것은 행동이 재앙적인 역 추적 의 예입니다. 정규식 일치를 구현하는 패턴 일치 엔진은 패턴 일치 할 수 있는 가능한 모든 방법을 유용하게 시도합니다.

(A+)+B 실제로 무엇을 의미하는지 살펴 보겠습니다. 피상적으로 "하나 이상의 A 문자 뒤에 B 값"이 붙어있는 것처럼 보이지만 실제로는 하나 이상의 그룹을 말하며 각 그룹은 하나 이상의 A 문자로 구성됩니다. 예를 들면 다음과 같습니다.

  • 'AB'는 한 방향으로 만 일치합니다 : '(A) B'
  • 'AAB'는 '(AA) B'또는 '(A) (A) B'와 일치합니다.
  • 'AAAB'는 '(AAA) B'또는 '(AA) (A) B or '(A)(AA)B 또는 '(A) (A) (A) B'
  • 등등

즉, 가능한 일치 항목의 수는 2 N입니다. 여기서 N은 A 문자 수입니다.

위의 예는 분명히 고안된 것이지만, 이러한 종류의 성능 특성 (즉, 큰 K 대해 O(2^N) 또는 O(N^K) 을 나타내는 패턴은 불분명 한 정규식이 사용될 때 자주 발생합니다. 몇 가지 표준 치료법이 있습니다.

  • 다른 반복 패턴 내에서 반복 패턴을 중첩하지 마십시오.
  • 너무 많은 반복 패턴을 사용하지 마십시오.
  • 적절하게 역행하지 않는 반복을 사용하십시오.
  • 복잡한 구문 분석 작업에는 정규 표현식을 사용하지 마십시오. 대신 적절한 파서를 작성하십시오.

마지막으로, 사용자 또는 API 클라이언트가 병리학 적 특성을 가진 정규식 문자열을 제공 할 수있는 상황을 조심하십시오. 우발적이거나 고의적 인 "서비스 거부"로 이어질 수 있습니다.

참고 문헌 :

Pitfall - 인터 네거티브 문자열을 사용하면 ==을 (를) 사용할 수 있습니다.

일부 프로그래머가이 조언을 볼 때 :

" == 사용하여 문자열을 테스트하는 것은 올바르지 않습니다 (문자열이 인자기되지 않는 한)"

초기 반응은 == 를 사용할 수 있도록 인턴 문자열에 있습니다. (결국 == String.equals(...) 호출하는 것보다 빠릅니다.)

이것은 여러 가지 관점에서 잘못된 접근 방식입니다.

취약성

무엇보다도, 당신은 안전하게 사용할 수 있습니다 == 당신이 모든 것을 알고있는 경우 String 이 구금 된 테스트 오브젝트. JLS는, 소스 코드 내의 String 리터럴이 인 텐트되었다는 것을 보증합니다. 그러나 표준 Java SE API 중 어느 것도 String.intern(String) 자체를 제외하고는 String.intern(String) 문자열을 반환하지 않습니다. 인턴되지 않은 String 객체의 소스를 하나만 놓친 경우, 응용 프로그램은 신뢰할 수 없습니다. 그 신뢰성이 떨어지면 발견하기가 더 어려워지는 예외가 아니라 위조 방지로 나타납니다.

'intern ()'사용 비용

후드 내부에서 이전에 인턴 된 String 객체를 포함하는 해시 테이블을 유지함으로써 인턴이 작동합니다. 내부 해시 테이블이 저장소 누수가되지 않도록 약한 참조 메커니즘이 사용됩니다. 해쉬 테이블은 네이티브 코드로 구현되지만 ( HashMap , HashTable 과는 달리) intern 콜은 여전히 ​​사용되는 CPU 및 메모리면에서 상대적으로 비용이 많이 든다.

이 비용은 equals 대신 == 를 사용하여 절약하려고하는 것과 비교되어야합니다. 사실 각 인턴 된 문자열을 다른 문자열과 "몇"비교하지 않으면 중단하지 않습니다.

(제외 : 인턴 가치가있는 몇 가지 상황이 같은 문자열이 여러 번 재발하는 응용 프로그램의 메모리 풋 프린트를 감소에 대한 경향, 이러한 문자열은 긴 수명을 가지고있다.)

가비지 수집에 미치는 영향

위에서 설명한 직접 CPU 및 메모리 비용 외에도, 내부 문자열은 가비지 수집기 성능에 영향을줍니다.

자바 7 이전의 Java 버전에서는 인자기 문자열이 가끔 수집되는 "PermGen"공간에 보관됩니다. PermGen을 수집해야하는 경우 (일반적으로) 전체 가비지 수집을 트리거합니다. PermGen 공간이 완전히 채워지면 일반 힙 공간에 여유 공간이 있더라도 JVM이 충돌합니다.

Java 7에서 문자열 풀이 "PermGen"에서 일반 힙으로 이동되었습니다. 그러나 해시 테이블은 여전히 ​​오래 유지되는 데이터 구조가 될 것이며, 이는 인턴 된 문자열의 수명을 연장시킵니다. 인턴 된 문자열 객체가 에덴 공간에 할당 된 경우에도 수집되기 전에 홍보 될 가능성이 큽니다.

따라서 모든 경우에 문자열을 지정하면 평범한 문자열과 비교하여 수명이 연장됩니다. 그러면 JVM 수명 동안 가비지 콜렉션 오버 헤드가 증가합니다.

두 번째 문제는 해시 테이블이 문자열 인 에이블 링 메모리 누수를 방지하기 위해 어떤 종류의 약한 참조 메커니즘을 사용해야한다는 것입니다. 그러나 이러한 메커니즘은 가비지 수집기에 더 많은 작업입니다.

이러한 가비지 수집 오버 헤드는 계량하기가 어렵지만 실제로 존재한다는 의심의 여지가 거의 없습니다. intern 광범위하게 사용하면 중요 할 수 있습니다.

문자열 풀 해시 테이블 크기

이 소스 에 따르면 Java 6부터 문자열 풀은 동일한 버킷에 해시되는 문자열을 처리하기 위해 체인이있는 고정 크기의 해시 테이블로 구현됩니다. Java 6의 초기 릴리스에서 해시 테이블은 고정 크기를 가졌습니다. 튜닝 매개 변수 ( -XX:StringTableSize )가 Java 6의 중간 수명 업데이트로 추가되었습니다. Java 7의 중간 수명 업데이트에서 풀의 기본 크기가 1009 에서 60013 으로 변경되었습니다.

결론은 당신이 사용하려는 경우이다 intern 코드에서 집중적으로,이 해시 테이블 크기가 조정입니다 자바의 버전을 선택하고 적절하게 당신 조정 크기 그것을 확인하는 것이 좋습니다. 그렇지 않으면 intern 의 성과는 수영장이 커짐에 따라 저하 될 수 있습니다.

잠재적 인 서비스 거부 벡터로서의 인터닝

문자열에 대한 해시 코드 알고리즘은 잘 알려져 있습니다. 악의적 인 사용자 나 응용 프로그램이 제공 한 문자열을 인턴으로받은 경우 DoS (Denial of Service) 공격의 일부로 사용할 수 있습니다. 악의적 인 에이전트가 제공하는 모든 문자열이 동일한 해시 코드를 갖도록 배열하면 intern 에 대한 불균형 해시 테이블과 O(N) 성능이 발생할 수 있습니다. 여기서 N 은 충돌 한 문자열의 수입니다.

(서비스에 대한 DoS 공격을 시작하는 데 더 간단하고 효과적인 방법이 있지만 DoS 공격의 목표가 보안을 깨거나 DoS 방어를 피하는 것이면이 벡터를 사용할 수 있습니다.)

Pitfall - 버퍼링되지 않은 스트림에 대한 읽기 / 쓰기가 적습니다. 비효율적입니다.

한 파일을 다른 파일에 복사하려면 다음 코드를 고려하십시오.

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new FileInputStream(args[0]);
             OutputStream os = new FileOutputStream(args[1])) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

(우리는이 예제의 요점 과 관련이 없기 때문에 일반적인 인수 검사, 오류보고 등을 생략했습니다.)

위 코드를 컴파일하고 거대한 파일을 복사하는 데 사용하면 매우 느리다는 것을 알 수 있습니다. 실제로 표준 OS 파일 복사 유틸리티보다 몇 배 이상 느려질 것입니다.

( 실제 실적 측정을 여기에 추가하십시오! )

위의 예제가 느린 주된 이유는 (대용량 파일의 경우) 버퍼링되지 않은 바이트 스트림에서 1 바이트 읽기 및 1 바이트 쓰기를 수행한다는 것입니다. 성능을 향상시키는 간단한 방법은 스트림을 버퍼링 된 스트림으로 래핑하는 것입니다. 예 :

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new BufferedInputStream(
                     new FileInputStream(args[0]));
             OutputStream os = new BufferedOutputStream(
                     new FileOutputStream(args[1]))) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

이러한 작은 변화는 다양한 플랫폼 관련 요소에 따라 데이터 복사 속도 를 최소한 두 배 정도 향상시킵니다. 버퍼링 된 스트림 랩퍼는 데이터를 더 큰 청크로 읽고 쓸 수있게합니다. 인스턴스에는 모두 바이트 배열로 구현 된 버퍼가 있습니다.

  • 함께 is , 데이터는 시간에 버퍼로 파일에서 몇 킬로바이트 판독된다. read() 가 불려 가면 read() , 구현은 통상, 버퍼로부터 1 바이트를 돌려줍니다. 버퍼가 비어있는 경우는, 기본이되는 입력 스트림로부터의 read 만합니다.

  • os 의 동작은 유사합니다. os.write(int) 호출하면 os.write(int) 1 바이트를 버퍼에 기입합니다. 버퍼가 가득 os 가 플러시되거나 닫힐 때만 데이터가 출력 스트림에 기록됩니다.

문자 기반 스트림은 어떻습니까?

자바 I / O는 바이너리 및 텍스트 데이터를 읽고 쓸 수있는 다양한 API를 제공합니다.

  • InputStreamOutputStream 은 스트림 기반 바이너리 I / O의 기본 API입니다.
  • ReaderWriter 는 스트림 기반 텍스트 I / O의 기본 API입니다.

텍스트 I의 경우 / O, BufferedReaderBufferedWriter 대한 등가물이다 BufferedInputStreamBufferedOutputStream .

버퍼링 된 스트림이 왜 이렇게 큰 차이를 만들어 냈습니까?

버퍼링 된 스트림이 성능에 도움이되는 진정한 이유는 응용 프로그램이 운영 체제와 대화하는 방식과 관련이 있습니다.

  • Java 애플리케이션의 Java 메소드 또는 JVM의 원시 런타임 라이브러리의 원시 프로 시저 호출은 빠릅니다. 일반적으로 몇 가지 기계 명령어를 사용하며 성능에 미치는 영향은 최소화됩니다.

  • 반대로 운영 체제에 대한 JVM 런타임 호출은 빠르지 않습니다. 그들은 "시스템 호출"이라고 알려진 것을 사용합니다. 시스템 콜의 전형적인 패턴은 다음과 같습니다 :

    1. syscall 인수를 레지스터에 넣습니다.
    2. SYSENTER 트랩 명령을 실행하십시오.
    3. 트랩 처리기가 권한있는 상태로 전환되고 가상 메모리 매핑이 변경됩니다. 그런 다음 특정 시스템 호출을 처리하기 위해 코드로 디스패치합니다.
    4. syscall 핸들러는 사용자 프로세스가 보지 말아야하는 메모리에 액세스하라는 지시를받지 않도록주의하면서 인수를 검사합니다.
    5. syscall 특정 작업이 수행됩니다. read 호출의 경우 다음이 포함될 수 있습니다.
      1. 파일 디스크립터의 현재 위치에서 읽을 데이터가 있는지 확인한다.
      2. 파일 시스템 핸들러를 호출하여 필요한 데이터를 디스크 (또는 버퍼가 저장된 곳)에서 버퍼 캐시로 가져오고,
      3. 버퍼 캐시에서 JVM 제공 주소로 데이터 복사
      4. thstream 포인터 파일 디스크립터 위치 조정
    6. 시스템 호출에서 돌아옵니다. 이것은 VM 매핑을 다시 변경하고 특권 상태를 벗어나는 것을 수반합니다.

상상할 수 있듯이 단일 시스템 호출을 수행하면 수천 개의 기계 명령어를 수행 할 수 있습니다. 보수적으로, 정규 메서드 호출보다 적어도 두 배 이상 더 길다. (아마 3 개 이상.)

이 점을 고려할 때 버퍼링 된 스트림이 큰 차이를 만드는 이유는 이들이 시스템 콜의 수를 대폭 줄입니다. 각 read() 호출에 대해 syscall을 수행하는 대신 버퍼 된 입력 스트림은 필요에 따라 많은 양의 데이터를 버퍼로 읽습니다. 버퍼링 된 스트림에서 대부분의 read() 호출은 간단한 경계를 검사하여 이전에 읽은 byte 를 반환합니다. 유사한 추론이 출력 스트림의 경우와 문자 스트림의 경우에도 적용됩니다.

(일부 사람들은 버퍼링 된 I / O 성능이 읽기 요청 크기와 디스크 블록의 크기, 디스크 회전 대기 시간 등의 불일치로 인한 것이라고 생각합니다. 사실 최신 OS는 여러 가지 전략을 사용하여 응용 프로그램은 일반적으로 디스크를 기다릴 필요가 없습니다. 실제 설명이 아닙니다.)

버퍼링 된 스트림은 항상 승리합니까?

항상 그런 것은 아닙니다. 버퍼링 된 스트림은 응용 프로그램이 많은 "작은"읽기 또는 쓰기를 수행하는 경우 확실히 승리합니다. 그러나 응용 프로그램이 큰 byte[] 또는 char[] 에 대한 큰 읽기 또는 쓰기 만 수행해야하는 경우 버퍼링 된 스트림은 실제 이점을주지 않습니다. 실제로 (작은) 성능 저하가있을 수도 있습니다.

이 방법이 Java에서 파일을 복사하는 가장 빠른 방법입니까?

아니야. Java의 스트림 기반 API를 사용하여 파일을 복사하면 데이터의 메모리 - 메모리 복사가 하나 이상 추가로 발생합니다. NIO ByteBufferChannel API를 사용하는 경우이를 피할 수 있습니다. 여기에 별도의 예에 대한 링크를 추가하십시오.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow