2017-01-10 5 views
7

나는 메모리 누수를 찾고 있는데, 힙 덤프는 여러 개의 람다 인스턴스가 문제의 객체를 보유하고 있음을 보여줍니다. 람다의 이름은 말미에 $$lambda$107 인 주변 클래스 이름입니다. 또한 힙을 채우는 객체를 참조하는 arg$1이라는 단일 필드 (이 필드에 적합한 이름)가 있음을 알 수 있습니다. 불행하게도이 클래스에는 람다 몇 개가 있습니다. 범위를 좁히기 위해 내가 할 수있는 일이 궁금합니다.힙 덤프에서 맹 글링 된 이름으로 Java lambda 찾기

arg$1은 암시 적 인수입니다. 람다가 클로저가 될 때 캡처되는 람다 식의 자유 변수입니다. 그 맞습니까?

나는 또한 107이 진짜 도움이되지 않는다고 추측하지만, 람다식이 어떤 숫자인지를 기록하도록 설정할 수있는 플래그가 있습니까?

기타 유용한 팁이 있습니까?

답변

5

영업의 추측 arg$1가 캡쳐 값을 포함하는 람다 객체의 필드 정확한지. lukeg의 답변은 람다 메타 팩토리가 프록시 클래스를 덤프하도록하는 데 올바른 경로에 있습니다. (+1)

javap 도구를 사용하여 참조를 소스 코드로 되돌려 놓은 인스턴스를 추적하는 방법은 다음과 같습니다. 기본적으로 올바른 프록시 클래스를 찾을 수 있습니다. 어떤 합성 람다 메서드를 호출하는지 알아 내기 위해 그것을 분해하십시오; 그 합성 람다 메서드를 소스 코드의 특정 람다 식과 연관시킵니다.

(대부분은이 정보 중 일부는 아니지만 대부분 Oracle JDK 및 OpenJDK에 적용됩니다. 다른 JDK 구현에서는 작동하지 않을 수도 있으며, 향후 변경 될 수 있습니다.) 최근 Oracle JDK 8 또는 OpenJDK 8이지만, JDK 9에서 계속 작동 할 것입니다.)

먼저 약간의 배경.람다를 포함하는 소스 파일이 컴파일되면 javac은 람다 본문을 포함하는 클래스에있는 합성 메소드로 컴파일합니다. 이러한 메서드는 private 및 static이며 이름은 lambda$<method>$<count>입니다. 메서드은 람다가 포함 된 메서드 이름이고 카운트은 소스 파일의 시작 부분에서 메서드에 번호를 매기는 순차 카운터입니다 0부터).

런타임에 람다 표현식이 으로 평가되면 람다 메타 팩토리가 호출됩니다. 이것은 람다 함수 인터페이스를 구현하는 클래스를 생성합니다. 이 클래스를 인스턴스화하고 함수 인터페이스 메소드 (있는 경우)로 인수를 가져 와서 캡처 된 값과 결합한 다음 위에서 설명한대로 javac으로 컴파일 된 합성 메소드를 호출합니다. 이 인스턴스를 "함수 개체"또는 "프록시"라고합니다.

프록시 클래스를 덤프하도록 lambda 메타 팩을 가져 오면 javap을 사용하여 바이트 코드를 디스 어셈블하고 프록시 인스턴스가 생성 된 람다 식으로 다시 추적 할 수 있습니다. 이것은 예를 들어 가장 잘 설명됩니다. 다음 코드를 고려하십시오.

public class CaptureTest { 
    static List<IntSupplier> list; 

    static IntSupplier foo(boolean b, Object o) { 
     if (b) { 
      return() -> 0;      // line 20 
     } else { 
      int h = o.hashCode(); 
      return() -> h;      // line 23 
     } 
    } 

    static IntSupplier bar(boolean b, Object o) { 
     if (b) { 
      return() -> o.hashCode();   // line 29 
     } else { 
      int len = o.toString().length(); 
      return() -> len;     // line 32 
     } 
    } 

    static void run() { 
     Object big = new byte[10_000_000]; 

     list = Arrays.asList(
      bar(false, big), 
      bar(true, big), 
      foo(false, big), 
      foo(true, big)); 

     System.out.println("Done."); 
    } 

    public static void main(String[] args) throws InterruptedException { 
     run(); 
     Thread.sleep(Long.MAX_VALUE); // stay alive so a heap dump can be taken 
    } 
} 

이 코드는 큰 배열을 할당 한 다음 네 가지 다른 람다 식을 계산합니다. 이 중 하나는 큰 배열에 대한 참조를 캡처합니다. (당신이 찾고있는 것을 안다면 검사로 알 수 있지만 때로는 어렵다.) 어떤 람다가 캡쳐를하고 있는가?

가장 먼저 할 일은이 클래스를 컴파일하고 javap -v -p CaptureTest을 실행하는 것입니다. -v 옵션에는 디스 어셈블 된 바이트 코드 및 줄 번호 표와 같은 기타 정보가 표시됩니다. 을 사적인 방법으로 분해하려면 -p 옵션을 제공해야합니다. 이것의 결과는 많은 재료를 포함하지만, 중요한 부분은 합성 람다 방법이다 :

private static int lambda$bar$3(int); 
    descriptor: (I)I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=1, args_size=1 
     0: iload_0 
     1: ireturn 
    LineNumberTable: 
     line 32: 0 
    LocalVariableTable: 
     Start Length Slot Name Signature 
      0  2  0 len I 

private static int lambda$bar$2(java.lang.Object); 
    descriptor: (Ljava/lang/Object;)I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=1, args_size=1 
     0: aload_0 
     1: invokevirtual #3     // Method java/lang/Object.hashCode:()I 
     4: ireturn 
    LineNumberTable: 
     line 29: 0 
    LocalVariableTable: 
     Start Length Slot Name Signature 
      0  5  0  o Ljava/lang/Object; 

private static int lambda$foo$1(int); 
    descriptor: (I)I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=1, args_size=1 
     0: iload_0 
     1: ireturn 
    LineNumberTable: 
     line 23: 0 
    LocalVariableTable: 
     Start Length Slot Name Signature 
      0  2  0  h I 

private static int lambda$foo$0(); 
    descriptor:()I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=0, args_size=0 
     0: iconst_0 
     1: ireturn 
    LineNumberTable: 
     line 20: 0 

방법 이름의 끝에서 카운터가 0에서 시작하여 파일의 시작으로부터 순차적으로 번호가된다. 또한 합성 메서드 이름에는 람다식이 포함 된 메서드 이름이 포함되므로 단일 메서드 내에서 발생하는 여러 람다 각각에서 생성 된 메서드를 확인할 수 있습니다.

그런 다음 java 명령에 명령 줄 인수 -Djdk.internal.lambda.dumpProxyClasses=<outputdir>을 제공하는 메모리 프로파일 러에서 프로그램을 실행하십시오. 이로 인해 람다 메타 팩토리는 생성 된 클래스를 이미 존재해야하는 명명 된 디렉터리로 덤프합니다.

응용 프로그램의 메모리 프로필을 가져 와서 검사하십시오. 이를 수행하는 데는 여러 가지 방법이 있습니다. NetBeans 메모리 프로파일 러를 사용했습니다. 나는 그것을 실행했을 때, 10,000,000 개의 원소를 가진 byte []가 CaptureTest$$Lambda$9 클래스의 arg$1 필드에 의해 유지되었다고 말했습니다. 이것은 OP가 얻은 한도 내입니다.

이 클래스 이름의 카운터는 런타임에 생성 된 순서대로 람다 메타 팩토리에서 생성 된 클래스의 시퀀스 번호를 나타내므로 유용하지 않습니다. 런타임 시퀀스를 아는 것이 소스 코드에서 시작된 위치에 대해서는별로 알려주지 않습니다.

그러나 lambda 메타 팩토리에 클래스를 덤프하도록 요청 했으므로이 클래스를보고 무엇이 수행되는지 살펴볼 수 있습니다. 실제로 출력 디렉토리에는 CaptureTest$$Lambda$9.class 파일이 있습니다.

final class CaptureTest$$Lambda$9 implements java.util.function.IntSupplier { 
    public int getAsInt(); 
    Code: 
     0: aload_0 
     1: getfield  #15     // Field arg$1:Ljava/lang/Object; 
     4: invokestatic #28     // Method CaptureTest.lambda$bar$2:(Ljava/lang/Object;)I 
     7: ireturn 
} 

당신은 정수 풀 항목을 디 컴파일 할 수 있지만, javap 유용하게 바이트 코드의 오른쪽에 코멘트 기호 이름을 넣습니다 : 거기에 javap -c을 실행하면 다음 보여준다. 잘못된 값인 arg$1 필드가로드되어 CaptureTest.lambda$bar$2 메서드에 전달됩니다. 이것은 우리 소스 파일에서 람다 번호 2 (0부터 시작 함)이며, bar() 메서드 내에서 두 개의 람다 표현식 중 첫 번째입니다. 이제 원본 클래스의 javap 출력으로 돌아가서 람다 정적 메서드의 행 번호 정보를 사용하여 소스 파일에서 위치를 찾을 수 있습니다. 이 위치에서 29 람다 라인에 CaptureTest.lambda$bar$2 방법 점의 행 번호 정보 obar() 메소드 인수 중 하나를 포착하는 자유 변수이다

() -> o.hashCode() 

이다.

+0

잡기입니다. 앞서 말했듯이 람다 클래스 이름의 숫자는 런타임 생성 순서의 시퀀스 번호입니다. 따라서 실제 람다 표현식에 대한 매핑은 람다 표현식의 정확한 평가 순서에 달려 있습니다. 람다 표현식은 복잡한 프로그램의 실행마다 다를 수 있습니다. – Holger

+0

@Holger Right. 대형 객체를 캡처 한 인스턴스는'CaptureTest $$ Lambda $ 9'가 아닐 수도 있습니다. 예를 들어 'CaptureTest $$ Lambda $ 347'이 될 수 있습니다. 힙 덤프는 어떤 클래스인지 알려줍니다. 해당 클래스가 있으면이를 분해하여 호출 할 정적 메서드를 찾을 수 있습니다. 정적 메서드는 소스 코드로 거슬러 올라갈 수 있습니다. 람다 정적 메서드의 이름 지정 규칙은 지정되지 않지만 안정적이며 상당히 예측 가능합니다. –

+0

@Holger 정적 메소드 디스 어셈블리의 행 번호 테이블은 소스 파일의 올바른 위치를 가리 킵니다. 나는 이것을 포함하도록 대답을 편집했다. –

0

런타임에 클래스에서 생성 된 각 람다가 생성 된 순서대로 결정되었으므로이 숫자는 꽤 쓸모가 없습니다. - 맞다면 하나의 클래스에 100 개의 람다를 가지고 있습니다. What does $$ in javac generated name mean?

참조하는 내용에 따라 문제가되는 λ에 대한 조사를 제한 할 수 없다면 가장 좋은 방법은 클래스를 약간 단순화하거나 가장 눈부신 용의자를 메소드 참조로 변환하는 것입니다.

3

이 조금 복잡하지만 당신은 시도 할 수 있습니다 :

  • -Djdk.internal.lambda.dumpProxyClasses=/path/to/directory/으로 JVM을 시작. 이 옵션을 사용하면 JVM 덤프 생성 프록시 객체 (클래스 파일)가 원하는 디렉토리에 생성됩니다.

  • 이렇게 생성 된 클래스를 디 컴파일 할 수 있습니다. 거기에서

    import java.util.function.IntPredicate; 
    
    // $FF: synthetic class 
    final class Test$$Lambda$3 implements IntPredicate { 
        private Test$$Lambda$3() { 
        } 
    
        public boolean test(int var1) { 
         return Test.lambda$bar$1(var1); 
        } 
    } 
    
  • : 내가 IntelliJ에 아이디어와가에 디 컴파일되어 생성 된 클래스 파일 중 하나 (시험 $$ 람다 $의 3.class라는 이름의 파일)을 열어 다음 람다를 사용하고 샘플 자바 코드를 만들었습니다 람다 유형 (예 : IntPredicate), 클래스 이름() 및 (bar)에 정의 된 메소드 이름을 유추 할 수 있습니다.