[External] : Re: Classes used in method body are loaded lazily or eagerly depending on method return type

Raffaello Giulietti raffaello.giulietti at oracle.com
Wed May 24 14:34:24 UTC 2023


As mentioned in my previous email, if you move the member class 
ChildClass out of TestLoading (out of the nest), and make it a top-level 
class like

     public class ChildClass extends TestLoading.BaseClass {
     }

and change

     URL classFileB = 
TestLoading.class.getResource(TestLoading.class.getSimpleName() + 
"$ChildClass.class");

to

     URL classFileB = 
TestLoading.class.getResource(ChildClass.class.getSimpleName() + ".class");

rebuild everything and run, nothing is thrown.

     deleting: <path-to>/ChildClass.class
     loading: TestLoading$ObjectReturner...
     loading: TestLoading$BaseClassReturner...

I can't see any substantial difference, except that the nest rooted at 
TestLoading lacks a member in the original setup and lacks nothing in 
this setup.

What's an explanation for this difference?


Greetings
Raffaello


On 2023-05-24 00:35, Remi Forax wrote:
> 
> 
> ----- Original Message -----
>> From: "David Holmes" <david.holmes at oracle.com>
>> To: "Raffaello Giulietti" <raffaello.giulietti at oracle.com>, "core-libs-dev" <core-libs-dev at openjdk.org>
>> Sent: Wednesday, May 24, 2023 12:23:24 AM
>> Subject: Re: Classes used in method body are loaded lazily or eagerly depending on method return type
> 
>> On 24/05/2023 12:50 am, Raffaello Giulietti wrote:
>>> I think the problem here is that you are deleting a class in a nest.
>>>
>>> During the verification of BaseClassReturner.getObject(), access control
>>> (JVMS, 5.4.4, second half) determines that the nest is broken, as
>>> ChildClass is not present anymore.
>>
>> Not sure access control gets involved at this stage of the verification
>> process. But in any case turning on logging does not show anything
>> related to nestmates happening between BaseClass and ChildClass. It
>> seems to just be the resolution of the return type during verification
>> of the method, that causes the loading of ChildClass and the subsequent
>> CNFE if it has been removed.
>>
>>> If you move ChildClass out of TestLoading so that it becomes a top-level
>>> class extending TestLoading.BaseClass, and if you adapt the line that
>>> initializes the local var classFileB to refer to the new location, the
>>> code will not throw, despite ChildClass being deleted.
>>
>> My simplified test shows it still throws when verifying BaseClassReturner.
> 
> Nestmate checking is done lazily, so if you do not call a method/access a field of a nested class, the VM should not trigger a class loading.
> 
> Moreover, if you test with Java 8 (nestmates were introduced in Java 11), it failed too.
> That's another clue that the error is not related to nestmates checking.
> 
>>
>>
>> Cheers,
>> David
> 
> regards,
> Rémi
> 
>>
>>>
>>> Greetings
>>> Raffaello
>>>
>>>
>>>
>>> On 2023-05-23 13:20, Сергей Цыпанов wrote:
>>>> Hello,
>>>>
>>>> originally this question was asked here:
>>>> https://urldefense.com/v3/__https://stackoverflow.com/q/76260269/12473843__;!!ACWV5N9M2RV99hQ!Mb5nhj7EbuftWzF7s4GX9auUZZlyPyCUnLs64c4mkmSGJm4pw0CNgRzQR5wOYuApyE_kHSAnVxGyTM9PHz5StCppGw$ ,
>>>> the code sample for reproduction will be put below or can be found via
>>>> the link
>>>>
>>>> The issue is about eager/lazy loading of a class depending on method
>>>> return type.
>>>> If one runs the code below with Java 11-19 it will fail with
>>>> NoClassDefFoundError (this is expected as delete class file for
>>>> ChildClass):
>>>>
>>>> java.lang.NoClassDefFoundError: org/example/TestLoading$ChildClass
>>>>      at java.base/java.lang.Class.forName0(Native Method)
>>>>      at java.base/java.lang.Class.forName(Class.java:390)
>>>>      at java.base/java.lang.Class.forName(Class.java:381)
>>>>      at org.example.TestLoading.loadMyClass(TestLoading.java:29)
>>>>      at org.example.TestLoading.main(TestLoading.java:23)
>>>> Caused by: java.lang.ClassNotFoundException:
>>>> org.example.TestLoading$ChildClass
>>>>      at
>>>> java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
>>>>      at
>>>> java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
>>>>      at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
>>>>      ... 5 more
>>>>
>>>> As of Java 20 chapter 12.4.1 of JLS states:
>>>> ---------------------------------------------------------
>>>> A class or interface T will be initialized immediately before the
>>>> first occurrence of any one of the following:
>>>>
>>>> - T is a class and an instance of T is created.
>>>> - a static method declared by T is invoked.
>>>> - a static field declared by T is assigned.
>>>> - a static field declared by T is used and the field is not a constant
>>>> variable (§4.12.4).
>>>>
>>>> When a class is initialized, its superclasses are initialized (if they
>>>> have not been previously initialized), as well as any superinterfaces
>>>> (§8.1.5) that declare any default methods (§9.4.3) (if they have not
>>>> been previously initialized).
>>>> Initialization of an interface does not, of itself, cause
>>>> initialization of any of its superinterfaces.
>>>> A reference to a static field (§8.3.1.1) causes initialization of only
>>>> the class or interface that actually declares it, even though it might
>>>> be referred to through the name of a subclass, a subinterface, or a
>>>> class that implements an interface.
>>>> Invocation of certain reflective methods in class Class and in package
>>>> java.lang.reflect also causes class or interface initialization.
>>>> A class or interface will not be initialized under any other
>>>> circumstance.
>>>> ---------------------------------------------------------
>>>> With the code snippet we see that calling
>>>> Class.forName(ObjectReturner.class.getName()) succeeds and
>>>> Class.forName(BaseClassReturner.class.getName()) fails even though
>>>> both declare returning an instance of ChildClass.
>>>> This failure is unexpected as in the code below we don't fulfill any
>>>> requirement for class loading as of JLS 12.4.1, but the JVM still
>>>> tries to load the class.
>>>>
>>>> I suspect it might be related to class file validation and/or
>>>> security, because when we run the code with -Xlog:class+init there's a
>>>> reference to LinkageError in loading log:
>>>>
>>>> loading: org.example.TestLoading$BaseClassReturner...
>>>> [0.277s][info][class,init] Start class verification for:
>>>> org.example.TestLoading$BaseClassReturner
>>>> [0.277s][info][class,init] 771 Initializing
>>>> 'java/lang/ReflectiveOperationException'(no method) (0x0000000800004028)
>>>> [0.277s][info][class,init] 772 Initializing
>>>> 'java/lang/ClassNotFoundException'(no method) (0x0000000800004288)
>>>> [0.277s][info][class,init] 773 Initializing
>>>> 'java/lang/LinkageError'(no method)
>>>> (0x00000008000044f8)                                  <----
>>>> [0.277s][info][class,init] 774 Initializing
>>>> 'java/lang/NoClassDefFoundError'(no method) (0x0000000800004758)
>>>> [0.277s][info][class,init] Verification for
>>>> org.example.TestLoading$BaseClassReturner has exception pending
>>>> 'java.lang.NoClassDefFoundError org/example/TestLoading$ChildClass'
>>>> [0.277s][info][class,init] End class verification for:
>>>> org.example.TestLoading$BaseClassReturner
>>>>
>>>>
>>>> So I've got three questions about this:
>>>> - Does class loading depend on method's return type?
>>>> - Which part of JLS/JVM spec describes eager class loading in this case?
>>>> - Could one point out the particular piece of the VM code responsible
>>>> for class loading in this case?
>>>>
>>>> Regards,
>>>> Sergey Tsypanov
>>>>
>>>> Code snippet for reproduction:
>>>>
>>>> public class TestLoading {
>>>>
>>>>     public static void main(String[] args) throws Exception {
>>>>       Class.forName(BaseClass.class.getName());
>>>>       URL classFileB =
>>>> TestLoading.class.getResource(TestLoading.class.getSimpleName() +
>>>> "$ChildClass.class");
>>>>       if (classFileB != null) {
>>>>         if (!"file".equals(classFileB.getProtocol())) {
>>>>           throw new UnsupportedOperationException();
>>>>         }
>>>>         Path path = new File(classFileB.getPath()).toPath();
>>>>         System.out.println("deleting: " + path);
>>>>         Files.delete(path);
>>>>       }
>>>>
>>>>       loadMyClass(ObjectReturner.class.getName());
>>>>       loadMyClass(BaseClassReturner.class.getName());
>>>>     }
>>>>
>>>>     private static void loadMyClass(String name) {
>>>>       System.out.println("loading: " + name + "...");
>>>>       try {
>>>>         Class.forName(name);
>>>>       } catch (Throwable e) {
>>>>         e.printStackTrace(System.out);
>>>>       }
>>>>     }
>>>>
>>>>     public static class BaseClass {
>>>>     }
>>>>
>>>>     public static class ChildClass extends BaseClass {
>>>>     }
>>>>
>>>>     public static class ObjectReturner {
>>>>       public Object getObject() {
>>>>         return new ChildClass();
>>>>       }
>>>>     }
>>>>
>>>>     public static class BaseClassReturner {
>>>>       public BaseClass getObject() {
>>>>         return new ChildClass();
>>>>       }
>>>>     }
>>>> }


More information about the core-libs-dev mailing list