anonymous file streams reusing descriptors

2019-07-24 02:50发布

问题:

Will the following code blocks (each are equivalent) cause unexpected errors? Can I depend on the behavior, or is it subject to change?

                     // 1st
FileOutputStream f = new FileOutputStream(...);
                     // some io, not shown

                     // 2nd
                 f = new FileOutputStream(f.getFD());
                     // some io, not shown

                     // 3rd
                 f = new FileOutputStream(f.getFD());
                     // some io, not shown


static FileOutputStream ExampleFunction(FileOutputStream fos) {
    return new FileOutputStream(fos.getFD());
}
//              |-- 3rd ------| |-- 2nd ------| |-- 1st ----------------|
ExampleFunction(ExampleFunction(ExampleFunction(new FileOutputStream(...))))

There are two possible results, which I outline before. I always assume the worst possible outcome, that the unreferenced objects will be collected as soon as no references hold them. What follows is in relation to the first code block.

Case #1:

When the second FileOutputStream is assigned to f, the first output stream will no longer have any references, and thus be collected. When it is finalized, the underlying file descriptor (shared between all three streams) will be closed. At this point, any IO operations (not shown) on the second FileOutputStream will throw IOException. However the second FileOutputStream keeps a reference to the FileDescriptor (now closed), so that the final f.getFD() on the RHS of the third assignment does succeed. When the third FileOutputStream is assigned to f, the second output stream will be collected and the underlying FileDescriptor will be closed again (generating an IOException, I believe). Once again, however, any IO on the third stream will fail.

Case #2:

Alternatively, a FileDescriptor keeps strong references to all closeables that have been assigned to it. When the second FileOutputStream is assigned to f, the FileDescriptor maintains a reference to the first FileOutputStream so that it is never collected and finalized, and so that the FileDescriptor remains open. When the the third FileOutputStream is assigned to f, all three streams are referenced by the descriptor and not eligible to be collected.

Test Case:

I don't have JDK7 for testing, but apparently Case #1 applies (JDK7 FileDescriptor.java), unless an unkown third part holds the references or the garbage collector makes a specific exemption.

However, JDK8 apparently changes FileDescriptor to hold a list of closeables, so that Case #2 applies (JDK8 FileDescriptor.java). I can confirm this behaviour (on openjdk8) using the following test program:

import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.Thread;
import java.lang.Runtime;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import com.sun.management.UnixOperatingSystemMXBean;
import java.util.ArrayList;
import java.util.Arrays;


class TestThread extends Thread {
    static void gc() {
        System.gc();

        try {
            sleep(1000);
        }
        catch (InterruptedException e) {
            System.err.println(e.getMessage());
        }
    }

    static void test(String message,
                     long fd_count_a,
                     ArrayList<WeakReference<FileOutputStream>> fw,
                     OperatingSystemMXBean os,
                     FileDescriptor fd
                     ) throws IOException {
        long fd_count_b = fd_count_b = ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount() - fd_count_a;

        System.out.println("Results, " + message + ":");
        for (int  i=0; i<fw.size(); i++) {
            String prefix = "fw_" + String.valueOf(i);
            if (fw.get(i).get() == null) {
                System.out.println(prefix + ":\t\t" + "null");
                System.out.println(prefix + " open" + ":\t" + "no");
            } else {
                System.out.println(prefix + ":\t\t" + fw.get(i).get().toString());
                System.out.println(prefix + " open" + ":\t" + (fw.get(i).get().getFD().valid() ? "yes" : "no"));
            }
        }
        System.out.println("fd  :\t\t" + ((fd == null) ? "null" : fd.toString()));
        System.out.println("fds :\t\t" + String.valueOf(fd_count_b));
        System.out.println();
    }

    public void run() {
        try {
            run_contents();
        }
        catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

    public void run_contents() throws IOException {
        FileOutputStream                        f       = null;
        WeakReference<FileOutputStream>         fw_1    = null;
        WeakReference<FileOutputStream>         fw_2    = null;
        WeakReference<FileOutputStream>         fw_3    = null;
        FileDescriptor                          fd      = null;

        OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();

        long fd_count_a = fd_count_a = ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount();

        f       = new FileOutputStream("/dev/null");
        fw_1    = new WeakReference<FileOutputStream>(f);
        f.write(1);
        gc();
        test("after fw_1", fd_count_a, new ArrayList<WeakReference<FileOutputStream>>(Arrays.asList(fw_1)), os, f.getFD());

        f       = new FileOutputStream(f.getFD());
        fw_2    = new WeakReference<FileOutputStream>(f);
        f.write(2);
        gc();
        test("after fw_2", fd_count_a, new ArrayList<WeakReference<FileOutputStream>>(Arrays.asList(fw_1, fw_2)), os, f.getFD());

        f       = new FileOutputStream(f.getFD());
        fw_3    = new WeakReference<FileOutputStream>(f);
        f.write(3);
        gc();
        test("after fw_3", fd_count_a, new ArrayList<WeakReference<FileOutputStream>>(Arrays.asList(fw_1, fw_2, fw_3)), os, f.getFD());

        f.close();

        gc();
        test("after closing stream", fd_count_a, new ArrayList<WeakReference<FileOutputStream>>(Arrays.asList(fw_1, fw_2, fw_3)), os, f.getFD());

        fd = f.getFD();

        f = null;

        gc();
        test("after dereferencing stream", fd_count_a, new ArrayList<WeakReference<FileOutputStream>>(Arrays.asList(fw_1, fw_2, fw_3)), os, fd);

        fd = null;

        gc();
        test("after dereferencing descriptor", fd_count_a, new ArrayList<WeakReference<FileOutputStream>>(Arrays.asList(fw_1, fw_2, fw_3)), os, fd);
    }
}

class Test {
    public static void main(String[] args) {
        TestThread t = new TestThread();
        t.start();

        try {
            t.join();
        }
        catch (InterruptedException e) {
            System.err.println(e.getMessage());
        }
    }
}

which has the following output:

Results, after fw_1:
fw_0:        java.io.FileOutputStream@7afd6488
fw_0 open:   yes
fd  :        java.io.FileDescriptor@743a95a7
fds :        1

Results, after fw_2:
fw_0:        java.io.FileOutputStream@7afd6488
fw_0 open:   yes
fw_1:        java.io.FileOutputStream@70050ff8
fw_1 open:   yes
fd  :        java.io.FileDescriptor@743a95a7
fds :        1

Results, after fw_3:
fw_0:        java.io.FileOutputStream@7afd6488
fw_0 open:   yes
fw_1:        java.io.FileOutputStream@70050ff8
fw_1 open:   yes
fw_2:        java.io.FileOutputStream@35079f9c
fw_2 open:   yes
fd  :        java.io.FileDescriptor@743a95a7
fds :        1

Results, after closing stream:
fw_0:        java.io.FileOutputStream@7afd6488
fw_0 open:   no
fw_1:        java.io.FileOutputStream@70050ff8
fw_1 open:   no
fw_2:        java.io.FileOutputStream@35079f9c
fw_2 open:   no
fd  :        java.io.FileDescriptor@743a95a7
fds :        0

Results, after dereferencing stream:
fw_0:        java.io.FileOutputStream@7afd6488
fw_0 open:   no
fw_1:        java.io.FileOutputStream@70050ff8
fw_1 open:   no
fw_2:        java.io.FileOutputStream@35079f9c
fw_2 open:   no
fd  :        java.io.FileDescriptor@743a95a7
fds :        0

Results, after dereferencing descriptor:
fw_0:        null
fw_0 open:   no
fw_1:        null
fw_1 open:   no
fw_2:        null
fw_2 open:   no
fd  :        null
fds :        0

However there seems to be a push, according to this bug report - which was later reverted and deferred - to prevent FileDescriptor from keeping strong references to closeables.

So, my questions:

  • Are my assumptions concerning JDK7 correct - it behaves unlike JDK8?
  • Can I depend on the behaviour of JDK8's FileDescriptor to hold strong references to closeables, or will that be reverted in future versions of the JDK?

Edit: I've posted the follow-up question Invalidate Stream without Closing.

回答1:

Can I depend on the behaviour

You can only depend on what's specified either in the package, class, public/protected field or public/protected method javadocs or in the JLS. Java implementations are not required to use the OpenJDK's classes after all, they can reimplement the interfaces from scratch.

Looking at the implementation may be useful when some wording is unclear, but the reference implementation is not part of the specification.

You can rely on implementation-specific behavior, but in that case it should be guarded by appropriate checks, offer fallback codepaths where possible and be visibly documented.

As I understand it direct use of FileDescriptors is discouraged due to such problems. E.g. this question shows android does close the FD when the first stream owning it is closed