[ 
https://issues.apache.org/jira/browse/GROOVY-12579?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Scott Murphy Heiberg updated GROOVY-12579:
------------------------------------------
    Description: 
{{MethodHandle.asType}} caches its result on the handle converted. JDK 17 keeps 
that cache as a strong reference to the last conversion; JDK 18 and later keep 
a conversion strongly only when the handle's own types keep the classes of the 
new type reachable. Linking an indy call site converts handles to the call site 
type, so on JDK 17 a handle that outlives the call site keeps the classes of 
that type, and their class loader, reachable:

* a cast converts {{MethodHandles.identity(Object.class)}}, which the JDK 
shares across the JVM;
* {{catchException}} converts the JDK's shared argument-array collector to the 
type of its target, which every linked method call goes through;
* the guard and filter handles in static fields of the runtime ({{IS_NULL}}, 
{{NON_NULL}}, {{UNWRAP_METHOD}}, and the handle being cast at the end of 
linking).

A script in a {{GroovyClassLoader}} that casts to its own class is therefore 
never released on JDK 17:

{code:groovy}
import java.lang.ref.WeakReference
import groovy.transform.CompileStatic

@CompileStatic
WeakReference<ClassLoader> runDiscarded() {
    def loader = new GroovyClassLoader(this.class.classLoader)
    Script script = (Script) loader.parseClass('''
        class Holder { String label = 'h'; Holder self(Holder o) { o ?: this } }
        def held = new Holder()
        Holder holder = held
        holder.self(null).label
    ''').getDeclaredConstructor().newInstance()
    script.run()
    script = null
    loader.close()
    new WeakReference<ClassLoader>(loader)
}

@CompileStatic
boolean collected(WeakReference<ClassLoader> ref) {
    for (int i = 0; i < 100; i++) {
        System.gc()
        if (i % 50 == 10) {
            try { List<byte[]> hog = []; while (true) hog << new byte[(int) 
Math.max(1024L, Runtime.runtime.freeMemory() >> 2)] } catch (OutOfMemoryError 
ignored) { }
        }
        if (ref.get() == null) return true
        Thread.sleep(10)
    }
    false
}

println "collected: ${collected(runDiscarded())}" // -Xmx256m: false on JDK 17, 
true on JDK 21
{code}

The class loader cleanup that the integration guide documents ends with such a 
cast, so on JDK 17 it does not release the class loader of the Groovy copy it 
cleans up either.

PR: https://github.com/apache/groovy/pull/3095

  was:
{{MethodHandle.asType}} caches its result on the handle converted. JDK 17 keeps 
that cache as a strong reference to the last conversion; JDK 18 and later keep 
a conversion strongly only when the handle's own types keep the classes of the 
new type reachable. Linking an indy call site converts handles to the call site 
type, so on JDK 17 a handle that outlives the call site keeps the classes of 
that type, and their class loader, reachable:

* a cast converts {{MethodHandles.identity(Object.class)}}, which the JDK 
shares across the JVM;
* {{catchException}} converts the JDK's shared argument-array collector to the 
type of its target, which every linked method call goes through;
* the guard and filter handles in static fields of the runtime ({{IS_NULL}}, 
{{NON_NULL}}, {{UNWRAP_METHOD}}, and the handle being cast at the end of 
linking).

A script in a {{GroovyClassLoader}} that casts to its own class is therefore 
never released on JDK 17:

{code:groovy}
import java.lang.ref.WeakReference
import groovy.transform.CompileStatic

@CompileStatic
WeakReference<ClassLoader> runDiscarded() {
    def loader = new GroovyClassLoader(this.class.classLoader)
    Script script = (Script) loader.parseClass('''
        class Holder { String label = 'h'; Holder self(Holder o) { o ?: this } }
        def held = new Holder()
        Holder holder = held
        holder.self(null).label
    ''').getDeclaredConstructor().newInstance()
    script.run()
    script = null
    loader.close()
    new WeakReference<ClassLoader>(loader)
}

@CompileStatic
boolean collected(WeakReference<ClassLoader> ref) {
    for (int i = 0; i < 100; i++) {
        System.gc()
        if (i % 50 == 10) {
            try { List<byte[]> hog = []; while (true) hog << new byte[(int) 
Math.max(1024L, Runtime.runtime.freeMemory() >> 2)] } catch (OutOfMemoryError 
ignored) { }
        }
        if (ref.get() == null) return true
        Thread.sleep(10)
    }
    false
}

println "collected: ${collected(runDiscarded())}" // -Xmx256m: false on JDK 17, 
true on JDK 21
{code}

The class loader cleanup that the integration guide documents ends with such a 
cast, so on JDK 17 it does not release the class loader of the Groovy copy it 
cleans up either.


> Class loaders of discarded scripts stay reachable through method handle 
> caches on JDK 17
> ----------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12579
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12579
>             Project: Groovy
>          Issue Type: Bug
>          Components: groovy-runtime
>    Affects Versions: 4.0.33, 5.0.8, 6.0.0
>            Reporter: Scott Murphy Heiberg
>            Priority: Major
>
> {{MethodHandle.asType}} caches its result on the handle converted. JDK 17 
> keeps that cache as a strong reference to the last conversion; JDK 18 and 
> later keep a conversion strongly only when the handle's own types keep the 
> classes of the new type reachable. Linking an indy call site converts handles 
> to the call site type, so on JDK 17 a handle that outlives the call site 
> keeps the classes of that type, and their class loader, reachable:
> * a cast converts {{MethodHandles.identity(Object.class)}}, which the JDK 
> shares across the JVM;
> * {{catchException}} converts the JDK's shared argument-array collector to 
> the type of its target, which every linked method call goes through;
> * the guard and filter handles in static fields of the runtime ({{IS_NULL}}, 
> {{NON_NULL}}, {{UNWRAP_METHOD}}, and the handle being cast at the end of 
> linking).
> A script in a {{GroovyClassLoader}} that casts to its own class is therefore 
> never released on JDK 17:
> {code:groovy}
> import java.lang.ref.WeakReference
> import groovy.transform.CompileStatic
> @CompileStatic
> WeakReference<ClassLoader> runDiscarded() {
>     def loader = new GroovyClassLoader(this.class.classLoader)
>     Script script = (Script) loader.parseClass('''
>         class Holder { String label = 'h'; Holder self(Holder o) { o ?: this 
> } }
>         def held = new Holder()
>         Holder holder = held
>         holder.self(null).label
>     ''').getDeclaredConstructor().newInstance()
>     script.run()
>     script = null
>     loader.close()
>     new WeakReference<ClassLoader>(loader)
> }
> @CompileStatic
> boolean collected(WeakReference<ClassLoader> ref) {
>     for (int i = 0; i < 100; i++) {
>         System.gc()
>         if (i % 50 == 10) {
>             try { List<byte[]> hog = []; while (true) hog << new byte[(int) 
> Math.max(1024L, Runtime.runtime.freeMemory() >> 2)] } catch (OutOfMemoryError 
> ignored) { }
>         }
>         if (ref.get() == null) return true
>         Thread.sleep(10)
>     }
>     false
> }
> println "collected: ${collected(runDiscarded())}" // -Xmx256m: false on JDK 
> 17, true on JDK 21
> {code}
> The class loader cleanup that the integration guide documents ends with such 
> a cast, so on JDK 17 it does not release the class loader of the Groovy copy 
> it cleans up either.
> PR: https://github.com/apache/groovy/pull/3095



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to