JDK-8334324 : Method::load_signature_classes initiates spontaneous class loading
  • Type: Bug
  • Component: hotspot
  • Sub-Component: runtime
  • Affected Version: 24
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • Submitted: 2024-06-14
  • Updated: 2024-06-24
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.
Other
tbdUnresolved
Related Reports
Relates :  
Description
All signature classes are eagerly loaded when a method is submitted for JIT-compilation. However, Method::load_signature_classes doesn't persist any failures happening during class loading.

src/hotspot/share/oops/method.cpp:
      // load everything, including arrays "[Lfoo;"
      Klass* klass = ss.as_klass(SignatureStream::ReturnNull, THREAD);
      // We are loading classes eagerly. If a ClassNotFoundException or
      // a LinkageError was generated, be sure to ignore it.
      if (HAS_PENDING_EXCEPTION) {
        if (PENDING_EXCEPTION->is_a(vmClasses::ClassNotFoundException_klass()) ||
            PENDING_EXCEPTION->is_a(vmClasses::LinkageError_klass())) {
          CLEAR_PENDING_EXCEPTION;

It's not a recent change in behavior. The logic was introduced long time ago (as part of JDK-4963485). But there's an ongoing discussion about whether JVM is allowed to ignore errors during eager class loading in presence of intermittent failures. (In other words, should subsequent loading attempts report the same error?)

And there is a larger issue here that demands some clarification.

When the VM resolves a CONSTANT_Class entry there are clear rules about recording the outcomes, including memoizing errors.  The location for that recording is CONSTANT_Class entry itself, as situated in the constant pool that defines it.  Several distinct constant pools can store different outcomes, but each entry must be consistent with itself.  Other factors such as class loader constraints and the finality of the “define-class” operation, tend to enforce consistency among queries scattered across multiple constant pools.

The missing bit here, though, is what happens when a class must be loaded apart from resolution of a CONSTANT_Class entry.  Several parts of the VM perform such loading.  Let’s call it “non-resolving loading”.  The case of load-signature-classes is non-resolving loading.  The verifier does it as well, and so do reflective APIs (when they reify descriptor types as classes, or load annotations).  Unlike the verifier, load-signature-classes is not in the VM spec directly, so (to the extent that it is actually correct!) it must be justified indirectly from the spec.

Another missing bit here, which overlaps with non-resolving loading, is the fact that the VM sometimes loads some class on its own “time table”, and specifically before the application requests the loading of that same class directly.  A direct request to load a class is a resolution of a CONSTANT_Class entry, which almost always corresponds to some identifiable instruction in the source code of the application.  But non-resolving loading actions can correspond only indirectly to the application code.  Reflection and verifier operations are somewhat indirect in this way, and the boot sequence of the VM (which loads a bunch of unpredictable classes in java.base) is completely indirect, since it happens before the application main is entered.

We need a clarification of the spec that allows for those indirect load events, and in particular which clearly defines the amount of liberty that the VM has in reordering such events.  Let’s call a VM class load event that occurs on the VM’s own "time table”, subject to the permissions given by the VM spec., a “spontaneous load”.  It is not spontaneous from the VM’s point of view, because the VM obviously has a good reason for scheduling the load.  But it is spontaneous from the point of view of the application and its author.

In the above terms, the loading of signature classes, which includes the subsequent final definition of the class and/or discarding of an error, is both non-resolving and also spontaneous.

Returning to the VM spec., class loads of every kind are required explicitly in some cases, and tacitly permitted (allowed implicitly) in others.  The load-signature-classes operation (to the extent that it is not a bug!) is almost certainly the latter.

Tacit permission usually looks something like this:

- The VM decides to shift the loading of a class to a time earlier than its first use by the application.
- From the application point of view, the class appears to be loaded at the moment of first use, except that it happens “very quickly”.
- This may be called an “as if” optimization:  The application-visible effects are "as if" the VM had loaded the class, even if the VM and the OS “know” it happened earlier.
- The VM ensures that the class loader used to load the class has no application-visible side effects.
- (Alternatively, some class loader effects, visible to the application, may be declared due to spontaneous loads.  But it’s probably better just to not optimize in the presence of an unknown class loader.)
- The application agrees not to snoop the VM or OS to try to detect when the relevant class file was accessed, or when the VM actually executed its class-loading logic.
- (Some “snooping” action, such as JVMTI or reflectively opening up private fields of VM implementation classes, might be viewed as important enough to allow to interfere with the “as if” optimization, causing the optimization to be disabled.  But some snooping will always be “below the virtual metal": you will see things your eyes won’t believe, and get over it.  This might not apply to JVMTI but surely applies to OS-level tracing and low-level VM logging operations, which can always expose any tricks that the VM is playing.)

One possible way to resolve questions with load-signature-classes is to reframe it as “find-signature-classes”, and then allow "as if” optimizations to shift the loads, as long as there is tacit permission to do so.

This might conceivably cause performance regressions with user-defined class loaders.  If so, the debt of fixing such problems might be moved to Project Leyden, which is constituted to create new rules for selectively shifting operations even when an “as if” rule does not apply, due to application-visible effects.  But, no such new rules are needed in the most important cases, of types which are hardwired into the JDK and/or loaded via class loaders whose behavior is known to the VM.  In those cases, the VM tacitly permits the shifted load, by an appeal to an “as if” optimization.

From this viewpoint, a “find signature classes” operation would be unobjectionable.  It can be narrated as “we are opportunistically finding classes that we know already exist.”  And  it can be extended with an as-if optimization: “…already exist or, by appeal to an as-if optimization, can eventually exist”.

Because it mentions loading, a “load signature classes” has to be footnoted more explicitly: “Any loading done here is justified as an as-if optimization”.  Maybe eventually the footnote can be: “The current Leyden rules allow us to issue a load here, as well as pick up classes already in the system dictionary”.

If other spec experts agree with the above reasoning, a suggested fix for the specific issue is to rename the JIT query as find_signature_classes, and go through the spontaneous loading logic putting in gating logic that refrains from touching (invoking methods on) class loaders unknown to the JDK.  And perhaps additional gating logic that refrains from touching even the built-in class loaders, if some JVMTI feature is enabled that could (a) observe their behavior or (b) change their behavior.
Comments
I've seen the "when can I load classes?" issue come up repeatedly in the last few months. I've captured the JVMS piece here: JDK-8334888.
24-06-2024

> Here's the thing - the signature classes don't ever need to be loaded to execute the program. That seems somewhat of an edge case though. "normally" you would not only ever call a method with null parameters. > That's both surprising and outside the allowed rules of the spec. How does anyone notice that they eventually got loaded such that they are surprised by it? > And once that kind of spurious loading becomes legitimized ... I think this is greatly overstating the impact of this particular issue. Most users would be surprised the types did not have to be loaded more eagerly.
24-06-2024

> But we are not talking about "arbitrary" loading. The types referenced in method signatures are an essential requirement for execution of the code of a class. At some point those types must be resolved and thus loaded, linked and initialized. Here's the thing - the signature classes don't ever need to be loaded to execute the program. Take this as an example: ``` class Example { public static void main(String[] args) { for (int i = 0; i < 1000000000; i++) { m(i, null, null); } } void m(int i, SomeClass c, OtherClass o) { return; } } ``` In Example, we have a method `m` that is called enough times to get compiled by C2. It takes three parameters, of which two `SomeClass c` & `OtherClass o` are never used. The program never allocates an instance of either class. Neither class appears in a ConstantPool entry. Verification doesn't need to load them. So we can run this program in the interpreter with either an eager or lazy linking strategy and never need to load SomeClass or OtherClass. After some period of time, C2 decides to compile the method and causes the classes from the signature to be loaded. That's both surprising and outside the allowed rules of the spec. And once that kind of spurious loading becomes legitimized, we have a problem as it makes it harder for users or implementors to reason about legal behaviour of the system.
19-06-2024

On a slight tangent, given the change to use tiered compilation, is Method::load_signature_classes still useful? My assumption is that loading the classes early helps the JIT to avoid deoptimization due to viewing paths as cold as the classes needed aren't loaded yet. With tiered compilation, will we have loaded the required classes - the ones the program will actually use - by the time we attempt the C2 compile? Is there still a practical need for this particular spurious loading mechanism?
19-06-2024

> doesn't mean the specs are blind to each other. The two specs are obviously connected as they deal with aspects of the Java platform. But the JVMS does not depend on the JNI spec for its own existence or to permit anything in JVMS. JVM creation does not have to happen via the JNI API - that is simply an API to allow external code to create a JVM. > the JVMS goes into great deal to say that execution is driven by the main method I assume you meant "great detail" but it really doesn't go into great detail at all, it just makes a very broad, general statement, that indicates how execution continues based on what the main class does. But there is still a ton of stuff going on that was already initiated before the main class was loaded and which can continue to execute and trigger actions unrelated to the main class. The JVMS is not a formal specification. Some parts are more formal looking than others. Some text, like 5.2, is is very generic and descriptive. Some key things about how the JVM has to operate are not specified in the JVMS itself but in the class libraries - ie. classloaders and their interactions and relationships. The JVMS is aware of two kinds of loaders (boot and user-defined) but not about any other relationships e.g. it doesn't know about the system loader or the app loader (they are just examples of user-defined loaders). JVM TI is another area where the interaction of the two spec is unclear/under-specified e.g. class retransformation is not known about by JVMS Ch 5 and working out the exactly how it interacts with the rules of 5.3 etc can lead to some surprising situations. > Allowing arbitrary loading ... But we are not talking about "arbitrary" loading. The types referenced in method signatures are an essential requirement for execution of the code of a class. At some point those types must be resolved and thus loaded, linked and initialized. I agree that the JVMS does not make it clear that these signature types can be eagerly loaded, but I disagree that it categorically prohibits it from occurring.** Also note that running with -Xcomp would be non-complaint under your strict view of 5.2 as the compiler loads things in the order it needs to. ** There are always two ways of looking at a specification in terms of what is allowed and not allowed: 1. I can only do what the spec specifically allows; or 2. I can do anything the spec doesn't specifically prohibit.
19-06-2024

The bug was intended to cover all concerning scenarios related to eager loading triggered by Method::load_signature_classes. I erroneously narrowed the scope to failing scenarios and error preserving aspect. Sorry for the confusion. Adjusted the summary to reflect the state of the discussion.
18-06-2024

My main “comment” here is to widen the issue description, to define "non-resolving class loading” and also “spontaneous class loading”, and connect them to the behavior of load_signature_classes. I would support a tightening of the behavior of that optimization to exclude the invoking of methods on class loaders which have too much coupling to application visible side effects. We may already do this, but perhaps the code needs to be reviewed. I am fond of spontaneous class loading, but I also agree with Dan H. that we need to clarify its basis in the VM spec. And in some cases, we might be spontaneously loading something we shouldn’t, given our best understanding of the spec; that would be a bug to be fixed.
18-06-2024

Also note this issue is not about the legality of doing the eager loading, it is about the handling of errors that might arise during that eager loading. And the JVMS is quite specific about when such errors can be presented to the application.
18-06-2024

> the JVM must have permission somewhere (currently from the Invocation API) to load those required classes before main Sorry to be pedantic but no. The JVMS does not require or depend upon the JNI specification for anything. How a JVM comes into existence and reaches a point where it can load and execute the main class is not specified in detail. It is covered broadly by the complete first paragraph of 5.2: "The Java Virtual Machine starts up by creating an initial class or interface using the bootstrap class loader (§5.3.1) or a user-defined class loader (§5.3.2). The Java Virtual Machine then links the initial class or interface, initializes it, and invokes the public static method void main(String[]). The invocation of this method drives all further execution." --- But the details of startup are not given in detail in JVMS. The VM must create a boot-strap loader or a user-defined loader to create the initial class or interface. But JVMS does not specifiy how that has to happen. The Class library API's describe how classloaders interact and are arranged at VM startup. The semantics of class loading in JVMS imply how some classes must be loaded to load a given class i.e. load supers first. Class initialization semantics imply we must statically init a class before we can create an instance and so <clinit> can then drive arbitrary execution and loading and initialization of other classes. None of the class initialization is specified in detail for any given class - it is an implementation detail. So the gross simplification of the the first sentence of 5.2 hides a myriad of details that result in hundreds of classes being loaded, and numerous threads being created before we even get to the point where the VM can create the main class. All of which is implementation detail and none of which is actually "driven" by the execution of the main method. So you cannot use 5.2 to say that no other classes can be loaded before the main class. Whether they are JDK classes, or classes referenced from the main class (and the transitive closure thereof). As long as the rules that are specified in JVMS are followed the VM has considerable flexibility in how eagerly, or lazily, it can perform various actions, including loading and linking.
18-06-2024

> > That's covered in the spec for the Invocation API > > The JNi spec is irrelevant here. The JVMS does not require nor depend on > JNI existing. You are appealing to JVMS 5.2 to severely restrict what classes > can be loaded by the VM, but 5.2 is a gross simplification in that regard. JVMS 5.2 talks about execution from main onwards. The only spec we have that talks about what happens before main is the JNI spec. Logically, a JVM must load a certain set of classes - Class, Object, String, Thread, etc, and their dependencies - to be able to execute main so the JVM must have permission somewhere (currently from the Invocation API) to load those required classes before main " drives all further execution". > > Method signatures are not actually involved in any of those activities. > > At what point would you say that the types in a method signature get resolved? Never. There's no reason for the a method signature to be resolved and nothing in the spec that talks about resolving method signatures. The VM doesn't care if the types named in the signature are resolved in the current class loader or not. Some of the classes from signatures may be loaded by the VM to satisfy verification and others to check class loading constraints but signatures are not a resolvable constant. For verification-related loading, that is defined to happen prior to linkage so we can't use that to drive loading signature classes later.
18-06-2024

> > the JVM must have permission somewhere (currently from the Invocation API) to load those required classes before main > > Sorry to be pedantic but no. The JVMS does not require or depend upon the JNI specification for anything. I don't understand this approach. We have a set of specifications that are backed by a compliance test that determines what a compliant implementation of Java does - that set of specs includes both the JVMS and the JNI spec. Just because one deals in more detail on creating the JVM (ie: the JNI spec as the Invocation API is set of JNI calls) doesn't mean the specs are blind to each other. There's no way to produce a fully compliant Java implementation while ignoring one or the other spec. > So you cannot use 5.2 to say that no other classes can be loaded before the main class. We agree here - the JVM has broad remit to load whatever classes it likes before initial class is loaded and the main method is executed. Once that occurs, we start to differ. > As long as the rules that are specified in JVMS are followed the VM has considerable flexibility in how eagerly, or lazily, it can perform various actions, including loading and linking. This is the crux of the issue - the JVMS goes into great deal to say that execution is driven by the main method which gives a predictable (ie: users can reason able it) set of constraints on what will occur when running a program. The JVM will do linking (and recursively loading) based on that execution behaviour, reporting errors at the appropriate places. Linking can be eager or lazy but it most be driven by the subparts as defined in Ch5 - that's verification, preparation and resolution with class loading only allowed to support one of those specified behaviours. > Also note this issue is not about the legality of doing the eager loading, it is about the handling of errors that might arise during that eager loading. And the JVMS is quite specific about when such errors can be presented to the application. They can't be =) as eager loading isn't specified. Eager linking is! Loading is only allowed when supporting a linking activity. Sorry to be pedantic, but this is a key point in a number of ongoing discussions (Valhalla, Leyden, etc) and getting this wrong destroys the users ability to reason about their code and execution behaviour. Allowing arbitrary loading is akin to telling the VM to feel free to rummage through the jars it can find and load classes at any point. No user would believe that's acceptable behaviour. No other implementation I've worked on views arbitrary loads as acceptable.
18-06-2024

> That's covered in the spec for the Invocation API The JNi spec is irrelevant here. The JVMS does not require nor depend on JNI existing. You are appealing to JVMS 5.2 to severely restrict what classes can be loaded by the VM, but 5.2 is a gross simplification in that regard. > Method signatures are not actually involved in any of those activities. At what point would you say that the types in a method signature get resolved?
17-06-2024

Only resolution errors needs to preserved such that all resolution attempts fail the same way. As Dan says this is pure loading so that does not apply. Valhalla may change that going forward but here and now I don't think this is an issue.
17-06-2024

> If you read that too literally/strictly then the whole VM initialization process is non-compliant because we initialize a ton of core classes before we even look for the "main class". And of course you can't load the "main class" first in a strict sense because you need, at a minimum the classloader to load it with. Maybe 5.2 should be reworded to match reality. That's covered in the spec for the Invocation API (Ch5 of the JNI spec) which is responsible for "[l]oads and initializes a Java VM." Given it further talks about Threads, the only reasonable interpretation is that necessary classes are loaded by the VM prior to main. The VM has leeway to load the classes it needs prior to main, after that execution must dictate what occurs. > 5.4 gives us permission to eagerly link and load: > > > This specification allows an implementation flexibility as to when linking activities > > (and, because of recursion, loading) take place, provided that all of the following > > properties are maintained: 5.4 gives us permission to apply linking activities where most convenient for the VM - either eagerly or lazily - provided we follow the rules. Unfortunately, the rules don't allow loading that isn't part of a linking activity. Linking, according to 5.4, involves: > verifying and preparing that class or interface, its direct superclass, its direct superinterfaces, and its element type (if it is an array type), if necessary. Linking also involves resolution of symbolic references in the class or interface, though not necessarily at the same time as the class or interface is verified and prepared. Notice that class loading, unless part of a linking activity, is not listed. Method signatures are not actually involved in any of those activities.
17-06-2024

> we don't have leeway to load arbitrary classes. But they aren't "arbitrary" if they are referenced in the signature of a method of class currently being processed. > JVMS 5.2 Java Virtual Machine Startup says that execution starts with the main class being loaded, linked, and initialized. If you read that too literally/strictly then the whole VM initialization process is non-compliant because we initialize a ton of core classes before we even look for the "main class". And of course you can't load the "main class" first in a strict sense because you need, at a minimum the classloader to load it with. Maybe 5.2 should be reworded to match reality. Also note that if we do speculatively load a class and it fails for some reason then 5.2 also prohibits reporting such an error because: > If an error occurs during loading of a class or interface - either when a class loader > is locating a binary representation, or when the Java Virtual Machine is deriving > and creating a class from it - then the error must be thrown at a point in the program > that (directly or indirectly) uses the class or interface being loaded. This is the same reasoning as to why the JIT does not report classloading errors, for any class it had to eagerly load. 5.4 gives us permission to eagerly link and load: > This specification allows an implementation flexibility as to when linking activities > (and, because of recursion, loading) take place, provided that all of the following > properties are maintained: as long as we obey the listed rules. So again I do not see any issue here. We can speculatively/eagerly load the signature classes, but if we encounter an error we are not permitted to report it, so we have to abandon what we were trying to do by doing the eager loading.
17-06-2024

In this particular case as it's pure loading (and not resolution), the concern is more whether we're permitted to do arbitrary loads. I spelled out some of the concerns about arbitrary loads on the Valhalla-spec-experts list [0] but can go into more detail if needed. Will read JDK-4963485 for the background on the original change and respond further after that. [0] https://mail.openjdk.org/pipermail/valhalla-spec-experts/2024-June/002466.html
14-06-2024