14

Suppose you do simple thing:

public class Main {
    public static void main(String[] args) {
        long started = System.currentTimeMillis();
        try {
            new URL(args[0]).openConnection();
        } catch (Exception ignore) {
        }
        System.out.println(System.currentTimeMillis() - started);
    }
}

Now run it with http://localhost as args[0]

It takes ~100 msec to complete.

Now try https://localhost

It takes 5000+ msec.

Now run the same thing on linux or in docker:

  • http: ~100 msec
  • https: ~350 msec

Why is this? Why such a huge difference between platforms? What can you do about it?

For long-running application servers and applications with their own long and heavy initialization sequence, these 5 seconds may not matter.

However, there are plenty of applications where this initial 5sec "hang" matters and may become frustrating...

patrikbeno
  • 1,114
  • 9
  • 23
  • 4
    See https://stackoverflow.com/questions/38942514/simple-java-program-100-times-slower-after-plugging-in-usb-hotspot/38944130#38944130 – apangin Mar 16 '18 at 16:05
  • Thanks, @Eugene. Probably a duplicate answer, yes. Question seems entirely different :-) – patrikbeno Mar 17 '18 at 09:43
  • not really, even the question is a disguised "the same"; ultimately it boils down to why it is so slow, circumstances being different though, agreed. – Eugene Mar 17 '18 at 09:44
  • 2
    not trying to be too pedantic here, but not all "why is it so slow?" questions are the same. (1) "why is my https:// so slow?" and (2) "why is my file IO so slow when I plug-in unrelated USB?" -- these are entirely different questions, and they might possibly have entirely different answers. – patrikbeno Mar 17 '18 at 10:01
  • @Eugene The arguments of patrikbeno are understood, and I don't mind if he accepts his own answer. So let me reopen the question. The related topics are already linked anyway. – apangin Mar 18 '18 at 00:32
  • @patrikbeno you got me thinking, seems you are correct, especially if I think that I were to search for such a thing... its good that someone re-tracted the duplicate vote. sorry about this – Eugene Mar 18 '18 at 09:00

1 Answers1

20

(Note: see also latest updates at the end of this answer)

Explanation

Reason for this is default SecureRandom provider.

On Windows, there are 2 SecureRandom providers available:

- provider=SUN, type=SecureRandom, algorithm=SHA1PRNG
- provider=SunMSCAPI, type=SecureRandom, algorithm=Windows-PRNG

On Linux (tested in Alpine docker with Oracle JDK 8u162):

- provider=SUN, type=SecureRandom, algorithm=NativePRNG
- provider=SUN, type=SecureRandom, algorithm=SHA1PRNG
- provider=SUN, type=SecureRandom, algorithm=NativePRNGBlocking
- provider=SUN, type=SecureRandom, algorithm=NativePRNGNonBlocking

These are specified in jre/lib/security/java.security file.

security.provider.1=sun.security.provider.Sun
...
security.provider.10=sun.security.mscapi.SunMSCAPI

By default, first SecureRandom provider is used. On Windows, the default one is sun.security.provider.Sun, and this implementation reports following when JVM is run with -Djava.security.debug="provider,engine=SecureRandom":

Provider: SecureRandom.SHA1PRNG algorithm from: SUN
provider: Failed to use operating system seed generator: java.io.IOException: Required native CryptoAPI features not  available on this machine
provider: Using default threaded seed generator

And the default threaded seed generator is very slow.

You need to use SunMSCAPI provider.

Solution 1: Configuration

Reorder providers in configuration:

Edit jre/lib/security/java.security:

security.provider.1=sun.security.mscapi.SunMSCAPI
...
security.provider.10=sun.security.provider.Sun

I am not aware this can be done via system properties.

Or maybe yes, using-Djava.security.properties (untested, see this)

Solution 2: Programmatic

Reorder providers programmatically:

Optional.ofNullable(Security.getProvider("SunMSCAPI")).ifPresent(p->{
    Security.removeProvider(p.getName());
    Security.insertProviderAt(p, 1);
});

JVM now reports following (-Djava.security.debug="provider,engine=SecureRandom"):

Provider: SecureRandom.Windows-PRNG algorithm from: SunMSCAPI

Solution 3: Programmatic v2

Inspired by this idea, following piece of code inserts only a single SecureRandom service, configured dynamically from existing SunMSCAPI provider without the explicit reliance on sun.* classes. This also avoids the potential risks associated with indiscriminate prioritization of all services of SunMSCAPI provider.

public interface WindowsPRNG {

    static void init() {
        String provider = "SunMSCAPI"; // original provider
        String type = "SecureRandom"; // service type
        String alg = "Windows-PRNG"; // algorithm
        String name = String.format("%s.%s", provider, type); // our provider name
        if (Security.getProvider(name) != null) return; // already registered
        Optional.ofNullable(Security.getProvider(provider)) // only on Windows
                .ifPresent(p-> Optional.ofNullable(p.getService(type, alg)) // should exist but who knows?
                        .ifPresent(svc-> Security.insertProviderAt( // insert our provider with single SecureRandom service
                                new Provider(name, p.getVersion(), null) {{
                                    setProperty(String.format("%s.%s", type, alg), svc.getClassName());
                                }}, 1)));
    }

}

Performance

<140 msec (instead of 5000+ msec)

Details

There is a call to new SecureRandom() somewhere down the call stack when you use URL.openConnection("https://...")

It calls getPrngAlgorithm() (see SecureRandom:880)

And this returns first SecureRandom provider it finds.

For testing purposes, call to URL.openConnection() can be replaced with this:

new SecureRandom().generateSeed(20);

Disclaimer

I am not aware of any negative side effects caused by providers reordering. However, there may be some, especially considering default provider selection algorithm.

Anyway, at least in theory, from functional point of view this should be transparent to application.

Update 2019-01-08

Windows 10 (version 1803): Cannot reproduce this issue anymore on any of latest JDKs (tested all from old oracle 1.7.0_72 up to openjdk "12-ea" 2019-03-19).

It looks like it was Windows issue, fixed in latest OS updates. Related updates may or may not have taken place in recent JRE releases, too. However, I cannot reproduce the original issue even with my oldest JDK 7 update 72 installation which was definitelly affected, and definitelly not patched in any way.

There are still minor performance gains when using this solution (cca 350 msec on average) but the default behavior no longer suffers the intolerable 5+ seconds penalty.

patrikbeno
  • 1,114
  • 9
  • 23
  • 1
    Nice post. If it weren't for this it I wouldn't have probably ever known there was such a big performance difference between different providers on OS's. You should raise a ticket with Oracle because it seems more for the fix to be part of JDK. – Catalin Mar 16 '18 at 14:30
  • I wonder why the native CryptoAPI features were not available. That's what I would be trying to discover. – President James K. Polk Mar 16 '18 at 22:45
  • @JamesKPolk, I agree. Deepest reason I could find is that NativeSeedGenerator.nativeGenerateSeed() returns false. See: [NativeSeedGenerator.java](https://github.com/dmlloyd/openjdk/blob/342a565a2da8abd69c4ab85e285bb5f03b48b2c9/src/java.base/windows/classes/sun/security/provider/NativeSeedGenerator.java#L55) , [WinCAPISeedGenerator.c](https://github.com/JetBrains/jdk8u_jdk/blob/master/src/windows/native/sun/security/provider/WinCAPISeedGenerator.c#L44) – patrikbeno Mar 17 '18 at 09:17
  • Also related [SO question](https://stackoverflow.com/questions/9885380/in-windows-java-securerandom-generateseed-failed-unexpected-cryptoapi-failure) – patrikbeno Mar 17 '18 at 09:21
  • How is your `WindowsPRNG ` class work? I'm unable to call 2 times as `ifPresent` returns a void return result for me – Ferrybig Aug 10 '18 at 07:47
  • @Ferrybig, how about this? https://gist.github.com/patrikbeno/b7bf35c773b15e7ad07456a5abb36cd2 Tested on Java 9.0.4-zulu, Windows 10. Does this work on your machine? If not, post your configuration details. If you have issues with your code that you are unable to resolve, post a gist, too. – patrikbeno Aug 13 '18 at 09:49
  • I see what I did wrong I got comfused in my "Brain based java compiler" by the fact that it looked like there were multiple calls to isPresent in the same chain, I would have written that part of the code ,more in the way promises are being teached, like so: https://gist.github.com/ferrybig/2b599fad57e58c95b851b68a9c1f0267 (using map, instead of the length isPresent) – Ferrybig Aug 13 '18 at 09:56
  • Beware that this answer may contain information that is very dependent on the platform and Java version used. The Windows version is unfortunately not mentioned. Versions of Java above SE 8 may behave differently, and there *have been changes* w.r.t. `SecureRandom` implementations in the various providers. – Maarten Bodewes Dec 20 '18 at 17:26
  • 1
    @MaartenBodewes, thanks. Fixed in Windows OS. I updated the answer. – patrikbeno Jan 08 '19 at 11:19