JDK-8175883 : bytecode generated for the enhanced for loop may block memory garbage collecting
  • Type: Enhancement
  • Component: tools
  • Sub-Component: javac
  • Affected Version: 9
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2017-02-26
  • Updated: 2018-04-03
  • Resolved: 2017-11-20
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 10
10 b33Fixed
Related Reports
Relates :  
Relates :  
Description
A DESCRIPTION OF THE REQUEST :
If a "for" loop iterates over a large list/array it keeps a temporary (implicit auto-generated) reference to the memory, and this memory might be locked for garbage collecting in the next steps which causes OutOfMemoryError.

The problem might happen if one uses:
  - a large list or arrays which allocates, for example, more than half of available heap space
  - a new list/array should be allocated immediately after the "for" loop and the old one expected to be discarded

JUSTIFICATION :
As soon as explicit reference to any (large) memory block is released it should be available for collecting as a garbage, otherwise an application may fall to OutOfMemoryError.

I found this example when i needed to process two large XML files: after parsing the first one i fell to OutOfMemoryError when i tried to parse the second one, and i discarded on declared references to the previously parsed XML file. Note, if i avoid "for" loop and use explicit "Iterator-while-hasNext-next-iterator=null" construction - the application works fine.

In the examples below i simplified the issue demonstration.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No OutOfMemoryError when i don't have any explicit references to allocated memory.

As soon as "for" block is finished the platform (not sure javac or JVM) must release those auto-generated implicit reference and make the memory block available for garbage collecting.
ACTUAL -
The program falls to OutOfMemoryError.

The program keeps it's own implicit auto-generated reference to the memory block till next implicit usage. If one tries to allocate a memory before that "next implicit usage" - OutOfMemoryError occurs.

---------- BEGIN SOURCE ----------
/**
* See more examples here: https://bitbucket.org/snippets/radistao/geerj
* <p>
 * Run with:
 * <pre>javac IteratorInOneScope.java && java  IteratorInOneScope</pre>
 *
 * and try two cases:<ul>
 *     <li>with the <b>for</b> loop: falls to <i>java.lang.OutOfMemoryError: Java heap space</i></li>
 *     <li>without <b>for</b> loop: completes successfully</li>
 * </ul>
 * <p>
 * <b>Conclusion</b>: <b>for</b> loop created automatically  a (temporary) reference to a memory block,
 * but has not released it after usage before the next memory allocation.
 */
public class IteratorInOneScope {

    private static final int HALF_OF_MEMORY = (int) (Runtime.getRuntime().maxMemory() * 0.5);

    public static void main(String[] args) {
        byte[] data = new byte[HALF_OF_MEMORY];

        for (byte b : data); // <-- if you comment this line - the application finished successfully
        data = null; // this expects to discard reference -> allow to release the memory

        byte[] data2 = new byte[HALF_OF_MEMORY]; // the memory can't be allocated second time, if the "for" loop statement above is used

        System.out.println("Success");
    }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
See more examples and workarounds in this snippet:
https://bitbucket.org/snippets/radistao/geerj

1. If you avoid those "for" loops - it works fine (no OutOfMemoryError)

2. If you use explicit code block with final nullify, like 
    Iterator itr = list.iterator();
    while (itr.hasNext()) {
        Object t = itr.next();
    }
    itr = null; // <-- saves our souls
  
3. If you make intermediate operations between the "for" loop and the next memory allocation - it may work fine, but guess there is not guarantee that exactly the same reference will be reused in those intermediate operation (see example https://bitbucket.org/snippets/radistao/geerj#file-IteratorInOtherScope.java where "System.gc()" or "double d = Math.random()" is used before second allocation)

4. The first open investigation started here
http://stackoverflow.com/questions/42403347/java-for-statement-implementation-prevents-garbage-collecting
as the explantation why those implicit reference occur. 

5. Tested on versions from 1.8.0_111 and 1.8.0_121, macos and windows, + 3 online java compilers (like this one: https://ideone.com/GJ1qoI )


Comments
comment from Dan: JLS 14.14.2 is only concerned with semantics of the statement, not garbage collection behavior. Compilers are free to generate whatever bytecode they want that produces the specified behavior. So, if javac wants to set some unused locals to null, it's free to do so. No spec change necessary, and it would be a mistake to include it in the spec, because it doesn't impact the statement's semantics.
15-11-2017

A reminder that a spec change should have a CSR before being pushed.
15-11-2017

Any change in behavior for javac should be coordinated with JLS 14.14.2.
14-11-2017

Moved to tools->javac
28-02-2017

Yes, on bytecode level there's a temp local which keeps another reference to the array: 0: getstatic #2 // Field HALF_OF_MEMORY:I 3: newarray byte 5: astore_0 <==== array ref in slot #0 6: aload_0 7: astore_1 <==== array ref in slot #1 8: aload_1 9: arraylength 10: istore_2 11: iconst_0 12: istore_3 13: iload_3 14: iload_2 15: if_icmpge 29 18: aload_1 19: iload_3 20: baload 21: istore 4 23: iinc 3, 1 26: goto 13 29: aconst_null 30: astore_0 <== nulls slot #0 31: getstatic #2 // Field HALF_OF_MEMORY:I 34: newarray byte 36: astore_1 37: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 40: ldc #4 // String Success 42: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 45: return
28-02-2017

This may be a javac issue: "If a "for" loop iterates over a large list/array it keeps a temporary (implicit auto-generated) reference to the memory," According the spec this should not happen: The enhanced for statement is equivalent to a basic for statement of the form: for (I #i = Expression.iterator(); #i.hasNext(); ) { {VariableModifier} TargetType Identifier = (TargetType) #i.next(); Statement } #i is an automatically generated identifier that is distinct from any other identifiers (automatically generated or otherwise) that are in scope (��6.3) at the point where the enhanced for statement occurs. Need to look at the bytecode generated to expand the for-each loop and see how garbage retention might occur.
28-02-2017

The test fails with -Xint as well. So, I conclude it's not related to compilers. Handing back to runtime.
28-02-2017

Compiler should handle initial triage since this is related to loops.
28-02-2017

This is an issue reproducible both on 8 and 9 == =8u121= -sh-4.1$ /opt/java/jdk1.8.0_121/bin/javac IteratorInOneScope.java -sh-4.1$ /opt/java/jdk1.8.0_121/bin/java IteratorInOneScope Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at IteratorInOneScope.main(IteratorInOneScope.java:25) =9 ea b157= -sh-4.1$ /opt/java/jdk-9_ea-157/bin/java IteratorInOneScope Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at IteratorInOneScope.main(IteratorInOneScope.java:25) Commenting out for loop " for (byte b : data)" makes program runs fine.There is some issue the program keeps it's own implicit auto-generated reference to the memory block till next implicit usage and its memory is being locked that causing OOM
27-02-2017