仅在一个特定系统上会导致 NoClassDefFoundError 的原因是什么?

What could cause a NoClassDefFoundError on one specific system only?

我目前正在使用 Swing 开发桌面应用程序,通过 HTTPS 与服务器通信。但是,在我必须支持的一台生产机器上,当前的开发版本抛出一个 NoClassDefFoundError,尽管 class 实际上包含在 JAR 中。

情况很简单:我正在启动 Swing 应用程序并配置要联系的服务器,然后用于快速 HTTPS 连接测试(在 class HTTPClient ).我促进 OkHttp 与服务器的所有通信。

我的 HTTPClient 类似于此代码:

package com.myprogram.http;

import okhttp3.*;
import java.net.*;
import java.io.*;

public class HTTPClient {
    private static final OkHttpClient CLIENT = new OkHttpClient ();

    static boolean sendHeartbeat (String server, int port) {
        URL url;
        try { url = new URL ("https", server, port, "/api/v1/heartbeat"); }
        catch (MalformedURLException e) { return false; }

        Request request = new Request.Builder ().url (url).build ();
        try (Response resonse = CLIENT.newCall (request).execute ()) {
            return true;
        } catch (IOException | NullPointerException e) { return false; }
    }
}

这段代码是从中间件调用的,它也进行状态检查等,是 UI 和 HTTP 代码之间的真正接口。这是 HTTPConnector class:

package com.myprogram.http;

import java.util.concurrent.*;

public class HTTPConnector {
    private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool (10);
    private static String server; // Set by UI code
    private static int port;      // Set by UI code
    
    public static void setServer (String server, int port) {
        HTTPConnector.server = server;
        HTTPConnector.port = port;
    }

    public static void testServerAvailability () {
        if (server == null) throw new IllegalStateException ("Must set server first!");
        
        boolean success = false;
        for (int i = 0; i < 5; i++) {
            Future <Boolean> future = null;
            try {
/* line x */    future = THREAD_POOL.submit (() -> HTTPClient.sendHeartbeat (server, port));
/* line y */    success = future.get (1500, TimeUnit.MILLISECONDS);
                if (success) break;
            } catch (TimeoutException e) {
                e.printStackTrace ();
                System.out.println ("Server status timed out");
                future.cancel (true);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace ();
                System.out.println ("Server status could not be queried");
            }
        }
    }
}

(请注意,为了 MCVE,我在这里删节和简化了我的代码。实际上,我确保只有一个线程将调用分派到 HTTPConnector。)

然而,在调用 HTTPConnector.testServerAvailability () 时,我首先得到一个 TimeoutException,然后是四个 NoClassDefFoundError,它们是:

java.util.concurrent.TimeoutException
    at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:204)
    at com.myprogram.http.HTTPConnector.testServerAvailability(HTTPConnector.java:x)
    at [REDACTED]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)

java.util.concurrent.ExecutionException: java.lang.NoClassDefFoundError: Could not initialize class com.myprogram.http.HTTPClient
    at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:205)
    at com.myprogram.http.HTTPConnector.testServerAvailability(HTTPConnector.java:y)
    at [REDACTED]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.NoClassDefFoundError: Could not initialize class com.myprogram.http.HTTPClient
    at com.myprogram.http.HTTPConnector.lambda$testServerAvailability(HTTPConnector.java:x)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    ... 3 more

我已经确认这个 Lambda 是第一个调用 HTTPClient 的地方,因此是唯一可以初始化 class 的地方。

到目前为止,我唯一看到这种行为的机器是具有双核 64 位 CPU 和 4 GiB RAM 的 Windows 7 Professional(SP1 内部版本 7601)。在我的 Windows 7 Professional (SP1 build 7601) 虚拟机上,虚拟双核 64 位 CPU 和 10 GiB 内存,同样的程序可以运行。令我困扰的是,JVM 在两个系统上使用的 RAM 数量完全相同。 (我在 JAR 旁边捆绑了 OpenJDK 17.0.1,两种情况下使用相同的可执行文件。)

对我来说,这意味着我有一个 class 路径问题,但鉴于完全相同的测试布局在我的 VM 中工作,我倾向于认为它与 RAM 相关。然而,这更令人好奇,因为 JVM 的内存消耗完全相同。

遗憾的是,在生产机器上升级 RAM 不在讨论之列,但由于我不确定真正导致问题的原因(它实际上是 RAM 吗?)我想在开始之前确保我没有做明显的错误竭尽全力寻找 运行 这台机器上的程序的替代品。

无论如何,RAM 真的是罪魁祸首吗?或者是 CPU 时钟速度(生产机器上的 3.3 GHz 与 VM 中的 3.6 GHz)和 OkHttp 需要超过 1500 毫秒来初始化,因此在提交未来时 TimeoutException

在我的例子中,由于我已经设置了 1,500 毫秒的超时时间,并且由于 CPU 异常机器的时钟速度较慢,所以 class 在发生超时。事实证明,OkHttp 或多或少是罪魁祸首,在给定机器上安装客户端需要超过 5 秒。

总而言之,我不再在第一次尝试连接测试时应用任何超时来给 OkHttp 足够的时间来初始化自己。

请注意,如果 HTTPClient 的初始化在应用程序生命周期的不同点失败,这将无法解决问题。但是由于连接测试的第一次尝试是第一个调用HTTPClient的地方,所以这是唯一可以初始化的地方。