영업의 추측 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
방법 점의 행 번호 정보 o
가 bar()
메소드 인수 중 하나를 포착하는 자유 변수이다
() -> o.hashCode()
이다.
잡기입니다. 앞서 말했듯이 람다 클래스 이름의 숫자는 런타임 생성 순서의 시퀀스 번호입니다. 따라서 실제 람다 표현식에 대한 매핑은 람다 표현식의 정확한 평가 순서에 달려 있습니다. 람다 표현식은 복잡한 프로그램의 실행마다 다를 수 있습니다. – Holger
@Holger Right. 대형 객체를 캡처 한 인스턴스는'CaptureTest $$ Lambda $ 9'가 아닐 수도 있습니다. 예를 들어 'CaptureTest $$ Lambda $ 347'이 될 수 있습니다. 힙 덤프는 어떤 클래스인지 알려줍니다. 해당 클래스가 있으면이를 분해하여 호출 할 정적 메서드를 찾을 수 있습니다. 정적 메서드는 소스 코드로 거슬러 올라갈 수 있습니다. 람다 정적 메서드의 이름 지정 규칙은 지정되지 않지만 안정적이며 상당히 예측 가능합니다. –
@Holger 정적 메소드 디스 어셈블리의 행 번호 테이블은 소스 파일의 올바른 위치를 가리 킵니다. 나는 이것을 포함하도록 대답을 편집했다. –