JDK-7181793 : Socket getOutputStream create streams that cannot be GC'ed until Socket is closed
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.net
  • Affected Version: 8
  • Priority: P2
  • Status: Closed
  • Resolution: Fixed
  • OS: os_x
  • CPU: x86
  • Submitted: 2012-07-04
  • Updated: 2018-05-16
  • Resolved: 2012-10-09
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8
8 b63Fixed
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
openjdk version "1.7.0-jdk7u6-b16"
OpenJDK Runtime Environment (build 1.7.0-jdk7u6-b16-20120704)
OpenJDK 64-Bit Server VM (build 23.2-b08, mixed mode)


A DESCRIPTION OF THE PROBLEM :
FileDescriptor keeps a hard reference to Closeables associated with the FD causing applications to leak memory. This is regression from version <= 1.7u5 caused by the fix to a different bug.

A very common pattern for systems that pool sockets is to ask for an outputstream along with each request. Long running processes with pooling will consume more and more memory until they run out.

If you execute the test case like this:

/Library/Java/JavaVirtualMachines/1.7.0u6.jdk/Contents/Home/bin/java -XX:+HeapDumpOnOutOfMemoryError -Xms32M -Xmx32M filedesc.Test

It will output:

100000
200000
300000
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid40237.hprof ...
Heap dump file created [42914849 bytes in 0.539 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.net.Socket.getOutputStream(Socket.java:912)
	at filedesc.Test.main(Test.java:23)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:601)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Analyzing the heap dump you will find approximately:

324339 instances of class [B
323816 instances of class java.lang.Object
323731 instances of class java.net.SocketOutputStream
2079 instances of class [C
2052 instances of class java.lang.String
771 instances of class java.util.TreeMap$Entry
757 instances of class [S
570 instances of class java.lang.Class
509 instances of class [I

If you run the test case on any previous version of the JVM it will run indefinitely and not run out of memory.

REGRESSION.  Last worked in version 7

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Make a socket, getOutputStream, let the outputstream go out of scope, GC, notice that it isn't collected and is being held by the FileDescriptor.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
You should be able to ask for an outputstream from a socket without closing it and without that outputstream leaking.
ACTUAL -
The FileDescriptor adds each SocketOutputStream to an ArrayList with a hard reference.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
package filedesc;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Test {
  public static void main(String[] args) throws IOException {
    final ServerSocket serverSocket = new ServerSocket(2000);
    new Thread(new Runnable() {
      public void run() {
        try {
          serverSocket.accept();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }).start();

    int i = 0;
    Socket socket = new Socket("localhost", 2000);
    while(true) {
      socket.getOutputStream();
      if (++i % 100000 == 0) System.out.println(i);
    }
  }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Replace FileDescriptor.class with a previous version that works correctly. Here is a suggested fix that uses weak references:

https://gist.github.com/3041647

Comments
Only AbstractPlainSocketImpl.getOutputStream seems to exhibit this bug. getInputStream creates one stream to use as reference for future callers.
09-10-2012

EVALUATION Note that changes to java.io have been backed out of 7u6 so this bug is only applicable to jdk8 now. We will fix java.net.Socket to do the right thing in jdk8 so cases where getInputStream or getOutputStream is invoked lots of times will not create new instances each time.
13-07-2012

EVALUATION The issue here is that Socket getInputStream and getOutputStream methods create a new stream connected to the FileDescriptor for each invocation. They should be changed so that there is at most one input and one output stream per Socket. Also note that changing the code to use weak references as proposed is highly problematic and was previously rejected because it allows streams with finalizers that close the stream to asynchronously close the stream when they are GC'ed (FileInputStream and FileOutputStream's finalizers are specified to do that). So this is the reason why a strong reference is required.
11-07-2012

EVALUATION Should avoid strong references given the example provided.
06-07-2012