RFR JDK-8185582, Update Zip implementation to use Cleaner, not finalizers

Peter Levart peter.levart at gmail.com
Mon Oct 30 10:21:05 UTC 2017

Hi Sherman,

On 10/28/2017 09:01 PM, Xueming Shen wrote:
> On 10/28/17, 10:47 AM, Peter Levart wrote:
>> Hi Florian,
>> On 10/28/17 16:16, Florian Weimer wrote:
>>> * Xueming Shen:
>>>> https://bugs.openjdk.java.net/browse/JDK-8187485
>>>> http://cr.openjdk.java.net/~sherman/8185582/webrev
>>> In ZipFile:
>>> 387                 Inflater inf = getInflater();
>>> 388                 InputStream is = new ZipFileInflaterInputStream(in, inf, (int)size,
>>> 389                                          () -> releaseInflater(inf));
>>> 390                 synchronized (streams) {
>>> 391                     streams.add(is);
>>> 392                 }
>>> Doesn't this leak the inflater if Cleaner.register() and thus the
>>> ZipFileInflaterInputStream constructor throw?
> There is reason why those are VirutalMachineError erros. It appears in 
> this situation the vm
> is in a more critical status than a inflater is getting leaked out. I 
> would assume you only catch
> these errors if/when the consequence of not catching is more severe 
> and the "thing" you are
> going to do in catch is not going to trigger more errors. Sure we do 
> that here and there in
> the libraries code for various reasons, but wonder if here is the 
> place that is worth doing that.
> That said, if this is a real concern, instead of catching all the 
> possible vm erros here and there
> to make Cleaner work correctly, a better alternative might be to go 
> back to the previous version
> to leave the zipfile/deflater cleaner configured/registerd (give up 
> the special constructor idea),
> so the deflater can get cleaned by itself in case all upstream clean 
> up mechanism failed and
> the Cleaner mechanism somehow still functions. Opinion?
> -Sherman

I wouldn't go so far as to try to deal with potential 
StackOverflowError(s), but for OOMEs, I have the following opinion:

- going back to previous version (give up the special constructor idea) 
might not, as you say, be any better. If you just look at the public 
Inflater constructor:

  121     public Inflater(boolean nowrap) {
  122         ZStreamRef ref = new ZStreamRef(init(nowrap), Inflater::end);
  123         this.zsRef = ref;
  124         this.cleanable = CleanerFactory.cleaner().register(this, 
  125     }

...OOME can be thrown after init() and before successful Cleaner 
registration in multiple places:

- method reference construction
- ZStreamRef object allocation
- Cleaner.register()

and the probability of that occurring when there is heap exhaustion is 
not any smaller.

So I'm more inclined to try to make such code more robust. There will 
always be places in such code where some native resource is 1st 
allocated, then some logic is executed which allocates objects and 
finally Cleaner.register() is attempted. By carefully programming those 
steps, a potential native resource leak in case of heap exhaustion can 
be prevented. There will usually be a way to back-out (i.e. free the 
native resource just allocated) without provoking further trouble. In 
the above example, it would be possible to do this:

      public Inflater(boolean nowrap) {
         long address = init(nowrap);
         try {
             ZStreamRef ref = new ZStreamRef(address, Inflater::end);
             this.zsRef = ref;
             this.cleanable = CleanerFactory.cleaner().register(this, ref);
         } catch (OutOfMemoryError oome) {
             throw oome;

An alternative would be to perform all steps that allocate objects 
upfront and then, as the final action, allocate the native resource and 
"inject" it into the final place:

     public Inflater(boolean nowrap) {
         ZStreamRef ref = new ZStreamRef(Inflater::end);
         this.zsRef = ref;
         this.cleanable = CleanerFactory.cleaner().register(this, ref);

Concerning ZipFile, I would make releaseInflater() more robust, because 
it is easy:

  private void releaseInflater(Inflater inf) {
    try {
        synchronized (inflaterCache) {
    } catch (OutOfMemoryError e) {
        // Inflater.end() does no allocation
        throw e;

For lines 387 ... 392, I would simply do this:

Inflater inf = getInflater();
InputStream is;

try {
     is = new ZipFileInflaterInputStream(in, inf, (int)size, () -> 
} catch (OutOfMemoryError e) {
     // calling releaseInflater() is not sensible here, because it 
allocates and will
     // probably result in Inflater.end() anyway...
     throw e;

try {
     synchronized (streams) {
} catch (OutOfMemoryError e) {
     // ZipFileInflaterInputStream.close() does not allocate, 
IOException is never thrown
     try { is.close(); } catch (IOException ignore) {}
     throw e;

Simply saying that "vm is in a more critical status than a inflater is 
getting leaked out" is, in my opinion, covering the problem under the 
rug. The VM is not in critical state - the program is. VM is robust 
enough to recover from OOMEs. The program might be in critical status 
(i.e. in inconsistent state) partly because of not accounting for such 
OOME situations. By taking care of them, the program has a better chance 
of recovering from such situation(s).

Handling native resources is one place where I think it is beneficial to 
complicate things in order to make native resource leaks (im/less 
)possible. The other such place is synchronization primitives. We must 
admit that finalization does have this benefit that it makes it hard to 
construct code that would allocate the native resource before cleanup 
registration (which is performed as part of Object.<init>, while logic 
to allocate native resource is usually invoked from subclass 
constructor). To achieve the same robustness with Cleaner API, one has 
to be careful to either perform registration upfront and then allocate 
native resource or arrange for back-out in case of trouble.

Regards, Peter

More information about the core-libs-dev mailing list