Bug 533475 - ExceptionInIntializerError using ecj-4.7.3 in Java 9. Java 8 works fine, as does compiling with JDK
Summary: ExceptionInIntializerError using ecj-4.7.3 in Java 9. Java 8 works fine, as d...
Status: VERIFIED FIXED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: Core (show other bugs)
Version: 4.7.3   Edit
Hardware: PC Windows 10
: P4 blocker with 2 votes (vote)
Target Milestone: 4.11 M3   Edit
Assignee: Stephan Herrmann CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2018-04-11 11:52 EDT by Greg Johnson CLA
Modified: 2019-02-19 18:01 EST (History)
5 users (show)

See Also:


Attachments
Source files to demonstrate problem (2.51 MB, application/x-zip-compressed)
2018-04-11 11:52 EDT, Greg Johnson CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Greg Johnson CLA 2018-04-11 11:52:41 EDT
Created attachment 273541 [details]
Source files to demonstrate problem

I've produced a code sample that executes correctly in Eclipse under Java 8, but fails with an ExceptionInInitializerError when executed under Java 9.

I've narrowed the problem to differences in how ECJ generates code under Java 8 and 9. (Compiling straight with the JDK compiler does not exhibit the run-time error.)

Initially located in a much larger code base, I've simplified to the lowest level possible. In this extremely small test case, the code compiles correctly, but at runtime an ExceptionInIntializerError is thrown under Java 9, but runs correctly under Java 8, as well as under the JDK compiled version.

Given the attached source files, here's the stack trace:
Exception in thread "main" java.lang.ExceptionInInitializerError
	at EnumSample.<clinit>(EnumSample.java:4)
	at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
	at EnumSample.values(EnumSample.java:1)
	at SwitchSample.$SWITCH_TABLE$EnumSample(SwitchSample.java:1)
	at SwitchSample.<clinit>(SwitchSample.java:3)
	... 2 more

Using javap, I've been looking at the output byte codes for SwitchSample.class. (By the way, the byte codes generated by ECJ for the two other classes are identical in both Java 8 and Java 9.) It appears that under Java 9, ECJ emits code that causes a switch statement to be executed from a class constructor, when if fact, the switch statement shouldn't be involved.

I am attaching the three relevant Java source files (Main.java, EnumSample.java, SwitchSample.java), a testcases.sh script I use for demonstrating the problem, and the javap output for SwitchSample.class for both the Java 8 and Java 9 versions.
Note: If the switch statement is removed from SwitchSample.java, the exception is no longer thrown at runtime.

In the decompiled output files, I noticed the following:
Java 8:
two opcodes are present in the Java 8 version which don't appear in the Java 9 version. Just before the 'areturn' statement which is located prior to the Exception table, those statements are:
dup
putstatic #32 // Field $SWITCH_TABLE$EnumSample:[I

Java 9:
There appear to be two extra opcodes in the constructor code that are not present in the Java 8 version. I believe these extra codes cause the switch statement to be executed when it shouldn't be. (Yeah, I'm arm-chair driving here.) The extra opcodes are:
invokestatic #19 // Method $SWITCH_TABLE$EnumSample: ()[I
putstatic #22 // Field $SWITCH_TABLE$EnumSample: [I

Here is the contents of my test script:
#!/bin/sh

mkdir binjdk9 binecj8 binecj9

# JDK Java 9

javac  -nowarn  -cp src -d binjdk9 src/*java
echo "Running JDK version 9"
java -cp binjdk9 Main

# ECJ Java 8

java -jar ecj-4.7.3.jar  -nowarn -8 -cp src -d binecj8 src/*java
echo "Running ECJ Java 8"
java -cp binecj8 Main

# ECJ Java 9

java -jar ecj-4.7.3.jar -nowarn -9 -cp src -d binecj9 src/*java
echo "Running ECJ Java 9"
java -cp binecj9 Main
Comment 1 Olivier Thomann CLA 2018-04-11 14:00:35 EDT
The problem was introduced by the fix for bug 526911.
The problem is that the static initializer of SwitchSample in java 9 mode calls the synthetic method that initializes the enum table. This happens during the static initializer of the EnumSample because one of the constant of the enum type is using a constant from SwitchSample. So by the time EnumSample.values() is called in the synthetic methods, EnumSample is not yet initialized.
javac works around the problem by defining the synthetic methods inside a static  anonymous class. This prevents that loop during class initialization.
Comment 2 Sasikanth Bharadwaj CLA 2018-04-12 00:51:27 EDT
Thanks Olivier for the analysis. For this case, we can get away by following javac and using a static inner class.

The problem, however is bigger, and I don't think can be solved by that method. Since JVMS insists that all final fields can ONLY be written inside a clinit, the loop is unavoidable if these kind of interdependencies exist. For example, with a small modification to the attached code as below, even javac generates code that crashes when run

public class Main
{
	public static void main(String args[])
	{
		Object anObject = EnumSample.ENUM1;
	}
}

public enum EnumSample
{
	ENUM1(123),
	ENUM2(new SwitchSample().STRING1);

	EnumSample(int msgId)
	{
	}

	EnumSample(String format)
	{
	}
}

public class SwitchSample 
{
	public final String STRING1;
	public SwitchSample() {
		STRING1="string1";
		EnumSample[] samples = EnumSample.values();
		// do something with samples
	}
}

I have no idea what to do about this
Comment 3 Nir Lisker CLA 2018-05-20 13:21:30 EDT
I'm hitting this as well with Java 10. Eclipse 4.7.3 does not complain, but Gradle reports "illegal reference to static field from initializer" on 'selected'.

enum MyEnum {
;

    private static MyEnum selected;

        private MyEnum() {
	    Supplier<MyEnum> s = () ->  selected = this;
	}
    }
}

Bug 482726 is probably the same.
Comment 4 Vassilis Virvilis CLA 2019-01-18 05:50:32 EST
The crash in comment #2 is not the same as in the crash of the original post.

The original reporter crash is related to the existence of a switch statement. Somebody has also reported that in jdk although it works there (java 11) according to my tests. See https://bugs.openjdk.java.net/browse/JDK-8208267

The code in comment #2 also crashes jdk 11 for me.

This bug is still present in eclipse 2018-12, ecj 3.16 if I can tell correctly.


   Vassilis
Comment 5 Stephan Herrmann CLA 2019-01-19 07:57:19 EST
Here's what I see today:

common 0, when compiled with ecj, any version since 4.7.3 (fix for bug 526911):
Exception in thread "main" java.lang.ExceptionInInitializerError
        at EnumSample.<clinit>(EnumSample.java:4)
        at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
        at EnumSample.values(EnumSample.java:1)
        at SwitchSample.$SWITCH_TABLE$EnumSample(SwitchSample.java:1)
        at SwitchSample.<clinit>(SwitchSample.java:3)
        ... 2 more
When compiled with javac, no exception occurs.

comment 2, when compiled with any compiler (ecj/javac) crashes like this:
Exception in thread "main" java.lang.ExceptionInInitializerError
        at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
        at EnumSample.values(EnumSample.java:1)
        at SwitchSample.<init>(SwitchSample.java:6)
        at EnumSample.<clinit>(EnumSample.java:4)
        ... 1 more

https://bugs.openjdk.java.net/browse/JDK-8208267 is closed, because the provided example doesn't reproduce the claimed problem (neither compiler).


IMHO, comment 2 is a hint that initialization order is inherently fragile, no 100% safe strategy is known.

Ergo, the difference between compilers is only in where is the boundary between safe and unsafe *cyclic* dependencies.

IOW, no compiler promises to nicely handle cyclic dependencies involving an enum.

Lowering priority, because I don't see a 'moral obligation' to handle this kind of cyclic dependencies in a fully safe manner. And I don't see anyone in the team spending time on this.

If s.o. from the community wants to step up, we certainly welcome a high-quality patch, but be warned that this is a hard nut to crack, involving many facets. I actually recommend, that prior to starting to hack the compiler, a strategy should be presented here, what exact bytecode the compiler should generate. From the description of such strategy it must be clear (a) how it solves the problem and (b) that it doesn't have any negative impact otherwise.
Comment 6 Vassilis Virvilis CLA 2019-01-30 08:04:22 EST
Hi,

Here is a slightly modified reproducer without any cyclic inter-dependencies.

The main method tries to use MyClass.State.values().

MyClass.State does not depend on anything outside MyClass.

Break in ecj - Works in openjdk

https://bugs.openjdk.java.net/browse/JDK-8208267 is closed because it works with openjdk

public class SwitchBug {
    static class MyClass {
        private static final Object C = "";

        public enum State {
                           ENABLED(C); // pass null constant

            State(Object value) {
            } // value can be ignored
        }

        /* unused method with switch statement IN SAME CLASS */
        private void unusedMethod() {
            switch (State.ENABLED) {
            case ENABLED:
                break;
            }
        }
    }
    
    public static void main(String[] args) {
        // access enum values from an other class
        MyClass.State.values();
        System.out.println("It runs.");
    }
}
Comment 7 Stephan Herrmann CLA 2019-01-30 09:41:08 EST
(In reply to Vassilis Virvilis from comment #6)

Can you quote JLS or JVMS on what should be the correct initialization sequence and where ecj violates this?
Comment 8 Vassilis Virvilis CLA 2019-01-30 09:49:13 EST
I am sorry I can't do that. I am not an expert on JVM internals.

My point was that the bug exists for no cyclical dependencies.

I can understand that in case of cyclical dependencies nothing can be done. OpenJDK itself crashes.

But in the case of non cyclical dependency should it crash?

If you say that is not covered from the standard and it fails under the 'not expected to work' area then I am not qualified enough to disagree and I will take your word for it.

Unless the code I posted also has a cyclical dependency and I failed to see it.
Comment 9 Eclipse Genie CLA 2019-01-31 14:06:29 EST
New Gerrit change created: https://git.eclipse.org/r/136102
Comment 10 Stephan Herrmann CLA 2019-01-31 14:17:25 EST
(In reply to Eclipse Genie from comment #9)
> New Gerrit change created: https://git.eclipse.org/r/136102

Re-reading bug 526911 I found that we are lucky and have just enough room for improvement to resolve this issue:

Since JVM 9, bytecode is verified with stricter rules regarding initialization of static final fields. This forced us to change the code pattern used for initializing an internal field synthesized for switch over enum (here: $SWITCH_TABLE$SwitchBug$MyClass$State).

It turned out, however, that bug 526911 resolved its issue in a slightly over-eager way: since the SWITCH_TABLE field must _sometimes_ be final, we generally changed the codegen pattern for Java 9+. In this particular example that field is not final and so we can go back to the pre-Java-9 pattern, which causes no issues in initialization order.

This increases the set of programs that are able to invoke implicit initialization without blowing up, while combining of different patterns of implicit initialization remains inherently brittle.
Comment 12 Stephan Herrmann CLA 2019-01-31 16:52:59 EST
(In reply to Eclipse Genie from comment #11)
> Gerrit change https://git.eclipse.org/r/136102 was merged to [master].
> Commit:
> http://git.eclipse.org/c/jdt/eclipse.jdt.core.git/commit/
> ?id=705bf0beff6f458ef068964094f5fecf359aec3c

Released for 4.10 M3
Comment 13 Manoj N Palat CLA 2019-02-19 18:01:44 EST
Verified for Eclipse 4.11 M3 with Build id: I20190218-1800