JDK-8081674 : EmptyStackException at startup if running with extended or unsupported charset
  • Type: Bug
  • Component: core-libs
  • Affected Version: 8,9
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2015-06-02
  • Updated: 2015-09-29
  • Resolved: 2015-06-11
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 8 JDK 9
8u60Fixed 9 b69Fixed
Related Reports
Relates :  
Relates :  
Relates :  
Description
Using an unsupported character encoding (e.g. vi_VN.TCVN on Linux) results in an immediate VM failure with jdk 8 and 9:

export LANG=vi_VN.TCVN
java -version
Error occurred during initialization of VM
java.util.EmptyStackException
	at java.util.Stack.peek(Stack.java:102)
	at java.lang.ClassLoader$NativeLibrary.getFromClass(ClassLoader.java:1751)
	at java.lang.ClassLoader$NativeLibrary.findBuiltinLib(Native Method)
	at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1862)
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1835)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1119)
	at java.lang.System.initializeSystemClass(System.java:1194)

This is a consequence of "8005716: Enhance JNI specification to allow support of static JNI libraries in Embedded JREs". 

With jdk 9 we get this error even if we're running with a supported charset which is in the ExtendedCharsets (as opposed to being in the StandardCharsets) class which is a consequence of delegating the loading of the ExtendedCharsets class to the ServiceLoader in jdk 9.

export LANG=eo.iso-8859-3
output-jdk9/images/jdk/bin/java -version
Error occurred during initialization of VM
java.util.EmptyStackException
	at java.util.Stack.peek(Stack.java:102)
	at java.lang.ClassLoader$NativeLibrary.getFromClass(ClassLoader.java:1737)
	at java.lang.ClassLoader$NativeLibrary.findBuiltinLib(Native Method)
	at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1866)
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1840)
	at java.lang.Runtime.loadLibrary0(Runtime.java:874)
	at java.lang.System.loadLibrary(System.java:1111)
	at java.lang.System.initializeSystemClass(System.java:1186)


Here's why the exception happens for an unsupported charset (see the mixed stack trace below for the full details):

java.lang.System.loadLibrary() wants to load libzip.so. It calls java.lang.Runtime.loadLibrary0() which at the very beginning calls the native method ClassLoader$NativeLibrary.findBuiltinLib() which checks if the corresponding library is already statically linked into the VM (introduced by 8005716). Java_java_lang_ClassLoader_00024NativeLibrary_findBuiltinLib(), the native implementation of findBuiltinLib() in Classloader.c calls GetStringPlatformChars() to convert the library name into the native platform encoding. GetStringPlatformChars() calls the helper function jnuEncodingSupported() to check if the platform encoding which is stored in the property "sun.jnu.encoding" is supported by Java. jnuEncodingSupported() is implemented as follows:

static jboolean isJNUEncodingSupported = JNI_FALSE;
static jboolean jnuEncodingSupported(JNIEnv *env) {
    jboolean exe;
    if (isJNUEncodingSupported == JNI_TRUE) {
        return JNI_TRUE;
    }
    isJNUEncodingSupported = (jboolean) JNU_CallStaticMethodByName (
                                    env, &exe,
                                    "java/nio/charset/Charset",
                                    "isSupported",
                                    "(Ljava/lang/String;)Z",
                                    jnuEncoding).z;
    return isJNUEncodingSupported;
}

Once the function finds that the platform encoding is supported (by calling java.nio.charset.Charset.isSupported()) it caches this value and always returns it on following calls. However if the platform encoding is not supported, it ALWAYS calls java.nio.charset.Charset.isSupported() an every subsequent invocation.

In order to call the Java method Charset.isSupported() (in JNU_CallStaticMethodByName() in file jni_util.c), we have to call jni_FindClass() to convert the symbolic class name "java.nio.charset.Charset" into a class reference.

But unfortunately, jni_FindClass() (from jni.cpp in libjvm.so) has a special handling if called from java.lang.ClassLoader$NativeLibrary to ensure that JNI_OnLoad/JNI_OnUnload are executed in the correct class context:

  instanceKlassHandle k (THREAD, thread->security_get_caller_class(0));
  if (k.not_null()) {
    loader = Handle(THREAD, k->class_loader());
    // Special handling to make sure JNI_OnLoad and JNI_OnUnload are executed
    // in the correct class context.
    if (loader.is_null() &&
        k->name() == vmSymbols::java_lang_ClassLoader_NativeLibrary()) {
      JavaValue result(T_OBJECT);
      JavaCalls::call_static(&result, k,
                                      vmSymbols::getFromClass_name(),
                                      vmSymbols::void_class_signature(),
                                      thread);
      if (HAS_PENDING_EXCEPTION) {
        Handle ex(thread, thread->pending_exception());
        CLEAR_PENDING_EXCEPTION;
        THROW_HANDLE_0(ex);
      }

So if that's the case and jni_FindClass() was reallycalled from ClassLoader$NativeLibrary, then jni_FindClass() calles ClassLoader$NativeLibrary().getFromClass() to find out the corresponding context class which is supposed to be saved there in a field of type java.util.Stack named "nativeLibraryContext":

// Invoked in the VM to determine the context class in
// JNI_Load/JNI_Unload
static Class<?> getFromClass() {
    return ClassLoader.nativeLibraryContext.peek().fromClass;
}

Unfortunately, "nativeLibraryContext" doesn't contain any entry at this point and the invocation of Stack.peek() will throw the exception shown before. In general, the "nativeLibraryContext" stack will be filled later on in Runtime.loadLibrary0() like this:

NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
nativeLibraryContext.push(lib);
try {
    lib.load(name, isBuiltin);
} finally {
    nativeLibraryContext.pop();
}

such that it always contains at least one element later when jni_FindClass() will be invoked.

So in summary, the problem is that the implementors of 8005716 didn't took into account that calling ClassLoader$NativeLibrary.findBuiltinLib() may trigger a call to jni_FindClass() if we are running on a system with an unsupported character encoding.


I'd suggest the following fix for this problem:

Change ClassLoader$NativeLibrary().getFromClass() to return null if the stack is empty instead of throwing an exception:

static Class<?> getFromClass() {
    return ClassLoader.nativeLibraryContext.empty() ?
        null : ClassLoader.nativeLibraryContext.peek().fromClass;
}

Unfortunately this also requires a HotSpot change in jni_FindClass() in order to properly handle the new 'null' return value:

  if (k.not_null()) {
    loader = Handle(THREAD, k->class_loader());
    // Special handling to make sure JNI_OnLoad and JNI_OnUnload are executed
    // in the correct class context.
    if (loader.is_null() &&
        k->name() == vmSymbols::java_lang_ClassLoader_NativeLibrary()) {
      JavaValue result(T_OBJECT);
      JavaCalls::call_static(&result, k,
                                      vmSymbols::getFromClass_name(),
                                      vmSymbols::void_class_signature(),
                                      thread);
      if (HAS_PENDING_EXCEPTION) {
        Handle ex(thread, thread->pending_exception());
        CLEAR_PENDING_EXCEPTION;
        THROW_HANDLE_0(ex);
      }
      oop mirror = (oop) result.get_jobject();
      if (oopDesc::is_null(mirror)) {
        loader = Handle(THREAD, SystemDictionary::java_system_loader());
      } else {
        loader = Handle(THREAD,
          InstanceKlass::cast(java_lang_Class::as_Klass(mirror))->class_loader());
        protection_domain = Handle(THREAD,
          InstanceKlass::cast(java_lang_Class::as_Klass(mirror))->protection_domain());
      }
    }
  } else {
    // We call ClassLoader.getSystemClassLoader to obtain the system class loader.
    loader = Handle(THREAD, SystemDictionary::java_system_loader());
  }

These changes are sufficient to solve the problem in Java 8. Unfortunately, that's still not enough in Java 9 because there the loading of the extended charsets has been delegated to ServiceLoader. But ServiceLoader calls ClassLoader.getBootstrapResources() which 
calls sun.misc.Launcher.getBootstrapClassPath(). This leads to another problem during class initialization of sun.misc.Launcher if running on an unsupported locale.

The first thing done in sun.misc.Launcher.<clinit> is the initialisation of the bootstrap URLClassPath in the Launcher. However this initialisation will eventually call Charset.isSupported() and if we are running on an unsupported locale this will inevitably end in another recursive call to ServiceLoader. But as explained below, ServiceLoader will query the Launcher's bootstrap URLClassPath which will be still uninitialized at that point.

So we'll have to additionally guard guard against this situation on JDK 9 like this:

private static Charset lookupExtendedCharset(String charsetName) {
    if (!sun.misc.VM.isBooted() ||             // see lookupViaProviders()
        sun.misc.Launcher.getBootstrapClassPath() == null)
        return null;

This fixes the crashes, but still at the price of not having the extended charsets available during initialization until Launcher.getBootstrapClassPath is set up properly. This may be still a problem if the jdk is installed in a directory which contains characters specific to an extended encoding or if we have such characters in the command line arguments. 

Mixed stack trace of the initial EmptyStackException for unsupported charsets described before:

Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
j  java.util.Stack.peek()Ljava/lang/Object;+1
j  java.lang.ClassLoader$NativeLibrary.getFromClass()Ljava/lang/Class;+3
v  ~StubRoutines::call_stub
V  [libjvm.so+0x9d279a]  JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x6b4
V  [libjvm.so+0xcad591]  os::os_exception_wrapper(void (*)(JavaValue*, methodHandle*, JavaCallArguments*, Thread*), JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x45
V  [libjvm.so+0x9d20cf]  JavaCalls::call(JavaValue*, methodHandle, JavaCallArguments*, Thread*)+0x8b
V  [libjvm.so+0x9d1d3b]  JavaCalls::call_static(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)+0x139
V  [libjvm.so+0x9d1e3f]  JavaCalls::call_static(JavaValue*, KlassHandle, Symbol*, Symbol*, Thread*)+0x9d
V  [libjvm.so+0x9e6588]  jni_FindClass+0x428
C  [libjava.so+0x20208]  JNU_CallStaticMethodByName+0xff
C  [libjava.so+0x21cae]  jnuEncodingSupported+0x61
C  [libjava.so+0x22125]  JNU_GetStringPlatformChars+0x125
C  [libjava.so+0xedcd]  Java_java_lang_ClassLoader_00024NativeLibrary_findBuiltinLib+0x8b
j  java.lang.ClassLoader$NativeLibrary.findBuiltinLib(Ljava/lang/String;)Ljava/lang/String;+0
j  java.lang.ClassLoader.loadLibrary0(Ljava/lang/Class;Ljava/io/File;)Z+4
j  java.lang.ClassLoader.loadLibrary(Ljava/lang/Class;Ljava/lang/String;Z)V+228
j  java.lang.Runtime.loadLibrary0(Ljava/lang/Class;Ljava/lang/String;)V+54
j  java.lang.System.loadLibrary(Ljava/lang/String;)V+7
j  java.lang.System.initializeSystemClass()V+113
v  ~StubRoutines::call_stub
V  [libjvm.so+0x9d279a]  JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x6b4
V  [libjvm.so+0xcad591]  os::os_exception_wrapper(void (*)(JavaValue*, methodHandle*, JavaCallArguments*, Thread*), JavaValue*, methodHandle*, JavaCallArguments*, Thread*)+0x45
V  [libjvm.so+0x9d20cf]  JavaCalls::call(JavaValue*, methodHandle, JavaCallArguments*, Thread*)+0x8b
V  [libjvm.so+0x9d1d3b]  JavaCalls::call_static(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)+0x139
V  [libjvm.so+0x9d1e3f]  JavaCalls::call_static(JavaValue*, KlassHandle, Symbol*, Symbol*, Thread*)+0x9d
V  [libjvm.so+0xe3cceb]  call_initializeSystemClass(Thread*)+0xb0
V  [libjvm.so+0xe44444]  Threads::initialize_java_lang_classes(JavaThread*, Thread*)+0x21a
V  [libjvm.so+0xe44b12]  Threads::create_vm(JavaVMInitArgs*, bool*)+0x4a6
V  [libjvm.so+0xa19bd7]  JNI_CreateJavaVM+0xc7
C  [libjli.so+0xa520]  InitializeJVM+0x154
C  [libjli.so+0x8024]  JavaMain+0xcc

Comments
JDK-8081674 only fixes the EmptyStackException. JDK-8087161 will fix the ServiceLoader issue which only appears in jdk9.
11-06-2015

The static findBuiltinLib method finds if JNI_OnLoad_$LIBNAME entry exists. It is not intended to trigger any shared library to be loaded. As findBuiltinLib doesn't load any shared library, it confuses JNI FindClass to call NativeLibrary::getFromClass. The findBuiltinLib method can simply be moved out from NativeLibrary class. I file JDK-8087161 for the ServiceLoader and getSystemClassLoader bootstrapping issue.
11-06-2015

I'll leave Sherman to comment on iconv. I think his proposal is to only use it when there isn't a java charset. I expect startup to be quite significant different when the module system comes in. In particular, VM.isBooted will not be able to return true until a bit later (after the module system is initialized). Mandy and I have spent some time working out how startup should work but we aren't ready to bring this to jdk9/dev yet.
03-06-2015

Regarding the use of iconv - I think that's an interesting idea. Although it will introduce yet another build dependency I've just checked that it seems to be supported at least on all the platforms we support (i.e. AIX, HPUX, Solaris, MaxOS, Linux). The only problem I see is that we must carefully pay attention that the iconv encoders/decoders behave exactly the same like their Java counterparts. And there's another caveat - having iconv doesn't necessarily means that iconv has all the required encodings installed and unfortunately the encodings supported by iconv are not equal to the encodings supported by the OS. Nevertheless, on all the systems I've looked at iconv seems to support more encodings than the OS which is good for us. What's also not completely clear to me is if there's a simple way to tell when the module system has been completely initialized. I think if we could do that (i.e. if we would have something like sun.misc.VM.isModuleSystemInitialized()) we could use that in Charset.isSupported() instead of using sun.misc.VM.isBooted() (which obviously means something different in the jdk 9 context) and everything would be fine.
03-06-2015

OK, I've sent out a RFR on core-libs-dev and hotspot-dev yesterday. Fixing the initial EmptyStackException is really just three lines in jni.cpp and a single line in ClassLoader.java which don't do any harm.
03-06-2015

I'll ask Sherman to comment on this. It's an issue that he's looked at in the past and at one point was thinking the JNI/native code should use iconv during startup. I think we should also re-examine the JNI static linking changes to see if we can avoid this (although that would be just another workaround). For JDK 9 then we expect startup will work very differently when the module system comes in. In particular, the module system will need be initialized before the VM can load classes from other modules. In other words, the VM should startup with just the base module. This is part of the rational for moving the charsets for all supported configurations in java.base.
03-06-2015

OK, but can we please solve the initial EmptyStackException problem first which appears in both JDK 8 and 9 and which is caused by the hacky interaction of jni_FindClass() and ClassLoader$NativeLibrary (I'd really like to get that into 8u60 if possible). Once we fixed that, we can discuss the startup behaviour with non-standard encodings in JDK 9 because only there we use ServiceLoader.
02-06-2015

The base module must contain the 6 standard charsets and any charsets needed to start the VM in a supported configuration. The jdk.charsets module will be a service provider module containing other charsets that might be used by applications or libraries. One question is whether startup should fail or fallback to UTF-8 when attempting to startup in an unsupported configuration. The JNI/native usage is awkward. I think Sherman proposed using iconv during startup at one point to avoid issues like this. Thanks for the info on running local-gen.
02-06-2015

Maybe I misunderstand something here, but we still have the charsets in StandardCharsets which are in the base module and the ExtendedCharsets which are in the jdk.charsets module (and are loaded by ServiceLoader). If you say that the extended charsets are not supported during startup (i.e. it's not possible to safely start the VM in such an environment) that's OK for me. But that means that these charsets can only be used AFTER VM initialisation so it will be not possible to pass in arguments in this encoding or install the JDK into a path with encoding-specific characters. Still, the current implementation IS USING ServiceLoader AFTER sun.misc.VM.isBooted() but BUT BEFORE sun.misc.Launcher.getBootstrapClassPath() is initialized if it doesn't find the actual platform encoding in the StandardCharsets. Just read my description carefully and have a look at the implementation of java.nio.charset.Charset.lookup2() which is indirectly called from jnuEncodingSupported(). If you fix the first problem (i.e. the EmptyStackException) alone as proposed in the first part of my report, you'll run into this ServiceLoader issue and get the following stack trace: Error occurred during initialization of VM java.lang.ExceptionInInitializerError at java.nio.charset.Charset.lookupExtendedCharset(Charset.java:449) at java.nio.charset.Charset.lookup2(Charset.java:479) at java.nio.charset.Charset.lookup(Charset.java:467) at java.nio.charset.Charset.isSupported(Charset.java:508) at java.io.UnixFileSystem.getBooleanAttributes0(Native Method) at java.io.UnixFileSystem.getBooleanAttributes(UnixFileSystem.java:242) at java.io.File.exists(File.java:819) at sun.misc.Launcher$ExtClassLoader$1.run(Launcher.java:152) at sun.misc.Launcher$ExtClassLoader$1.run(Launcher.java:144) at java.security.AccessController.doPrivileged(Native Method) at sun.misc.Launcher$ExtClassLoader.getExtClassLoader(Launcher.java:143) at sun.misc.Launcher.<init>(Launcher.java:76) at sun.misc.Launcher.<clinit>(Launcher.java:64) at java.lang.ClassLoader.initSystemClassLoader(ClassLoader.java:1443) at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1428) Caused by: java.lang.NullPointerException at java.nio.charset.Charset.lookupExtendedCharset(Charset.java:450) at java.nio.charset.Charset.lookup2(Charset.java:479) at java.nio.charset.Charset.lookup(Charset.java:467) at java.nio.charset.Charset.isSupported(Charset.java:508) at java.io.UnixFileSystem.canonicalize0(Native Method) at java.io.UnixFileSystem.canonicalize(UnixFileSystem.java:172) at java.io.File.getCanonicalPath(File.java:618) at java.io.File.getCanonicalFile(File.java:643) at sun.misc.URLClassPath$JImageLoader.<init>(URLClassPath.java:1135) at sun.misc.URLClassPath$3.run(URLClassPath.java:389) at sun.misc.URLClassPath$3.run(URLClassPath.java:377) at java.security.AccessController.doPrivileged(Native Method) at sun.misc.URLClassPath.getLoader(URLClassPath.java:376) at sun.misc.URLClassPath.getLoader(URLClassPath.java:353) at sun.misc.URLClassPath.access$000(URLClassPath.java:80) at sun.misc.URLClassPath$2.next(URLClassPath.java:293) at sun.misc.URLClassPath$2.hasMoreElements(URLClassPath.java:304) at java.lang.ClassLoader$2.hasMoreElements(ClassLoader.java:1273) at java.util.ServiceLoader$LazyIterator.hasNextService(ServiceLoader.java:352) at java.util.ServiceLoader$LazyIterator.hasNext(ServiceLoader.java:391) at java.util.ServiceLoader$1.hasNext(ServiceLoader.java:472) at java.nio.charset.Charset$ExtendedProviderHolder$1.run(Charset.java:435) at java.nio.charset.Charset$ExtendedProviderHolder$1.run(Charset.java:429) at java.security.AccessController.doPrivileged(Native Method) at java.nio.charset.Charset$ExtendedProviderHolder.extendedProviders(Charset.java:429) at java.nio.charset.Charset$ExtendedProviderHolder.<clinit>(Charset.java:426) at java.nio.charset.Charset.lookupExtendedCharset(Charset.java:449) at java.nio.charset.Charset.lookup2(Charset.java:479) at java.nio.charset.Charset.lookup(Charset.java:467) at java.nio.charset.Charset.isSupported(Charset.java:508) at java.io.UnixFileSystem.getBooleanAttributes0(Native Method) at java.io.UnixFileSystem.getBooleanAttributes(UnixFileSystem.java:242) at java.io.File.exists(File.java:819) at sun.misc.Launcher$ExtClassLoader$1.run(Launcher.java:152) at sun.misc.Launcher$ExtClassLoader$1.run(Launcher.java:144) at java.security.AccessController.doPrivileged(Native Method) at sun.misc.Launcher$ExtClassLoader.getExtClassLoader(Launcher.java:143) at sun.misc.Launcher.<init>(Launcher.java:76) at sun.misc.Launcher.<clinit>(Launcher.java:64) at java.lang.ClassLoader.initSystemClassLoader(ClassLoader.java:1443) at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1428) By the way, the problem should be easily reproducible as described in the begining of my report. But it is not enough to just set LANG as described - you'll also have to have these encodings installed, otherwise Linux will just fall back to ASCII or UTF-8. You can verify which locales you have with 'locale -a'. On my Ubuntu machine, I can install additional locals by doing (as root): locale-gen vi_VN.TCVN locale-gen eo 'eo' is for Esperanto and will use the supported (but extended) charset ISO-8859-3. Not sure how this works on other distros but I'm sure you'll find out:) At the end 'nl_langinfo(CODESET)' should return the corresponding encoding (i.e. TCVN (unsupported) or ISO-8859-3 (extended)) when called from 'ParseLocale()' in "java_props_md.c" in order to reproduce the problem.
02-06-2015

With JDK 9 then the base module must contain the charsets for all supported configurations. There shouldn't be any attempt to load from jdk.charsets or use ServiceLoader during startup. It would be good to get instructions on how to duplicate this. I'm unable to duplicate with by setting LANG to an unsupported value.
02-06-2015