Finalizer being run while class still in use (escape analysis bug)

David Holmes david.holmes at oracle.com
Wed Oct 10 09:39:03 UTC 2018


<dropped jdk-dev >

On 10/10/2018 7:10 PM, Luke Hutchison wrote:
> Hi Volker,
> 
> Thanks for the standalone example, and I agree that your example does not
> violate the spec, because after the last reference to "eaf" is used in "for
> (Integer i : eaf.al)", the eaf object is free to be garbage collected.
> 
> However, this is not an example of the behavior I was illustrating, because
> the for loop is running within a *static* method, main. I specifically
> mentioned non-static methods, so that a running method can access "this" at
> any time (and "this" will never "go out of scope"). My point was that if
> any *non-static* method of an object instance is currently running (i.e.
> whenever an object is "currently serving as an invocation target"), that
> object should never be considered unreachable. I believe that I did not
> misread the spec, and that this is covered by the wording, "A reachable
> object is any object that can be accessed in any potential continuing
> computation from any live thread", since if a non-static method of an
> object is currently running, that object not only "can be accessed" by a
> live thread, it *is* currently being accessed by a live thread.

This unexpected scenario is exactly what the part of the JLS I 
referenced is referring to. An executing method need not reference 
"this" and so the instance can be considered unreachable and hence 
finalizable.

David
-----


> In fact, if you minimally modify your code example to put the loop inside a
> non-static method, this program no longer throws
> ConcurrentModificationException, illustrating that at least for the example
> you gave, the compiler and Hotspot are doing the right thing. The following
> code will iterate until OutOfMemoryError.
> 
> =================================================
> import java.util.ArrayList;
> 
> public class EAFinalizer {
> 
>    private static ArrayList<Integer> cal = new ArrayList<Integer>(10_000);
>    static {
>      for (int i = 0; i < 10_000; i++) cal.add(i);
>    }
>    private static ArrayList<Object> sink = new ArrayList<Object>();
> 
>    private ArrayList<Integer> al;
> 
>    public EAFinalizer(ArrayList<Integer> al) {
>      this.al = new ArrayList<Integer>(al);
>    }
> 
>    public static void main(String[] args) {
>      while(true) {
>        EAFinalizer eaf = new EAFinalizer(cal);
>        System.err.println("Iterating " + eaf);
>        eaf.iterate();
>        // System.err.println("Iterated " + eaf);
>      }
>    }
> 
>    private void iterate() {
>      for (Integer i : al) {
>        // Create some garbage to trigger GC and finalization
>        sink.add(new int[i.toString().length() * 1_000]);
>      }
>    }
> 
> 
>    @Override
>    protected void finalize() throws Throwable {
>      System.err.println("Finalizing " + this);
>      al.clear();
>    }
> }
> =================================================
> 
> The reason I didn't give a minimal testcase like the above for the behavior
> I am seeing is that I have not been able to duplicate this behavior except
> by running the actual code that was reported to me by the user.
> 
> Thanks,
> Luke
> 
> 
> 
> 
> On Wed, Oct 10, 2018 at 1:32 AM Volker Simonis <volker.simonis at gmail.com>
> wrote:
> 
>> Hi Luke,
>>
>> I think the behaviour you see in not related to Escape Analysis at
>> all. It should also occur if you run with -XX:-DoEscapeAnalysis (note
>> that -XX:+DoEscapeAnalysis is the default). I also think that this
>> behaviour doesn't break the Java specification. You have to carefully
>> read the section cited by David.
>>
>> Consider the following small, stand alone program which should
>> simulate your use case quite well:
>>
>> =================================================
>> import java.util.ArrayList;
>>
>> public class EAFinalizer {
>>
>>    private static ArrayList<Integer> cal = new ArrayList<Integer>(10_000);
>>    static {
>>      for (int i = 0; i < 10_000; i++) cal.add(i);
>>    }
>>    private static ArrayList<Object> sink = new ArrayList<Object>();
>>
>>    private ArrayList<Integer> al;
>>
>>    public EAFinalizer(ArrayList<Integer> al) {
>>      this.al = new ArrayList<Integer>(al);
>>    }
>>
>>    public static void main(String[] args) {
>>      while(true) {
>>        EAFinalizer eaf = new EAFinalizer(cal);
>>        System.err.println("Iterating " + eaf);
>>        for (Integer i : eaf.al) {
>>          // Create some garbage to trigger GC and finalization
>>          sink.add(new int[i.toString().length() * 1_000]);
>>        }
>>        // System.err.println("Iterated " + eaf);
>>      }
>>    }
>>
>>    @Override
>>    protected void finalize() throws Throwable {
>>      System.err.println("Finalizing " + this);
>>      al.clear();
>>    }
>> }
>> ==================================================
>>
>> Running this program (even with -XX:-DoEscapeAnalysis) will give you:
>>
>> $ java -XX:-DoEscapeAnalysis EAFinalizer
>> Iterating EAFinalizer at 5ecddf8f
>> Iterating EAFinalizer at 3f102e87
>> Iterating EAFinalizer at 27abe2cd
>> Iterating EAFinalizer at 5f5a92bb
>> Iterating EAFinalizer at 6fdb1f78
>> Iterating EAFinalizer at 51016012
>> Iterating EAFinalizer at 29444d75
>> Iterating EAFinalizer at 2280cdac
>> Finalizing EAFinalizer at 2280cdac
>> Finalizing EAFinalizer at 29444d75
>> Exception in thread "main" java.util.ConcurrentModificationException
>>      at
>> java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
>>      at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
>>      at EAFinalizer.main(EAFinalizer.java:21)
>>
>> The problem with the program is that in the JIT compiled version of
>> "test()" the EAFinalizer object "eaf" is not reachable in the for loop
>> any more (because the JIT compiler is smart enough to detect that
>> after extracting the AraryList "al" from "eaf", "eaf" is no longer
>> needed). So when a GC is being triggered in the loop because the
>> allocation of the integer array fails, the VM can safely garbage
>> collect the "eaf" object and call its finalizer (notice that within
>> the loop we only reference "eaf"s ArrayList field "al", but not the
>> containing EAFinalizer object).
>>
>> You can easily fix this little toy example by uncommenting the second
>> System.println statement in the "main()" method. This outputs "eaf"
>> and thus keeps it alive. You can also run the program in interpreted
>> mode ("-Xint") in which case you won't see the problem as well. That's
>> because the Interpreter doesn't optimizes as eagerly as the JIT
>> compiler and keeps "eaf" alive for the whole life time of the
>> corresponding method invocation. That's actually what most people
>> naivly expect  to be the "guaranteed" life time of an object, but as
>> the specification tells us, that's not required by the standard. If it
>> would, the JIT would miss a whole lot of optimization opportunities.
>>
>> Regards,
>> Volker
>>
>>


More information about the hotspot-dev mailing list