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