【译】Java的类加载会带来哪些性能问题?如何解决?转载
java.lang.ClassLoader #loadClass() API被 3rd 方库、JDBC 驱动程序、框架、应用服务器用于将 java 类加载到内存中。应用程序开发人员不经常使用此 API。然而,当他们使用诸如“java.lang.Class.forName()”或“org.springframework.util.ClassUtils.forName()”之类的API时,他们在内部调用这个“java.lang.ClassLoader#loadClass()”API .
在运行时在不同线程中频繁使用此 API 会降低应用程序的性能。有时它甚至可以使整个应用程序无响应。在这篇文章中,让我们更多地了解这个 API 及其对性能的影响。
'ClassLoader.loadClass()' API 的目的是什么?
通常,如果我们想实例化一个新对象,我们会这样编写代码:
1. new io.ycrash.DummyObject();
但是,您可以使用 ClassLoader.loadClass() API 并实例化对象。以下是代码的外观:
1. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
2. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
3. myClass.newInstance();
您可以注意到在第 2 行中调用了“classLoader.loadClass()”。此行会将“io.ycrash.DummyObject”类加载到内存中。在第 3 行中,“io.ycrash.DummyObject”类使用“newInstance()”API 进行实例化。
这种实例化对象的方式就像用手触摸鼻子,穿过脖子后面。你可能想知道为什么有人会这样做?仅当您在编写代码时知道类的名称时,才能使用“new”实例化对象。在某些情况下,您可能仅在运行时才知道类的名称。例如,如果您正在编写框架(如 Spring Framework、XML 解析器等),您将知道仅在运行时要实例化的类名。在编写代码时,您将不知道要实例化哪些类。在这种情况下,您最终将不得不使用“ClassLoader.loadClass()”API。
在哪里使用“ClassLoader.loadClass()”?
'ClassLoader.loadClass()' 用于几个流行的 3rd 方库、JDBC 驱动程序、框架和应用程序服务器。本节重点介绍一些使用“ClassLoader.loadClass()”API 的流行框架。
阿帕奇夏兰
当您使用Apache Xalan框架序列化和反序列化 XML 时,将使用“ClassLoader.loadClass()”API。下面是使用 Apache Xalan 框架中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
at java.lang.ClassLoader.loadClass(ClassLoader.java:404)
- locked <0x6d497769> (a com.wm.app.b2b.server.ServerClassLoader)
at com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1175)
at com.wm.app.b2b.server.ServerClassLoader.loadClass(ServerClassLoader.java:1108)
at org.apache.xml.serializer.ObjectFactory.findProviderClass(ObjectFactory.java:503)
at org.apache.xml.serializer.SerializerFactory.getSerializer(SerializerFactory.java:129)
at org.apache.xalan.transformer.TransformerIdentityImpl.createResultContentHandler(TransformerIdentityImpl.java:260)
at org.apache.xalan.transformer.TransformerIdentityImpl.transform(TransformerIdentityImpl.java:330)
at org.springframework.ws.client.core.WebServiceTemplate$4.extractData(WebServiceTemplate.java:441)
:
:
谷歌 GUICE 框架
当您使用Google GUICE 框架时,将使用 'ClassLoader.loadClass()' API。下面是使用 Google GUICE 框架中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
at java.lang.Object.wait(Native Method)
- waiting on hudson.remoting.RemoteInvocationHandler$RPCRequest@1e408f0
at hudson.remoting.Request.call(Request.java:127)
at hudson.remoting.RemoteInvocationHandler.invoke(RemoteInvocationHandler.java:160)
at $Proxy5.fetch2(Unknown Source)
at hudson.remoting.RemoteClassLoader.findClass(RemoteClassLoader.java:122)
at java.lang.ClassLoader.loadClass(ClassLoader.java:321)
- locked hudson.remoting.RemoteClassLoader@15c7850
at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
at com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:69)
at com.google.inject.internal.BindingProcessor.visit(BindingProcessor.java:43)
at com.google.inject.internal.BindingImpl.acceptVisitor(BindingImpl.java:93)
at com.google.inject.internal.AbstractProcessor.process(AbstractProcessor.java:56)
at com.google.inject.internal.InjectorShell$Builder.build(InjectorShell.java:183)
at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:104)
- locked com.google.inject.internal.InheritingState@1c915a5
at com.google.inject.Guice.createInjector(Guice.java:94)
at com.google.inject.Guice.createInjector(Guice.java:71)
at com.google.inject.Guice.createInjector(Guice.java:61)
:
:
Oracle JDBC 驱动程序
如果您使用Oracle JDBC Driver,将使用“ClassLoader.loadClass()”API。下面是使用 Oracle JDBC 驱动程序中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
at com.ibm.ws.classloader.CompoundClassLoader.loadClass(CompoundClassLoader.java:482)
- waiting to lock <0xffffffff11a5f7d8> (a com.ibm.ws.classloader.CompoundClassLoader)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:170)
at oracle.jdbc.driver.PhysicalConnection.safelyGetClassForName(PhysicalConnection.java:4682)
at oracle.jdbc.driver.PhysicalConnection.addClassMapEntry(PhysicalConnection.java:2750)
at oracle.jdbc.driver.PhysicalConnection.addDefaultClassMapEntriesTo(PhysicalConnection.java:2739)
at oracle.jdbc.driver.PhysicalConnection.initializeClassMap(PhysicalConnection.java:2443)
at oracle.jdbc.driver.PhysicalConnection.ensureClassMapExists(PhysicalConnection.java:2436)
:
:
AspectJ 库
如果您使用AspectJ 库,将使用 'ClassLoader.loadClass()' API。下面是使用 AspectJ 框架中的“ClassLoader.loadClass()”API 的线程的堆栈跟踪。
:
:
at java.base@11.0.7/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
at java.base@11.0.7/java.lang.Class.forName0(Native Method)
at java.base@11.0.7/java.lang.Class.forName(Class.java:398)
at app//org.aspectj.weaver.reflect.ReflectionBasedReferenceTypeDelegateFactory.createDelegate(ReflectionBasedReferenceTypeDelegateFactory.java:38)
at app//org.aspectj.weaver.reflect.ReflectionWorld.resolveDelegate(ReflectionWorld.java:195)
at app//org.aspectj.weaver.World.resolveToReferenceType(World.java:486)
at app//org.aspectj.weaver.World.resolve(World.java:321)
- locked java.lang.Object@1545fe7d
at app//org.aspectj.weaver.World.resolve(World.java:231)
at app//org.aspectj.weaver.World.resolve(World.java:436)
at app//org.aspectj.weaver.internal.tools.PointcutExpressionImpl.couldMatchJoinPointsInType(PointcutExpressionImpl.java:83)
at org.springframework.aop.aspectj.AspectJExpressionPointcut.matches(AspectJExpressionPointcut.java:275)
at org.springframework.aop.support.AopUtils.canApply(AopUtils.java:225)
:
:
研究性能影响
现在我假设您已经对 Java 类加载有足够的了解。现在是时候研究它对性能的影响了。为了方便我们的学习,我创建了这个简单的程序:
1. package io.ycrash.classloader;
2.
3. public class MyApp extends Thread {
4.
5. @Override
6. public void run() {
7.
8. try {
9.
10. while (true) {
11.
12. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
13. Class<?> myClass = classLoader.loadClass("io.ycrash.DummyObject");
14. myClass.newInstance();
15. }
16. } catch (Exception e) {
17.
18. }
19. }
20.
21. public static void main(String args[]) throws Exception {
22.
23. for (int counter = 0; counter < 10; ++counter) {
24.
25. new MyApp().start();
26. }
27. }
28. }
如果你注意到这个程序,我在 main() 方法中创建了 10 个线程。
每个线程都进入一个无限循环并在 run() 方法中实例化 'io.ycrash.DummyObject',使用第 13 行中的 'classLoader.loadClass()' API。这意味着 'classLoader.loadClass()' 将所有这 10 个线程一次又一次地调用。
ClassLoader.loadClass() – 阻塞线程
我们执行了上面的程序。在程序执行时,我们运行了开源yCrash 脚本。此脚本从应用程序中捕获 360 度数据(线程转储、GC 日志、堆转储、netstat、VMstat、iostat、top、内核日志……)。我们使用线程转储分析工具fastThread分析dump文件,此工具为该程序生成的线程转储分析报告可在此处找到。工具报告 10 个线程中有 9 个处于 BLOCKED 状态。如果线程处于 BLOCKED 状态,则表明它被卡在了资源上。当它处于 BLOCKED 状态时,它不会前进。它会妨碍应用程序的性能。您可能想知道——为什么上面的简单程序会使线程进入 BLOCKED 状态。
图:显示 9 个 BLOCKED 线程的传递图(由fastThread生成)
以上是线程转储分析报告的摘录。您可以看到 9 个线程('Thread-0'、'Thread-1'、'Thread-2'、'Thread-3'、'Thread-4'、'Thread-5'、'Thread-7'、' Thread-8', 'Thread-9') 被 'Thread-6' 阻塞。下面是一个 BLOCKED 状态线程(即 Thread-9)的堆栈跟踪:
Thread-9
Stack Trace is:
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(ClassLoader.java:404)
- waiting to lock <0x00000003db200ae0> (a java.lang.Object)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at io.ycrash.classloader.MyApp.run(MyApp.java:13)
Locked ownable synchronizers:
- None
您会注意到 java.lang.ClassLoader.loadClass() 方法中的“Thread-9”被 BLOCKED。它正在等待获取“<0x00000003db200ae0>”上的锁定。所有其他处于 BLOCKED 状态的其余 8 个线程也具有完全相同的堆栈跟踪。
下面是阻塞所有其他 9 个线程的 'Thread-6' 的堆栈跟踪:
Thread-6
java.lang.Thread.State: RUNNABLE
at java.lang.ClassLoader.findLoadedClass0(Native Method)
at java.lang.ClassLoader.findLoadedClass(ClassLoader.java:1038)
at java.lang.ClassLoader.loadClass(ClassLoader.java:406)
- locked <0x00000003db200ae0> (a java.lang.Object)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at io.ycrash.classloader.MyApp.run(MyApp.java:13)
Locked ownable synchronizers:
- None
您可以注意到'Thread-6' 能够获得锁(即'<0x00000003db200ae0>')并继续前进。但是,所有其他 9 个线程都在等待获取此锁。
为什么调用 ClassLoader.loadClass() 时线程会阻塞?
要了解为什么线程在调用 'ClassLoader.loadClass()' 方法时会进入 BLOCKED 状态,我们将不得不查看它的源代码。下面是 ClassLoader.loadClass() 方法的源代码摘录。如果您想查看 java.lang.ClassLoader 的完整源代码,可以参考这里:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
:
:
在源代码突出显示的行中,你将看到“同步”代码块的用法。当一个代码块被同步时,只允许一个线程进入该块。在我们上面的示例中,有 10 个线程正在尝试同时访问“ClassLoader.loadClass()”。只允许一个线程进入同步代码块,其余 9 个线程将进入 BLOCKED 状态。
下面是“getClassLoadingLock()”方法的源代码,它返回一个对象并在该对象上发生同步。
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
你会注意到,“getClassLoadingLock()”方法每次都会为相同的类名返回相同的对象。即如果类名是'io.ycrash.DummyObject'——它每次都会返回相同的对象。因此,所有 10 个线程都将返回同一个对象。在这一个对象上,将发生同步。它会将所有线程置于 BLOCKED 状态。
如何解决这个问题?
之所以会出现此问题,是因为在每次循环迭代时都会再次加载“io.ycrash.DummyObject”类。这会导致线程进入 BLOCKED 状态。如果我们在程序启动期间只能加载一次类,则可以解决此问题。这可以通过如下所示修改代码来实现。
1. package io.ycrash.classloader;
2.
3. public class MyApp extends Thread {
4.
5. private Class<?> myClass = initClass();
6.
7. private Class<?> initClass() {
8.
9. try {
10. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
11. return classLoader.loadClass("io.ycrash.DummyObject");
12. } catch (Exception e) {
13. }
14.
15. return null;
16. }
17.
18. @Override
19. public void run() {
20.
21. while (true) {
22.
23. try {
24. myClass.newInstance();
25. } catch (Exception e) {
26. }
27. }
28. }
29.
30. public static void main(String args[]) throws Exception {
31.
32. for (int counter = 0; counter < 10; ++counter) {
33.
34. new MyApp().start();
35. }
36. }
37. }
进行此代码更改解决了该问题。如果你现在看到 'myClass' 在第 5 行被初始化。与之前的方法不同,myClass 在每次循环迭代时都初始化,现在 myClass 在实例化线程时只初始化一次。由于代码中的这种转变,'ClassLoader.loadClass()' API 将不会被多次调用。因此它将阻止线程进入 BLOCKED 状态。
解决方案
如果您的应用程序也遇到这个类加载性能问题,那么这里是解决它的潜在解决方案。
1,尝试查看是否可以在应用程序启动时而不是运行时调用“ClassLoader.loadClass()”API。
2,如果程序在运行时一次又一次地加载同一个类,则尝试仅加载一次该类。在那之后,缓存该类并重新使用它,如上例所示。
3,使用诸如fastThread、yCrash等故障排除工具来检测是哪个框架或 3rd 方库或代码路径触发了问题。检查框架是否在其最新版本中提供了任何修复,如果有,请升级到最新版本。
原文地址:https://blog.fastthread.io/2022/05/03/java-class-loading-performance-impact/
原文作者:fastthread