由 ServiceLoader 引发的CPU 100%
最近遇到2次由于 ServiceLoader 引起的 CPU 100%, 导致业务线程不能正常运行.
什么是 Service Loader
Spring 里面有个核心的概念, 就是依赖注入: 我期望有个服务, 但是一开始我并不指定具体的实现类, 等到我真正需要的时候, 这个依赖根据运行时自动注入. 同样, JDK 6 也引入了一个一样的实现框架, 就是 ServiceLoader. 它的实现也很简单. 使用的方法如下:
ServiceLoader<ServiceAPI> serviceLoader =ServiceLoader.load(ServiceAPI.class);
Iterator<ServiceAPI> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
ServiceAPI impl = iterator.next();
}
它的主要作用就是: 你需要那个服务的具体实现, 让我来帮你找, 可能找到一个或多个, 或找不到. 结果返回的是一个 Iterator.
如何找到具体的实现的?
如果某个 Jar 包提供某个服务的具体实现, 按照 JDK 定义的规则, 它就会在在 Jar 包的 META-INFO/services
文件夹提供一个名为某个service的文件, 文件的内容就是具体的实现类.
比如 xerceslmpl-x.x.x.jar 提供了 javax.xml.datatype.DatatypeFactory
的具体实现:
文件的内容就是本 jar 包里面的具体实现类的全名.
所以, 可以通过判断当前 jar 包里面的 META-INFO 文件夹下面是不是包含某个service 文件名来判断是不是有这个实现.
如何出问题的?
出问题的就是下面这行代码:
javax.xml.datatype.DatatypeFactory df = javax.xml.datatype.DatatypeFactory.newInstance();
就是要初始化一个xml 转换成 Java对象的类型工厂类, 如果去 JDK 里面查看这个类的源代码, 会发现其实它是一个抽象 Service. 运行时它有4种查找具体实现类的方法. 前2种都是通过配置, 第三种就是通过 ServiceLoader 去查找它的具体实现.
出问题的方式就是通过 ServiceLoader 的方式, 这种方式就是通过 ClassLoader 去查找所有的 Jar 包, 一个个去看有没有某个 jar 的 META-INFO/services 文件夹下面包含这么一个 service 的具体实现.
通常的实现的一个具体栈:
java.lang.Thread.State: RUNNABLE
at java.util.zip.ZipCoder.getBytes(ZipCoder.java:77)
at java.util.zip.ZipFile.getEntry(ZipFile.java:325)
- locked <0x00000007157ac988> (a java.util.jar.JarFile)
at java.util.jar.JarFile.getEntry(JarFile.java:253)
at java.util.jar.JarFile.getJarEntry(JarFile.java:236)
at sun.misc.URLClassPath$JarLoader.getResource(URLClassPath.java:1084)
at sun.misc.URLClassPath$JarLoader.findResource(URLClassPath.java:1062)
at sun.misc.URLClassPath$1.next(URLClassPath.java:281)
at sun.misc.URLClassPath$1.hasMoreElements(URLClassPath.java:291)
at java.net.URLClassLoader$3$1.run(URLClassLoader.java:609)
at java.net.URLClassLoader$3$1.run(URLClassLoader.java:607)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader$3.next(URLClassLoader.java:606)
at java.net.URLClassLoader$3.hasMoreElements(URLClassLoader.java:631)
at sun.misc.CompoundEnumeration.next(CompoundEnumeration.java:45)
at sun.misc.CompoundEnumeration.hasMoreElements(CompoundEnumeration.java:54)
at java.util.ServiceLoader$LazyIterator.hasNextService(ServiceLoader.java:354)
at java.util.ServiceLoader$LazyIterator.hasNext(ServiceLoader.java:393)
at java.util.ServiceLoader$1.hasNext(ServiceLoader.java:474)
at javax.xml.datatype.FactoryFinder$1.run(FactoryFinder.java:296)
at java.security.AccessController.doPrivileged(Native Method)
at javax.xml.datatype.FactoryFinder.findServiceProvider(FactoryFinder.java:292)
at javax.xml.datatype.FactoryFinder.find(FactoryFinder.java:268)
at javax.xml.datatype.DatatypeFactory.newInstance(DatatypeFactory.java:144)
通过上面的栈, 我们可以看到, 它其实是到 jar 到文件里面去看有没有这个项目, 没有就继续查找下一个.
这种方式相对消耗CPU到, 因为每次都要查找所有的jar 包, 一个个去查看压缩jar里面有没有这个文件. 如果以线上项目有2百多个jar, 查找一次要消耗即使毫秒.
但是, 即便这样, 还打不到让CPU很高的程度.
如何推高 CPU 的?
如果大家查看上面的线程栈, 其实在遍历某个jar 之前, 外层的遍历其实是遍历一些 ClassLoader, 然后每个 ClassLoader 都会有一些 Jar, 然后再遍历这些 jar.
其实真正出问题的是在 TomcatEmbeddedWebappClassLoader 里面. 这个 ClassLoader 在遍历每个Jar 的时候, 如果没有对应的 service 具体实现的 META-INFO/services 文件, 它会抛出一个 FileNotFoundException, 既然有 Exception, 就会有回溯栈, 就会非常耗时, 甚至进入C 代码. 看下面的火焰图: