介绍
最近(201506),碰到一个关于在java 8_45下加载applet非常缓慢的问题。而java 1.7(如 1.7.0_51)没有这个问题。
本文章将介绍该问题调查过程与结论,包括此间涉及的相关细节。
基本上涉及以下方面的知识
1) JVM CPU profiling - JVisualVM
2) DNS
3) Java 8 关于Applet的更改
实际工作中的问题可能是比较复杂的。如本文中的问题,它不仅是简单的检查cpu使用,还涉及DNS解析、新版JDK引入的对既有行为的改变等。忽然想起一句话:没有人会像教科书上讲的那样生病。
有道云笔记链接: http://note.youdao.com/share/?id=1e426753c042c0b420be2ccd2259dad7&type=note
根本原因
1)Applet在加载第一个图片(toolkit.getImage)时,出现SocketPermission。此过程时间长度不定,有时很快(1秒内),有时10秒+.
此过程为crossdomain检查是,JDK(1.8.0_45)调用了DNS解析(参考下面章节中的调用栈)。此 解析被JDK8自身新引入的RIA permission变化所拦截,导致无法解析。异常信息 如:java.security.AccessControlException: access denied ("java.net.SocketPermission" "your_domain_name.com" "resolve") 。
2)在加载接下来的其他图片(还是toolkit.getImage)时,每个图片5秒钟时间,共30+个图片。作为客户,算是等的花儿已谢了。
是由于JDK调用了DNS反向解析(从ip 查询域名)- java.net.Inet64AddressImpl.getHostByAddr。此解析失败。
第N(N>=2)此加载图片,JDK的调用分支改变为调用DNS反向解析,而非1)中的正向解析,可能是记住了上次的permission检查失败的状态,此不同可以通过比较上面1)中AccessControlException的调用栈,和 本2)中的cpu profiling结果得到。
注意:虽然加载图片出现了异常或者timeout,图片最终还是成功加载。具体原因也将在后面所描述。
关于java 8引入的Permision,请参考
解决方案
暂时没有最终完美解决方案
1 将所有图片文件写入一个zip包,applet读取的时候缓存整个zip包
那么,加载很多图片文件不会引起多次5秒timeout。因为只有一次获取zip文件的过程。
或者
2 将域名加入本地hosts文件
或者
3 更改本地java.policy,加入socketpermission,允许resolve
接下来将详细解释如何发现的这个问题,其过程比较曲折。
步骤
1. 重现问题
本机安装1.8.0_45, 访问测试环境的applet。重现成功! 注意:测试环境的url也使用域名,而非ip。域名解析是使用公司内部的域名服务器。
同时,还通过Eclipse ,以调用application的形式试图重现,发现其在非sandbox环境下,没有问题!
2.检查java console log
感谢写图片加载的同事,每个图片加载前和加载成功分别打印了log。由此成功发现了直接原因 - 即加载每个图片都花费了5秒钟之间。
目前可以定位到具体代码行。
大概看了一下,只有两行实际调用的程序,如下:
print 'begin loading' URL URL = this.getClass().getResource(imageFileName); iconImg = Toolkit.getDefaultToolkit().getImage(URL); pring 'load done'
都是jdk内部类,使用方法也没看到什么不妥的地方,而且只有jdk8有问题,jdk7没问题,此中必有隐情(元芳,你怎么看).
3. 明确到底是上面哪行代码(getResource, OR getImage )出了问题, by JVisualVM Sampler
同时确定线程名称,对接下来的profiling JDK的时候,快速定位有帮助。否则jdk的profiling结果老长老长。
JVisualVM的操作步骤很简单
1) 网页打开applet 2) visualvm挂载applet 3) 在load图片之前,点击 Sampler -> CPU 4) 等图片load结束以后,点击 'Snapshot'保存当前profiling结果。Snapshot可以作为单独文件保存,使用jvisualvm随时查看。
备注:为什么使用Sampler,而非Profilger标签。因为Sampler简单实用。关于 Sampler标签与Profiler标签区别,可以参考http://stackoverflow.com/questions/12130107 /difference-between-sampling-and-profiling-in-jvisualvm
检查也很简单,上图
此图明确的告诉我们,getImage花费了146秒。它就是真凶,但为什么呢?由于再往里的调用均为JDK内部的实现,我们需要更改JVisualVM的设置,才能捕捉到。下一个步骤就做这个事情。
4. 检查JVM里面到底在做什么, by JVisualVM Sampler
JVisualVM的操作步骤与上面一样,除了在点击 ‘CPU' 进行profiling之前,设定Settings 为 只检查 java.*. javax.*, sun.* 等等(这是java 8 visualvm 默认的).
检查结果如下(好深的调用! 要尽量避免如此深的调用栈。)
看来是在进行DNS反向解析的时候,5秒钟TimeOut了(这个5秒是另外一个同事告诉我的,他调查过Java7的一个相关问题)。
备注:为啥调用URL.hashCode就扯出这么长的一段调用呢。URL应该设计成不可变类。我们写程序要注意,尽量多用。快速看了一下java.net.URL类,只有StreamHandler可以修改它,其他情况下,它是不可变的。继续上图
备注:其实在上面检查步骤的同时,我通过google查询了java 8的关于applet的release note,以及其他人是否碰到过加载applet缓慢的问题。
--java 8在applet permission方面,增加了一些限制(https://docs.oracle.com/javase/8/docs/technotes /guides/deploy/whatsnew_deployment.html),我粗粗看了一下,认为与此无关(我错了!)。想起一篇文章《Tomcat7连接数异常导致超时问题的排查》,文中作者碰到问题,应用了很多很厉害的troubleshooting过程,最后定位到根本原因。但是他最后也发现其实log中已经有有偶发的StackOverflowError。我猜测也是log太多,这行log没有得到及时发现。否则可能更快的找到问题。
--有人在加载文件(jar in jar)时,加载缓慢(http://stackoverflow.com/questions/28504943/java-sound- dramatically-slower-after-jvm-8-update)。我们的applet不是这个情况,也排除了。
此处,我想说明的是,要从内向外(profiler),和从外向内(java 8 release note)双向调查。不要拘泥于profiling。一头扎进profiling,可能会使你迷失方向。
("java.net. SocketPermission" "your_domain_name" "connect,accept, resolve")
)
1)在加载第一个图片(toolkit.getImage())时,JDK(1.8.0_45)调用意外的调用 了DNS解析(参考下面章节中的调用栈)。而此解析被JDK8自身新引入的RIA permission变化所拦截,导致无法解析。异常信息 如:java.security.AccessControlException: access denied ("java.net.SocketPermission" "your_domain_name.com" "resolve") 。 此过程时间长度不定,有时很快(1秒内),有时10秒+.
2)在加载接下来的其他图片时,JDK记住了上次的permission检 查失败(原因未明,但与sun.plugin2.applet.SecurityManager.checkConnectionHelper有关),而 改变了调用分支,最终调用了 DNS反向解析(从ip 查询域名)- java.net.Inet64AddressImpl.getHostByAddr。此解析失败,有5秒钟的time out时间。
每个图片5秒钟,共30+个图片,共花了150秒左右。作为客户,算是等的花儿已谢了。
[深入:为什么第一次解析以后不存下来,后面每次重新解析一次导致了每个5秒的延时?]
因为:第一次压根就没成功,以后任意一次也都没有成功!
不管怎样,临时解决方案出来了,虽然很Ugly。把域名加到客户本地的hosts文件。客户是不会同意的,但技术上是可行的(已验证)