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

Luke Hutchison luke.hutch at gmail.com
Wed Oct 10 09:10:01 UTC 2018


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.

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 jdk-dev mailing list