介绍
我们在客户办公室的 Java 小程序存在一些问题。该小程序旨在通过定期截屏来记录屏幕。它已签名,并且应该以提升的权限运行。
我们遇到的问题是:
- Java 小程序有时可能根本无法启动。如果 Java 控制台出现,它可能很快就会消失。感觉好像整个 JVM 都崩溃了。
- 如果 Java 小程序启动,它有时会在启动后 10 秒左右崩溃。
- 如果 Java 小程序启动,它几乎总是会遇到持续长达 12 秒的临时冻结。
- (小程序启动有点慢。)
一个典型的情况是,在启动 Windows 后,第一次尝试时无法加载小程序。刷新/重新登录/重新启动浏览器(我不完全确定需要什么)后,小程序启动,体验冻结然后崩溃。在第三次尝试时,它不会再崩溃,但仍然会冻结。
背景
客户有一个可能与该问题有关的 Blue Coat Systems HTTP 代理。网络的设置使得到 Internet 的所有流量都必须通过代理。例如,ping www.google.com
超时后会失败,因为主机 www.google.com 无法解析。我发现了一些描述 Java 和 Blue Coat 代理问题的页面。我不认为这些提供了太多有用的信息,但它们加强了我的信念,即代理与问题有关。
- Java 无法解析为 bluecoat 代理
- 访问 GoToAssist、GoToMeeting、GoToWebinar 或 GoToMyPC 时出现问题
- 访问 Salesforce.com 时出现问题
- 使用 Firefox 浏览器访问 RealTimeGraph 时出现 Java 错误。
代理需要 NTLM 身份验证,这意味着当浏览器第一次尝试通过时,我会收到一个弹出窗口,询问我的用户名和密码。这是我尝试使用 Chrome 访问 Google 时来自代理的 HTTP 响应的一部分:
HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: NTLM TlRMTVNTUAACAAAADAAMADgAAAAFgokC/+XiO/tpmQMAAAAAAAAAAKQApABEAAAABQLODgAAAA9KAEEATQBQAFQASQACAAwASgBBAE0AUABUAEkAAQASAFAASwBIAEsASQBEAEMAMAAxAAQAHgBwAHUAdQBrAGUAcwBrAHUAcwAuAGwAbwBjAGEAbAADADIAcABrAGgAawBpAGQAYwAwADEALgBwAHUAdQBrAGUAcwBrAHUAcwAuAGwAbwBjAGEAbAAFAB4AcAB1AHUAawBlAHMAawB1AHMALgBsAG8AYwBhAGwAAAAAAA==
环境
我在客户的场所用我自己的笔记本电脑亲身体验了这个问题。我的设置是在虚拟机中运行 Ubuntu Linux 12.04 和 Windows 7。我使用 Windows 和 Java 1.6.35 进行大部分测试。我确实在 Linux 中运行过一次小程序(我认为使用 Oracle Java 1.7.0_07)并且它没有遇到冻结,但小程序拍摄的屏幕截图可能是空白或损坏的。
我在 Java 设置中取消选中“在我的计算机上保留临时文件”框,因为缓存的 Java 小程序有时会干扰开发。我在客户办公室调试小程序时禁用了临时文件,但我认为此设置不会影响问题,因为我们的客户可能启用了临时文件并且他们总是遇到这些问题。
这些问题似乎与浏览器无关。我试过 IE9、Firefox、Chrome 和 Opera。
观察
在 Windows 中运行小程序时,我将 Eclipse 调试器附加到小程序。当小程序被冻结时,我暂停了它的执行,并检查了它在做什么。这是堆栈的样子。“OurOwnClass.doStillMoreSomething”是我们自己代码中正在执行的最后一行,它创建了一个类的新实例。
Thread [AWT-EventQueue-2] (Suspended)
Inet6AddressImpl.lookupAllHostAddr(String) line: not available [native method]
InetAddress$1.lookupAllHostAddr(String) line: not available
InetAddress.getAddressFromNameService(String, InetAddress) line: not available
InetAddress.getAllByName0(String, InetAddress, boolean) line: not available
InetAddress.getAllByName(String, InetAddress) line: not available
InetAddress.getAllByName(String) line: not available
InetAddress.getByName(String) line: not available
Handler(URLStreamHandler).getHostAddress(URL) line: not available
Handler(URLStreamHandler).hostsEqual(URL, URL) line: not available
Handler(URLStreamHandler).sameFile(URL, URL) line: not available
Handler(URLStreamHandler).equals(URL, URL) line: not available
URL.equals(Object) line: not available
JarVerifier$VerifierCodeSource(CodeSource).equals(Object) line: not available
JarVerifier$VerifierCodeSource.equals(Object) line: not available
HashMap<K,V>.getEntry(Object) line: not available
HashMap<K,V>.containsKey(Object) line: not available
HashSet<E>.contains(Object) line: not available
CPCallbackHandler.isTrusted(CodeSource) line: not available
CPCallbackHandler.access$1200(CPCallbackHandler, CodeSource) line: not available
CPCallbackHandler$ChildElement.checkResource(String) line: not available
DeployURLClassPath$JarLoader.checkResource(String, boolean, JarEntry, JarFile, DeployURLClassPath$PathIterator) line: not available
DeployURLClassPath$JarLoader.getResource(String, boolean, DeployURLClassPath$PathIterator) line: not available
DeployURLClassPath.getResource(String, boolean) line: not available
Plugin2ClassLoader$2.run() line: not available
AccessController.doPrivileged(PrivilegedExceptionAction<T>, AccessControlContext) line: not available [native method]
Applet2ClassLoader(Plugin2ClassLoader).findClassHelper(String) line: not available
Applet2ClassLoader.findClass(String, boolean) line: not available
Applet2ClassLoader(Plugin2ClassLoader).loadClass0(String, boolean, boolean) line: not available
Applet2ClassLoader(Plugin2ClassLoader).loadClass(String, boolean, boolean) line: not available
Applet2ClassLoader(Plugin2ClassLoader).loadClass(String, boolean) line: not available
Applet2ClassLoader(ClassLoader).loadClass(String) line: not available
OurOwnClass.doStillMoreSomething(String) line: 701
OurOwnClass.doMoreSomething() line: 671
OurOwnClass.doSometihng() line: 655
OurOwnClass.handleTimer() line: 510
OurOwnClass.actionPerformed(ActionEvent) line: 391
Timer.fireActionPerformed(ActionEvent) line: not available
Timer$DoPostEvent.run() line: not available
InvocationEvent.dispatch() line: not available
EventQueue.dispatchEventImpl(AWTEvent, Object) line: not available
EventQueue.access$400(EventQueue, AWTEvent, Object) line: not available
EventQueue$2.run() line: not available
EventQueue$2.run() line: not available
AccessController.doPrivileged(PrivilegedAction<T>, AccessControlContext) line: not available [native method]
AccessControlContext$1.doIntersectionPrivilege(PrivilegedAction<T>, AccessControlContext, AccessControlContext) line: not available
EventQueue.dispatchEvent(AWTEvent) line: not available
EventDispatchThread.pumpOneEventForFilters(int) line: not available
EventDispatchThread.pumpEventsForFilter(int, Conditional, EventFilter) line: not available
EventDispatchThread.pumpEventsForHierarchy(int, Conditional, Component) line: not available
EventDispatchThread.pumpEvents(int, Conditional) line: not available
EventDispatchThread.pumpEvents(Conditional) line: not available
EventDispatchThread.run() line: not available
有几行代码会冻结小程序。堆栈看起来与我们自己代码中的第一行相同(上图:OurOwnClass.doStillMoreSomething),所以看起来所有的冻结都有相同的原因。我们导致这个问题的所有行都在创建新对象。我相信在所有这些情况下,这也是第一次加载特定的类,尽管我没有检查这一点。其中一个类是我们自己的公共类,但其中大多数是匿名类,名称类似于 OurOwnClass$4,代码如下:
worker = new SwingWorker<Void,Void>() {
@Override
public Void doInBackground() {
// Do stuff.
return null;
}
};
堆栈顶部的 Java 方法是 Inet6AddressImpl.lookupAllHostAddr。调试器表明此方法正在尝试解析为小程序提供服务的主机的主机名。我想我在某处看到了我们的小程序 JAR 文件的完整 URL,所以我的印象是 JVM 试图再次访问我们的小程序 JAR 文件,即使它应该已经有了它——它还在执行什么?
分析
我最好的猜测是类加载器需要对即将加载的类进行某种权限检查。也许 JVM 正在尝试检查 JAR 文件自最初加载以来是否已更新,但 JVM 在尝试使用本机方法解析我们服务器的主机名时挂起。当地址解析失败时,类加载器的执行返回。这一切有点奇怪,因为小程序可以很好地与我们的服务器通信(它从浏览器获取代理信息),但我觉得 JVM 不会使用代理来完成它在这里所做的任何事情。这个解释听起来可信吗?
再说一次,我不知道如何解释崩溃以及让小程序首先启动的问题。我没有花时间尝试调试这些问题,所以我不知道发生了什么。如果我以后还去客户办公室,也许我应该尝试查找 hs_err_*.log 文件。
如果我有与客户类似的设置,这将有助于调试。我可能会尝试设置一个只能使用 HTTP 代理访问 Internet 但我不知道它是否必须是 Blue Coat 代理的环境。我也不确定是否可以使用 NTLM 协议而不是其他方法(基本、摘要)使代理需要身份验证。
在任何情况下,我都会感谢帮助摆脱冻结(如果可能的话,崩溃和启动问题)。我们能否以某种方式更改代码或 JAR 包以避免这些问题?是否有一些 JVM 启动参数可以提供帮助?我还应该做些什么来调查这个问题吗?
编辑
我更改了我的 Windows 虚拟机的网络设置,使其只能与主机 Linux 通信,而不是与整个 Internet 通信。然后我在主机 Linux 上设置了 Squid HTTP 代理,并告诉 Windows 使用它作为代理。现在我遇到了类似的问题,就像我们客户办公室发生的问题一样,虽然不那么可靠。看来我不需要让代理需要身份验证。
编辑 2
我使用了模拟环境并启动了 Wireshark 来查看 Windows 客户机正在尝试做什么。显然,它正在与主机打一种乒乓球:它向我们的服务器询问 ip 并返回响应“无法访问目标(无法访问端口)”(类型 3,代码 3)。这种情况发生了五次,它们之间的间隔是 1、1、2 和 4 秒。然后在 4 秒后,小程序继续愉快地做它正在做的任何事情。间隔总计为 12 秒,这恰好是最常见的延迟长度。我不知道我们客户办公室的网络是否响应主机无法访问或任何情况。Ping 会在那里和我的这个模拟设置中超时。
我想知道这是JVM中的错误还是设计错误。我通常不会期望实例化一个类会冻结线程一段时间。我还希望类加载器/JVM 使用代理。通常我会认为尝试几次主机名解析是有意义的,但我不确定在这种情况下这样做是否正确。
编辑 3
一位评论者是对的:是操作系统在打乒乓球。我通过捕获由生成的网络流量进行了尝试,ping www.google.com
并得到了相同的行为。