2014-10-29 7 views
16

최근 메시지 처리 애플리케이션을 Java 7에서 Java 8로 업그레이드했습니다. 업그레이드 이후에 스트림이 읽히는 동안 스트림이 닫혔다는 예외가 때때로 발생합니다. 로깅은 종료 자 스레드가 스트림을 보유하는 객체 (즉 스트림을 닫음)에서 finalize()을 호출하고 있음을 나타냅니다. 다음과 같이Java에서 강력하게 접근 할 수있는 객체를 호출하는 finalize() 8

코드의 기본 개요는 다음과 같습니다

MIMEWriter writer = new MIMEWriter(out); 
in = new InflaterInputStream(databaseBlobInputStream); 
MIMEBodyPart attachmentPart = new MIMEBodyPart(in); 
writer.writePart(attachmentPart); 

MIMEWriterMIMEBodyPart이 집에서 성장 MIME/HTTP 라이브러리의 일부입니다. MIMEBodyPart는 다음 갖고, HTTPMessage 확장 :

public void close() throws IOException 
{ 
    if (m_stream != null) 
    { 
     m_stream.close(); 
    } 
} 

protected void finalize() 
{ 
    try 
    { 
     close(); 
    } 
    catch (final Exception ignored) { } 
} 

예외는 다음과 같다 MIMEWriter.writePart의 호출 체인에서 발생 후 part.writeBodyPartContent(this) 를 호출

  1. MIMEWriter.writePart()이 부분에 대한 헤더를 쓰는
  2. MIMEBodyPart.writeBodyPartContent() 우리의 유틸리티 메서드 IOUtil.copy(getContentStream(), out)을 호출하여 출력 내용에 내용을 스트리밍합니다.
  3. MIMEBodyPart.getContentStream() jus t는 컨스트럭터에 전달 된 입력 스트림을 반환합니다 (위의 코드 블록 참조).
  4. IOUtil.copy에는 입력 스트림에서 8K 청크를 읽고 입력 스트림이 비어있을 때까지 출력 스트림에 쓰는 루프가 있습니다.

MIMEBodyPart.finalize()IOUtil.copy이 실행되는 동안라고하며 다음과 같은 예외를 얻을 수있다 :

java.io.IOException: Stream closed 
    at java.util.zip.InflaterInputStream.ensureOpen(InflaterInputStream.java:67) 
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:142) 
    at java.io.FilterInputStream.read(FilterInputStream.java:107) 
    at com.blah.util.IOUtil.copy(IOUtil.java:153) 
    at com.blah.core.net.MIMEBodyPart.writeBodyPartContent(MIMEBodyPart.java:75) 
    at com.blah.core.net.MIMEWriter.writePart(MIMEWriter.java:65) 

우리는 호출자의 스택 트레이스를 기록하고 있음을 입증 HTTPMessage.close() 방법에 약간의 로깅을 넣어 IOUtil.copy()이 실행되는 동안 HTTPMessage.finalize()을 호출하는 finalizer 스레드입니다.

MIMEBodyPart 개체는 MIMEBodyPart.writeBodyPartContent의 스택 프레임에 현재 스레드 스택에서 this으로 확실히 도달 할 수 있습니다. JVM이 finalize()이라고하는 이유를 이해할 수 없습니다.

관련 코드의 추출을 시도하고 제 기계에서 엄격한 루프로 실행했지만 문제를 재현 할 수 없습니다. 우리는 dev 서버 중 하나에서 높은 부하로 문제를 신뢰할 수있게 재현 할 수 있지만 재현 할 수없는 작은 테스트 케이스를 만들려는 시도는 실패했습니다. 이 코드는 Java 7에서 컴파일되지만 Java 8에서 실행됩니다. 다시 컴파일하지 않고 Java 7로 다시 전환하면 문제가 발생하지 않습니다.

해결 방법으로, Java Mail MIME 라이브러리를 사용하여 영향을받는 코드를 다시 작성 했으므로 문제가 해결되었습니다 (아마도 Java Mail은 finalize()을 사용하지 않습니다). 그러나 응용 프로그램의 다른 finalize() 메서드가 잘못 호출되거나 Java가 아직 사용중인 개체를 가비지 수집하려고하는 것이 우려됩니다.

현재 가장 좋은 방법은 finalize()을 사용하지 말 것을 권장하며,이 자생 라이브러리를 다시 방문하여 finalize() 메서드를 제거합니다. 즉, 이전에이 문제를 만난 사람이 있습니까? 원인에 대한 아이디어가있는 사람이 있습니까?

+2

다른 설명이 없다면 놀랄 것입니다. 현재 스레드는 항상 콜렉터가 활성 객체를 식별하는 "루트"입니다. IOUtils.copy()가 반환되기 전에 파이널 라이저가 호출되는지 * 어떻게 알 수 있습니까? – Bhaskar

+0

이것은 JIT 버그와 매우 비슷합니다. JIT 디버깅을 켜고 패턴이 있는지 여부를 확인합니다. – chrylis

+0

@Bhaskar, 예외는'IOUtil.copy()'가 실행되는 동안 스트림이 닫혔다는 것을 증명합니다. – Nathan

답변

19

여기에 약간의 추측이 있습니다. 스택의 로컬 변수에 대한 참조가 있더라도 스택에 객체의 인스턴스 메소드에 대한 활성 호출이 있더라도 객체가 마무리되고 가비지 수집 될 수 있습니다! 요구 사항은 개체가 에 도달 할 수 없으므로입니다. 스택에있는 경우에도 후속 코드가 해당 참조를 건드리지 않으면 잠재적으로 도달 할 수 없습니다.

개체를 참조하는 로컬 변수가 아직 범위 내에있는 동안 개체를 GC로 처리하는 방법에 대한 예제는 this other answer을 참조하십시오. loop() 방법이 활성화되어있는 동안,에 대한 참조와 함께 아무것도 코드의 가능성이

class FinalizeThis { 
    protected void finalize() { 
     System.out.println("finalized!"); 
    } 

    void loop() { 
     System.out.println("loop() called"); 
     for (int i = 0; i < 1_000_000_000; i++) { 
      if (i % 1_000_000 == 0) 
       System.gc(); 
     } 
     System.out.println("loop() returns"); 
    } 

    public static void main(String[] args) { 
     new FinalizeThis().loop(); 
    } 
} 

없다 : 여기에

은 객체가 인스턴스 메서드 호출이 활성화되어있는 동안 완성 할 수있는 방법의 예 FinalizeThis 개체이므로 도달 할 수 없습니다. 따라서이를 마무리하고 GC 할 수 있습니다. JDK 8 GA에서는 다음과 같이 인쇄됩니다.

loop() called 
finalized! 
loop() returns 

MimeBodyPart과 비슷한 내용이있을 수 있습니다. 로컬 변수에 저장됩니까? (코드 필드는 m_ 접두사로 명명 규약을 준수 보이기 때문에 그렇게 보인다.) 의견에

UPDATE

가, 영업 이익은 다음과 같이 변경하고 제안 :

을 그는 마무리를 관찰하고 있지 않습니다 변화
public static void main(String[] args) { 
     FinalizeThis finalizeThis = new FinalizeThis(); 
     finalizeThis.loop(); 
    } 

, 어느 쪽도 아니이 추가로 변경이되어있는 경우, 그러나 I.를 수행

public static void main(String[] args) { 
     FinalizeThis finalizeThis = new FinalizeThis(); 
     for (int i = 0; i < 1_000_000; i++) 
      Thread.yield(); 
     finalizeThis.loop(); 
    } 

마무리가 다시 발생합니다. 그 이유는 루프가 없으면 main() 메쏘드가 컴파일되지 않고 해석된다는 것입니다. 통역사는 도달 가능성 분석에 대해 덜 공격적입니다. yield 루프가 제자리에 있으면 main() 메서드가 컴파일되고 JIT 컴파일러는 loop() 메서드가 실행되는 동안 finalizeThis에 도달 할 수 없게 된 것을 감지합니다.

이 동작을 트리거하는 또 다른 방법은 -Xcomp 옵션을 JVM에 사용하는 것입니다.이 옵션은 실행 전에 JIT 컴파일되도록 강제합니다. JIT는 모든 것을 컴파일하는 데 시간이 많이 걸리고 많은 공간을 차지할 수 있지만 루프를 사용하지 않고 작은 테스트 프로그램에서 이와 같은 경우를 플러시하는 데 유용합니다.

+2

Thanks @Stuart Marks - 스택 오버플로에 대한 믿음을 회복했습니다. 흥미로운 점은 Java 7과 Java 8에서 나에게 똑같이 적용된다는 것입니다. 제 문제는 Java 8로 전환했을 때만 나타났습니다. 분석은 의미가 있으며, 우리가'finalize() '를 제거하도록 더 고무 시켰습니다. 코드 기반. – Nathan

+0

또 다른 쿼리 - 프로그램의 main 메소드를'FinalizeThis finalizeThis = new FinalizeThis();로 변경했습니다. finalizeThis.loop();'. 일단 내가 그랬 으면, 그 물체는 결코 완성되지 않았다. 나는 그것이 왜 다를지 확신하지 못합니다. 아이디어가 있습니까? – Nathan

+2

@ Nathan 귀하의 의견에 답하여 답변을 업데이트했습니다. Java 8에서 앱의 문제점 만 볼 수있는 이유에 대해서는 확신 할 수 없습니다. JIT 휴리스틱 스 (heuristics)는 아마 7과 8 사이에서 바뀌었을 것입니다. 또한 파이널 라이저를 제거하는 경우 무언가가 결국 스트림을 닫아야합니다. 아마도 try-with-resources를 사용할 수 있습니다. –

2

finalizer가 올바르지 않습니다.

먼저 catch 블록이 필요 없으며 super.finalize()finally{} 블록으로 호출해야합니다.

protected void finalize() throws Throwable 
{ 
    try 
    { 
     // do stuff 
    } 
    finally 
    { 
     super.finalize(); 
    } 
} 

을 두 번째로, 당신은 당신이 또는 정확하지 않을 수 있습니다 m_stream에 대한 유일한 참조를 보유 가정하고 다음과 같이 파이널 라이저의 정규 형식입니다. m_stream 회원은 완결되어야합니다. 그러나 당신은 그것을 성취하기 위해 무엇인가 할 필요가 없습니다. 궁극적으로 m_streamFileInputStream 또는 FileOutputStream 또는 소켓 스트림이되며 이미 올바르게 마무리됩니다.

난 그냥 그것을 제거합니다.

+0

나는'super.finalize()'를 호출하는 것에 원칙적으로 동의하지만,이 경우에는'HTTPMessage'는 빈'finalize()'가있는'Object'를 확장합니다. 나는 그 코드가 최적의 것이 아니라는 것에 동의하지만, 그 역시 관련이 있는지 확신 할 수 없다. 문제는'finalize()'가 Java 8에서는 잘못 호출 된 것처럼 보이지만 Java 7에서는 그렇지 않다는 것입니다. – Nathan

+0

나는 여전히 그것을 제거하고 어떤 일이 발생하는지 봅니다. 당신은 그것을 필요로하지 않으며 그것은 내가 말한 모든 이유에 대한 오류의 가능한 원천입니다. – EJP

+0

이 경우에는'finalize()'가 필요 없다는 것에 동의합니다 (코드에서 직접 in.close()를 호출합니다). 그러나 우리의 HTTP/MIME 라이브러리는'finalize()'가 필요한 다른 시나리오에서 사용됩니다. 앞에서 언급했듯이 앞으로 라이브러리와 모든 용도를 다시 방문하여'finalize()'를 제거 할 것입니다. 또한 문제를 피하기 위해 문제의 코드를 이미 다시 작성했습니다. 우리는'finalize()'메소드를 가지고있는 다른 코드 영역도 가지고 있습니다. 내 관심사는 우리가 코드의 다른 부분에서이 같은 문제에 부딪 힐 수도 있다는 것입니다. – Nathan